@1aboveio/cicd-skills 0.1.21 → 0.1.22

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.
@@ -43,11 +43,16 @@ Queue states worth recognising:
43
43
 
44
44
  ```bash
45
45
  gh pr checks <PR> # check-run table, exit 1 if any failed
46
- gh pr checks <PR> --watch # block until they settle
47
46
  gh pr view <PR> --json state,isDraft,mergeable,mergeStateStatus,statusCheckRollup
48
47
  gh pr view <PR> --json labels --jq '.labels[].name' # queued / dequeued labels
49
48
  ```
50
49
 
50
+ A one-shot table is the right tool here. Waiting is not: `gh pr checks <PR> --watch` repaints
51
+ its table and its elapsed clock every 30 seconds, which reads as progress to anything watching
52
+ the log while the actual CI steps slide out of view. To wait for CI, run the delivery watcher
53
+ ([`watch-contract.md`](watch-contract.md)) — or, for a PR outside a queue, the `github-flow`
54
+ skill's `scripts/watch-checks.mjs`, which emits one record per real check-state change.
55
+
51
56
  Mergify labels a PR `queued` on entry and `dequeued` on removal
52
57
  (`merge_queue.queued_label` / `dequeued_label`). These are **lifecycle** signals,
53
58
  not a cheap membership check — what they mean depends on what you saw before
@@ -49,7 +49,7 @@ Two of the four were written *after* the agent had been told to check for dequeu
49
49
 
50
50
  ### `phases` — the progression as output
51
51
 
52
- The watcher emits `phases`: the ordered list of lifecycle phases actually observed, on both the terminal verdict and every heartbeat. It resolves the one ambiguity the terminal outcome cannot:
52
+ The watcher emits `phases`: the ordered list of lifecycle phases actually observed, on both the terminal verdict and every transition record. It resolves the one ambiguity the terminal outcome cannot:
53
53
 
54
54
  | terminal | `phases` | meaning | fix |
55
55
  |---|---|---|---|
@@ -181,7 +181,8 @@ so the newest batch wins over stale comment fallback.
181
181
  - **Run the bundled script. Do not hand-roll a watcher** — see the progression section above for why the hand-rolled shape cannot hold the contract, whatever care goes into the code. (If you are reading a legacy shell watcher: avoid shell-reserved variable names such as `status`.)
182
182
  - Keep enqueue and watch separate. A watch observes only; it does not post `@mergifyio queue`, `@mergifyio dequeue`, or merge.
