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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a166543ba3eb11eba412ad6a1488205fdd51b11703eb4ea522eeb5727333d68b
4
- data.tar.gz: 12c496c7424ed9e70ef7228729e7f141bf284215982fe68f5e9d3f932415a861
3
+ metadata.gz: e4eb67b03367a19fc6064a2c043904d02d1cac933df0171c1a6ee07afbfc34dd
4
+ data.tar.gz: 3b16df7a68a3db78683194662013c0760ca2285513ad562c5adf6f12ac2220f0
5
5
  SHA512:
6
- metadata.gz: 940643ae6f6727d66ea805520d04d4fba76c1e6232c70489f74d67240c99dde23cedcde86295c452bb9bbddcea0f91905614bf0b98d5d6c882965d206e73809d
7
- data.tar.gz: 92674d70e31a78dd2f4bef0843df59a724d387d0a64865f6c756851724795123a1849316b2c63d8dc648f58476e2583917bb9b1095e89d3c04e3ab53b1076248
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 trigger/cancel
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
@@ -16,5 +16,5 @@ require_relative "forge_ci/compiler"
16
16
  # JSON-serializable intermediate representation (IR) that a scheduler and
17
17
  # runner can execute. Pure Ruby, stdlib only, no network, no processes.
18
18
  module ForgeCI
19
- VERSION = "0.4.0"
19
+ VERSION = "0.5.0"
20
20
  end
@@ -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. For manual runs, sign in with your web-UI
152
- account. Triggering requires an admin; members can view and cancel.
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forge-ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Azmi Muwahid