forge-ci 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +16 -0
- data/lib/forge_ci/cli.rb +125 -1
- data/lib/forge_ci.rb +1 -1
- data/skills/forge-ci/SKILL.md +35 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4eb67b03367a19fc6064a2c043904d02d1cac933df0171c1a6ee07afbfc34dd
|
|
4
|
+
data.tar.gz: 3b16df7a68a3db78683194662013c0760ca2285513ad562c5adf6f12ac2220f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19d3a8d7e869fa4682a4a3565fe52956df8de6769ea6d3f2574e523d5379d78d394457471ecda1a6eaa12590087f159c1ddda2dbe21bffc2f5cc9c44b133af40
|
|
7
|
+
data.tar.gz: 3a47c508f61583bd7fc84d3d96ad28f5b92e9379fbf2b9ca38741c1416c1b4fec7330a3a104906cb85378614b600880ef00e376ae13bbbb9666c80c975e32b89
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ separately.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.5.0 (2026-10-26)
|
|
10
|
+
|
|
11
|
+
- Add `forge-ci pipelines --project NAME` to discover existing automatic runs
|
|
12
|
+
with branch/SHA filters, plus `forge-ci status ID [--wait]` to inspect and
|
|
13
|
+
watch workflow/job states without triggering another run. Both support
|
|
14
|
+
saved OAuth sessions and JSON output. Add member-readable bounded project
|
|
15
|
+
pipeline listing API; existing status-by-ID API remains unchanged.
|
|
16
|
+
|
|
9
17
|
- Admin **Settings → Runners** shows per-process online/busy/offline presence,
|
|
10
18
|
hostname, implementation, class, trust pool, last contact and current job.
|
|
11
19
|
Updated Ruby and Go runners report via existing claim polls and job heartbeats;
|
data/README.md
CHANGED
|
@@ -95,6 +95,22 @@ for upgrade requirements and the 120-second offline threshold.
|
|
|
95
95
|
- **From any script or CI:** the same `forge-ci trigger` with the
|
|
96
96
|
`FORGE_CI_URL` / `FORGE_CI_ADMIN_TOKEN` environment variables.
|
|
97
97
|
|
|
98
|
+
Inspect automatically triggered runs without starting another pipeline:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
export FORGE_CI_URL=https://ci.example.com
|
|
102
|
+
forge-ci login
|
|
103
|
+
forge-ci pipelines --project owner/repo --branch main
|
|
104
|
+
forge-ci status 42
|
|
105
|
+
forge-ci status 42 --wait
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`pipelines` lists recent runs newest first; use an ID from that list with
|
|
109
|
+
`status` to see workflows/jobs. `--wait` watches that existing run and exits
|
|
110
|
+
nonzero if it fails or is canceled. Both commands accept `--json` and reuse
|
|
111
|
+
your saved login; members can inspect runs. Deploy the updated control plane
|
|
112
|
+
for pipeline listing.
|
|
113
|
+
|
|
98
114
|
Check a config locally first; the compiler needs only Ruby 3.0+. `gem install`
|
|
99
115
|
puts the `forge-ci` CLI on your PATH:
|
|
100
116
|
|
data/lib/forge_ci/cli.rb
CHANGED
|
@@ -36,7 +36,7 @@ module ForgeCI
|
|
|
36
36
|
[--manual] sign in to a forge-ci server
|
|
37
37
|
in the browser (OAuth); the
|
|
38
38
|
token is stored and picked
|
|
39
|
-
up by
|
|
39
|
+
up by server commands
|
|
40
40
|
(URL defaults to FORGE_CI_URL;
|
|
41
41
|
--manual pastes a code instead
|
|
42
42
|
of waiting on the browser)
|
|
@@ -54,6 +54,15 @@ module ForgeCI
|
|
|
54
54
|
checkout; --wait blocks until
|
|
55
55
|
it finishes and exits 1 if it
|
|
56
56
|
did not succeed)
|
|
57
|
+
pipelines --project P [--branch B] [--sha SHA] [--limit N]
|
|
58
|
+
[--json] [--url URL] [--token T] list existing pipelines
|
|
59
|
+
(default limit 20, max 100)
|
|
60
|
+
status <pipeline-id> [--wait] [--poll SECONDS] [--json]
|
|
61
|
+
[--url URL] [--token T] show pipeline/workflow/jobs;
|
|
62
|
+
--wait watches changes and
|
|
63
|
+
exits 1 on failure/cancel;
|
|
64
|
+
--json emits one final JSON
|
|
65
|
+
(poll default 5 seconds)
|
|
57
66
|
cancel <pipeline-id> [--url URL] [--token T]
|
|
58
67
|
cancel a running pipeline
|
|
59
68
|
skill install [--dir DIR] [--force] install the forge-ci agent
|
|
@@ -91,6 +100,8 @@ module ForgeCI
|
|
|
91
100
|
when "login" then login_command(argv)
|
|
92
101
|
when "logout" then logout_command(argv)
|
|
93
102
|
when "trigger" then trigger_command(argv)
|
|
103
|
+
when "pipelines" then pipelines_command(argv)
|
|
104
|
+
when "status" then status_command(argv)
|
|
94
105
|
when "cancel" then cancel_command(argv)
|
|
95
106
|
when "skill" then skill_command(argv)
|
|
96
107
|
when "-h", "--help", nil then help
|
|
@@ -360,6 +371,119 @@ module ForgeCI
|
|
|
360
371
|
end
|
|
361
372
|
end
|
|
362
373
|
|
|
374
|
+
# -- Read existing pipelines without triggering another run. --------------
|
|
375
|
+
|
|
376
|
+
def pipelines_command(argv)
|
|
377
|
+
options = { url: ENV["FORGE_CI_URL"], token: ENV["FORGE_CI_ADMIN_TOKEN"], limit: "20" }
|
|
378
|
+
parser = OptionParser.new do |opts|
|
|
379
|
+
opts.banner = "Usage: forge-ci pipelines --project P [--branch B] [--sha SHA] [--limit N] [--json] [--url URL] [--token T]"
|
|
380
|
+
opts.on("--project P", "project id or name on the server") { |v| options[:project] = v }
|
|
381
|
+
opts.on("--branch B", "filter by branch") { |v| options[:branch] = v }
|
|
382
|
+
opts.on("--sha SHA", "filter by commit sha") { |v| options[:sha] = v }
|
|
383
|
+
opts.on("--limit N", "maximum results (1-100, default 20)") { |v| options[:limit] = v }
|
|
384
|
+
opts.on("--json", "print JSON") { options[:json] = true }
|
|
385
|
+
opts.on("--url URL", "server base URL (env FORGE_CI_URL)") { |v| options[:url] = v }
|
|
386
|
+
opts.on("--token T", "bearer token (env FORGE_CI_ADMIN_TOKEN, or forge-ci login)") { |v| options[:token] = v }
|
|
387
|
+
opts.on("-h", "--help", "show this help and exit") do
|
|
388
|
+
@out.puts opts
|
|
389
|
+
return 0
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
parser.parse!(argv)
|
|
393
|
+
return usage_error("pipelines takes no positional arguments") unless argv.empty?
|
|
394
|
+
return usage_error("--project is required") if options[:project].to_s.empty?
|
|
395
|
+
if options[:project].match?(/\A\d+\z/) && options[:project].to_i.zero?
|
|
396
|
+
return usage_error("--project id must be positive")
|
|
397
|
+
end
|
|
398
|
+
unless options[:limit].match?(/\A\d+\z/) && (1..100).cover?(options[:limit].to_i)
|
|
399
|
+
return usage_error("--limit must be an integer between 1 and 100")
|
|
400
|
+
end
|
|
401
|
+
err = server_options_error(options)
|
|
402
|
+
return usage_error(err) if err
|
|
403
|
+
|
|
404
|
+
project_id = resolve_project_id(options)
|
|
405
|
+
query = URI.encode_www_form(options.slice(:branch, :sha, :limit))
|
|
406
|
+
result = api_request(options, :get, "/api/v1/projects/#{project_id}/pipelines?#{query}")
|
|
407
|
+
if options[:json]
|
|
408
|
+
@out.puts JSON.generate(result)
|
|
409
|
+
elsif result.fetch("pipelines").empty?
|
|
410
|
+
@out.puts "forge-ci: no pipelines found"
|
|
411
|
+
else
|
|
412
|
+
@out.puts "ID\tSTATUS\tBRANCH\tSHA"
|
|
413
|
+
result.fetch("pipelines").each do |pipeline|
|
|
414
|
+
@out.puts pipeline.values_at("id", "status", "branch", "sha").join("\t")
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
0
|
|
418
|
+
rescue OptionParser::ParseError => e
|
|
419
|
+
usage_error(e.message)
|
|
420
|
+
rescue ApiError => e
|
|
421
|
+
@err.puts "forge-ci: #{e.message}"
|
|
422
|
+
1
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def status_command(argv)
|
|
426
|
+
options = { url: ENV["FORGE_CI_URL"], token: ENV["FORGE_CI_ADMIN_TOKEN"], poll: 5.0 }
|
|
427
|
+
parser = OptionParser.new do |opts|
|
|
428
|
+
opts.banner = "Usage: forge-ci status <pipeline-id> [--wait] [--poll SECONDS] [--json] [--url URL] [--token T]"
|
|
429
|
+
opts.on("--wait", "watch until the pipeline finishes") { options[:wait] = true }
|
|
430
|
+
opts.on("--poll SECONDS", Float, "positive poll interval (default 5)") { |v| options[:poll] = v }
|
|
431
|
+
opts.on("--json", "print one JSON response (final response with --wait)") { options[:json] = true }
|
|
432
|
+
opts.on("--url URL", "server base URL (env FORGE_CI_URL)") { |v| options[:url] = v }
|
|
433
|
+
opts.on("--token T", "bearer token (env FORGE_CI_ADMIN_TOKEN, or forge-ci login)") { |v| options[:token] = v }
|
|
434
|
+
opts.on("-h", "--help", "show this help and exit") do
|
|
435
|
+
@out.puts opts
|
|
436
|
+
return 0
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
parser.parse!(argv)
|
|
440
|
+
id = argv.shift
|
|
441
|
+
unless id && id.match?(/\A\d+\z/) && id.to_i.positive?
|
|
442
|
+
return usage_error("status requires a positive integer <pipeline-id>")
|
|
443
|
+
end
|
|
444
|
+
return usage_error("status takes exactly one <pipeline-id> argument") unless argv.empty?
|
|
445
|
+
unless options[:poll].finite? && options[:poll].positive?
|
|
446
|
+
return usage_error("--poll must be a positive finite number of seconds")
|
|
447
|
+
end
|
|
448
|
+
err = server_options_error(options)
|
|
449
|
+
return usage_error(err) if err
|
|
450
|
+
|
|
451
|
+
last = {}
|
|
452
|
+
loop do
|
|
453
|
+
pipeline = api_request(options, :get, "/api/v1/pipelines/#{id.to_i}")
|
|
454
|
+
last = print_pipeline_status(pipeline, last) unless options[:json]
|
|
455
|
+
terminal = !%w[pending running].include?(pipeline["status"])
|
|
456
|
+
unless options[:wait] && !terminal
|
|
457
|
+
@out.puts JSON.generate(pipeline) if options[:json]
|
|
458
|
+
return options[:wait] && pipeline["status"] != "success" ? 1 : 0
|
|
459
|
+
end
|
|
460
|
+
sleep options[:poll]
|
|
461
|
+
end
|
|
462
|
+
rescue OptionParser::ParseError => e
|
|
463
|
+
usage_error(e.message)
|
|
464
|
+
rescue ApiError => e
|
|
465
|
+
@err.puts "forge-ci: #{e.message}"
|
|
466
|
+
1
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def print_pipeline_status(pipeline, last)
|
|
470
|
+
if last.empty?
|
|
471
|
+
@out.puts "forge-ci: branch #{pipeline["branch"]} sha #{pipeline["sha"]}"
|
|
472
|
+
end
|
|
473
|
+
states = { "pipeline #{pipeline["id"]}" => pipeline["status"] }
|
|
474
|
+
Array(pipeline["workflows"]).each do |workflow|
|
|
475
|
+
label = "workflow #{workflow["id"]} (#{workflow["name"]})"
|
|
476
|
+
states[label] = workflow["status"]
|
|
477
|
+
Array(workflow["jobs"]).each do |job|
|
|
478
|
+
states["#{label} job #{job["id"]} (#{job["name"]}[#{job["node_index"]}])"] = job["status"]
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
states.each do |label, status|
|
|
482
|
+
@out.puts "forge-ci: #{label}: #{status}" if last[label] != status
|
|
483
|
+
end
|
|
484
|
+
states
|
|
485
|
+
end
|
|
486
|
+
|
|
363
487
|
# -- forge-ci cancel <pipeline-id> ----------------------------------------
|
|
364
488
|
|
|
365
489
|
def cancel_command(argv)
|
data/lib/forge_ci.rb
CHANGED
data/skills/forge-ci/SKILL.md
CHANGED
|
@@ -148,14 +148,21 @@ commands in the job images.
|
|
|
148
148
|
|
|
149
149
|
## Run, watch and cancel
|
|
150
150
|
|
|
151
|
-
Pipelines normally start on push.
|
|
152
|
-
|
|
151
|
+
Pipelines normally start on push. Inspect that existing run before considering
|
|
152
|
+
`trigger`: triggering again creates duplicate work and can repeat deployments.
|
|
153
|
+
Sign in with your web-UI account. Triggering requires an admin; members can
|
|
154
|
+
list, inspect, watch and cancel.
|
|
153
155
|
|
|
154
156
|
```sh
|
|
155
157
|
export FORGE_CI_URL=https://ci.example.com
|
|
156
158
|
forge-ci login
|
|
157
159
|
# Over SSH/headless: forge-ci login --manual
|
|
158
160
|
|
|
161
|
+
# Inspect automatic runs; these commands never trigger a new pipeline.
|
|
162
|
+
forge-ci pipelines --project owner/repo --branch main
|
|
163
|
+
forge-ci status <id>
|
|
164
|
+
forge-ci status <id> --wait
|
|
165
|
+
|
|
159
166
|
# Starts a pipeline for the current branch and HEAD; push the commit first.
|
|
160
167
|
# --wait blocks and exits non-zero on failure.
|
|
161
168
|
forge-ci trigger .circleci/config.yml --project owner/repo --wait
|
|
@@ -163,6 +170,32 @@ forge-ci cancel <id>
|
|
|
163
170
|
forge-ci logout
|
|
164
171
|
```
|
|
165
172
|
|
|
173
|
+
`pipelines` lists the newest 20 runs (use `--limit 1..100`, `--branch`, or
|
|
174
|
+
exact `--sha` to narrow it). `status` prints workflow/job states; `--wait`
|
|
175
|
+
watches the selected ID and exits 0 on success, 1 on failure/cancellation.
|
|
176
|
+
A one-shot status read exits 0 even for a failed run. `--poll SECONDS` sets
|
|
177
|
+
watch interval (positive, default 5). Both commands accept `--json`; watching
|
|
178
|
+
with JSON emits only the final pipeline document. Members can read these
|
|
179
|
+
commands; pipeline listing requires the updated server.
|
|
180
|
+
|
|
181
|
+
For a pushed commit, select the exact run rather than watching an unrelated
|
|
182
|
+
latest pipeline:
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
forge-ci pipelines --project owner/repo --sha "$(git rev-parse HEAD)" --json
|
|
186
|
+
# Choose the returned pipeline ID matching the intended branch/run.
|
|
187
|
+
forge-ci status 42 --wait --poll 5
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
An empty list is not permission to trigger: check the server URL, project,
|
|
191
|
+
commit push and GitHub webhook delivery first. Multiple runs can share a SHA;
|
|
192
|
+
confirm the intended ID. `--sha` requires the full exact SHA, not its short form.
|
|
193
|
+
Check `forge-ci --help` for `pipelines` and `status` before using them: an older
|
|
194
|
+
installed gem may lack these commands even if OAuth login works. Upgrade to a
|
|
195
|
+
release containing them, or run the updated checkout with
|
|
196
|
+
`ruby -Ilib exe/forge-ci`. If listing returns `404`, verify the server deploy;
|
|
197
|
+
status by a known ID uses the existing API. Do not substitute `trigger --wait`.
|
|
198
|
+
|
|
166
199
|
Login uses authorization-code + PKCE. Credentials live per server in
|
|
167
200
|
`~/.config/forge-ci/credentials.json` (0600), or `FORGE_CI_CREDENTIALS`;
|
|
168
201
|
`XDG_CONFIG_HOME` changes the default config directory. Tokens refresh
|