183
183
  - Confirm the PR is queued, or can become queued, *before* launching the watch — see [Enqueue Before You Watch](#enqueue-before-you-watch). A watch that cannot enqueue, on a PR nothing else will enqueue either, can only end `STALLED`.
184
- - Emit an observation record every poll, not only on the terminal verdict. `NOT_YET_QUEUED` and `QUEUED` are both non-terminal and cover the entire healthy path, so a watcher that speaks only at the end is silent for essentially its whole life — indistinguishable, to the caller trying to decide whether delivery is progressing, from a hung or dead process. Keep the heartbeat off the machine-contract stream: stdout carries exactly the one terminal JSON document callers parse, stderr carries the per-poll records.
184
+ - Emit the initial observation, then delivery-state or provider-readability transitions. Repeated identical polls stay quiet. A process supervisor counts new log bytes as activity even when a JSON record says `progress: false`, so labelling every heartbeat is insufficient. The initial record makes a waiting watch visible; the terminal verdict and existing no-change bounds explain how it ended. stdout remains exactly one terminal JSON document; stderr carries transition records and diagnostics.
185
+ - Mark observation records `activity: true`; set `progress`/`changed` only when the canonical delivery state changed. Provider readability changes remain visible without resetting that change clock. `at`, `sinceChangeMs` and `elapsedMs` are diagnostic clocks, never progress. Keep the queue memory, readable-source snapshots, and existing stall bounds across quiet polls.
185
186
  - Bound on stalled state, not total elapsed time alone. `waiting_for_previous_batches` can be slow but healthy if position/state changes.
186
187
 
187
188
  ## Script
@@ -203,14 +204,14 @@ Options:
203
204
  | `--admission-stall-minutes` | `3` | pre-admission terminal stall bound — no observable change *before this process has observed membership* is `STALLED` with `reason: "admission-stall"`. Shorter on purpose: after enqueue, queue entry should appear quickly, and a dead admission path must not wait out the full post-entry bound. Capped at `--stall-minutes` so a caller that shortens the post-entry bound keeps both phases tight. Applies only while no non-Mergify check is pending; while CI runs, the post-entry bound applies, so a CI-gated admission needs no relaunch |
204
205
  | `--max-minutes` | `60` | absolute safety ceiling; bounds the watch even while the queue keeps reporting change, so a busy queue whose position keeps moving cannot keep the stall clock resetting forever. **60 minutes is also a hard maximum** — a larger value exits `2` rather than being silently clamped, because a caller that believes it has a four-hour watch and has one hour is worse off than one told plainly to relaunch. An hour without a verdict is a queue to hand back to a human or relaunch. The sibling `resolve-issues` watcher enforces the same ceiling; a cap honoured by one of two watchers is not a cap |
205
206
  | `--slow-job-minutes` | `5` | warn when a single CI job runs longer than this (see [CI profile](#ci-profile)) |
206
- | `--quiet` | off | suppress the per-poll progress heartbeat on stderr (slow-job warnings and the closing profile summary still print) |
207
+ | `--quiet` | off | suppress delivery/readability transition records on stderr (slow-job warnings and the closing profile summary still print) |
207
208
 
208
209
  Output streams:
209
210
 
210
211
  | stream | content |
211
212
  |---|---|
212
213
  | stdout | exactly one pretty-printed terminal JSON verdict, at exit — carrying `phases`, `seenQueued`, the outcome, and `ciProfile` |
213
- | stderr | one compact JSON record per poll (`progress: true`), with the live outcome, PR labels, queue membership/position, `phases`, `changed`, and `sinceChangeMs` — the stall clock made visible; plus one `{"warning":"slow-ci-job"}` record per slow CI job; and, last, one `{"ciProfileSummary":true}` line after the verdict |
214
+ | stderr | one compact JSON record per delivery/readability transition, marked `activity: true` (an observation was emitted) with `progress` set to whether the delivery state actually changed, plus the live outcome, PR labels, queue membership/position, `phases`, `changed`, and `sinceChangeMs` — the stall clock made visible; plus one `{"warning":"slow-ci-job"}` record per slow CI job; and, last, one `{"ciProfileSummary":true}` line after the verdict |
214
215
 
215
216
  Each regular-poll subprocess read (`gh pr view`, `mergify queue show`) also carries its own bounded
216
217
  timeout — a small multiple of `--interval-seconds`, clamped to never exceed the **effective
@@ -265,9 +266,11 @@ returns the PR's check runs with their start and finish times.
265
266
  `{"ciProfileSummary":true,"outcome","verdict","jobCount","slowJobMinutes","slowest","slowJobs"}`,
266
267
  with at most three slow jobs named as `"<name> <sec>s"`. The verdict is pretty-printed over
267
268
  many lines, so a short log tail lands inside `phases` and misses the profile; this line is
268
- what a 10-line tail shows. A background runner that reports only the exit code (Pi's
269
- `bg_task_status`) shows neither: read the log tail (`bg_task_log`) when the watch ends, and
270
- report the profile. Identity #894 (2026-09-29) warned about a 338s job that no one read.
269
+ what a 10-line tail shows. A background runner that reports only the exit code shows
270
+ neither: read the log tail when the watch ends, and report the profile. Diagnose movement
271
+ from the emitted `progress`/`outcome` and phases. Repeated polls stay quiet, so a process
272
+ supervisor cannot mistake periodic heartbeat writes for progress. Identity #894
273
+ (2026-09-29) warned about a 338s job that no one read.
271
274
 
272
275
  Profiling is observation only: it never changes the outcome or the exit code. A slow-job
273
276
  warning is a lead for whoever owns the CI (split the job, cache, move it to a nightly lane) —
@@ -319,9 +319,8 @@ export const DEFAULT_SLOW_JOB_MINUTES = 5
319
319
  //
320
320
  // Over-ceiling values are REFUSED (exit 2), not silently clamped. Clamping would let a caller
321
321
  // believe it had a four-hour watch while getting one hour, and the two streams here are already
322
- // committed: stdout is exactly one terminal JSON verdict, stderr is one NDJSON heartbeat per
323
- // poll, so there is no channel for a warning that would not break a documented contract. An
324
- // exit-2 usage error is loud, immediate, and matches how every other bad bound is handled.
322
+ // committed: stdout is exactly one terminal JSON verdict; stderr contains NDJSON transitions
323
+ // and diagnostics. An exit-2 usage error is loud, immediate, and matches other bad bounds.
325
324
  export const MAX_MINUTES_CEILING = 60
326
325
 
327
326
  export function readTimeoutMs(intervalSeconds, boundMinutes, readsPerPoll = 1) {
@@ -730,28 +729,12 @@ export function classifyDequeue({ rawReason = null, failedChecks = [] } = {}) {
730
729
  return 'unknown'
731
730
  }
732
731
 
733
- // Every poll's observation, as one compact NDJSON record — the watcher's heartbeat.
734
- //
735
- // Before this existed the loop's ONLY output sites were the three terminal ones (`MERGED`/
736
- // `DEQUEUED`/`CLOSED_UNMERGED` below, plus the two `STALLED` paths), so a watch sitting in
737
- // `NOT_YET_QUEUED` or `QUEUED` — the overwhelmingly common case, and the entire healthy path —
738
- // wrote zero bytes for its whole life. Reproduced live on PR #1396: 25 minutes of watching a
739
- // PR that was never queued produced a log containing only the harness's own spawn banner, and
740
- // the watch was killed 4 minutes short of the then-30-minute stall bound, so it never printed
741
- // even its terminal verdict. (The bound is 10 now, so that same watch would settle on its own
742
- // well before a human gave up on it — but the heartbeat is what makes the wait legible while
743
- // it runs, and no bound short enough to substitute for it would be long enough to be correct.) A silent process is indistinguishable from a hung or dead one, which is
744
- // exactly the state the caller is trying to resolve, and the hand-written shell watchers this
745
- // script is meant to replace all printed a per-poll status line. Losing that on the way to a
746
- // correct state machine traded one failure (wrong answers) for another (no answers).
747
- //
748
- // Deliberately stderr, not stdout: stdout is the machine contract — exactly one pretty-printed
749
- // terminal JSON object, which callers parse whole — and interleaving heartbeats into it would
750
- // break every such caller. stderr is unparsed by the contract and is merged into the log by
751
- // every process supervisor that would run this, which is the place a human actually looks.
752
- // `terminal: false` and the separate stream both distinguish these from the real verdict.
732
+ // Emit the initial delivery snapshot and subsequent delivery/readability transitions.
733
+ // Repeated snapshots stay quiet: process supervisors count newly written bytes as activity
734
+ // even when a record says progress:false. A readability transition remains visible without
735
+ // resetting the delivery change clock. stdout remains one terminal JSON verdict.
753
736
  export function progressRecord({ payload, changed, sinceChangeMs, nowIso }) {
754
- return JSON.stringify({ ...payload, progress: true, changed, sinceChangeMs, at: nowIso })
737
+ return JSON.stringify({ ...payload, activity: true, progress: changed, changed, sinceChangeMs, at: nowIso })
755
738
  }
756
739
 
757
740
  // Defaults. `stallMinutes` is a NO-CHANGE bound, not an elapsed-time one — any observable
@@ -813,11 +796,12 @@ Options:
813
796
  --seen-queued Resume hint only: keep phases/callers aware the PR was queued.
814
797
  Does NOT seed lost-membership counting — membership must be
815
798
  observed in this process (everActive) before DEQUEUED can fire.
816
- --quiet Suppress the per-poll progress heartbeat on stderr
799
+ --quiet Suppress delivery/readability transition records on stderr
817
800
 
818
801
  Output:
819
802
  stdout exactly one pretty-printed terminal JSON verdict, at exit
820
- stderr one compact JSON progress record per poll (progress:true), unless --quiet;
803
+ stderr one compact JSON record per delivery/readability change (activity:true, progress:<the delivery state
804
+ actually changed>), unless --quiet;
821
805
  plus one {"warning":"slow-ci-job"} record per CI job that runs past
822
806
  --slow-job-minutes (emitted even with --quiet); and, last of all, one
823
807
  {"ciProfileSummary":true} line (outcome, profile verdict, slow jobs) so a
@@ -882,6 +866,7 @@ export async function runCli(argv) {
882
866
  let missingActiveReads = 0
883
867
  let stallObservationCache = null
884
868
  let lastChangeMs = startedMs
869
+ let lastReadability = null
885
870
  let observations = 0
886
871
  let phases = []
887
872
  let lastBatchPrNumber = null
@@ -890,7 +875,7 @@ export async function runCli(argv) {
890
875
  const warnedSlowJobs = new Set()
891
876
  const slowThresholdMs = args.slowJobMinutes * 60_000
892
877
  // Warn once per slow job run. Warnings go to stderr even under --quiet: they are the point
893
- // of profiling, and --quiet only silences the routine heartbeat.
878
+ // of profiling, and --quiet only silences routine transition records.
894
879
  const warnSlowJobs = () => {
895
880
  for (const { key, job } of newSlowJobs(ciJobs, { thresholdMs: slowThresholdMs, warned: warnedSlowJobs })) {
896
881
  warnedSlowJobs.add(key)
@@ -1025,12 +1010,12 @@ export async function runCli(argv) {
1025
1010
  lastChangeMs = Date.now()
1026
1011
  }
1027
1012
 
1028
- // Heartbeat before the bound checks below, so the last thing in the log before a STALLED
1029
- // verdict is the unchanging observation that produced it — see `progressRecord`.
1030
- if (!args.quiet) {
1013
+ const readability = JSON.stringify([pr.readable !== false, queue.readable !== false])
1014
+ if (!args.quiet && (changed || readability !== lastReadability)) {
1031
1015
  const now = Date.now()
1032
1016
  console.error(progressRecord({ payload, changed, sinceChangeMs: now - lastChangeMs, nowIso: new Date(now).toISOString() }))
1033
1017
  }
1018
+ lastReadability = readability
1034
1019
 
1035
1020
  const elapsedMs = Date.now() - startedMs
1036
1021
  if (elapsedMs >= args.maxMinutes * 60_000) {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: podman
3
3
  description: "Debug, design, or fix Podman: rootless/rootful containers, pods, podman-compose, the socket and systemd services, Docker-compatible API (DOCKER_HOST/CONTAINER_HOST), networking, volumes, SELinux labels, Docker migration, and CI/SSH provisioning of rootless sockets (user lingering, XDG_RUNTIME_DIR). Covers FailedToOpenSocket and systemctl --user DBUS/XDG errors."
4
- release-version: 0.50.1
4
+ release-version: 0.50.2
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # Podman Operations