@biffo/cli 0.250.4 → 0.251.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.250.4",
3
+ "version": "0.251.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -102,6 +102,60 @@ export const VERDICT = {
102
102
  FLEET_FAULT_UNEXPLAINED: 'fleet-fault-unexplained',
103
103
  /** Ran on a GitHub-hosted runner, so there is no fleet to correlate against. */
104
104
  NOT_SELF_HOSTED: 'not-self-hosted',
105
+ /**
106
+ * The job never ran. Not a gate rejecting the change, not a runner dying
107
+ * mid-job — GitHub's own control plane failed before the work began, or no
108
+ * runner was ever assigned. Re-run it; there is nothing in the repo to fix.
109
+ */
110
+ NEVER_RAN: 'never-ran',
111
+ }
112
+
113
+ /**
114
+ * A job that GitHub could not start, as distinct from one that ran and failed.
115
+ *
116
+ * Two shapes, both observed on 2026-08-06/07 during a critical GitHub Actions
117
+ * incident, and both previously adjudicated `real-failure` because
118
+ * `isRunnerKill` answers "did a runner die MID-job?" and neither of these got
119
+ * that far:
120
+ *
121
+ * 1. **Died in setup.** The only step is `Set up job` and it failed —
122
+ * `Failed to resolve action download info. Error: Service Unavailable`.
123
+ * A gate that genuinely rejects a change has run checkout and install
124
+ * first, so it always carries more steps than this.
125
+ * 2. **Never scheduled.** `conclusion: 'cancelled'` with **no steps at all**
126
+ * and no `runner_name`: the job sat in the queue until GitHub gave up.
127
+ * Observed at 28m51s against an empty fleet during the same incident.
128
+ *
129
+ * Why this is worth its own verdict rather than folding into `real-failure`:
130
+ * the whole point of this tool is that "red" and "your code is broken" are
131
+ * different claims. It cost three separate diagnoses in one session — a hunt
132
+ * for a secret in a file that had none, a hunt for a closing-keyword problem
133
+ * in a PR body that had none, and a hunt for a bug in a `*_pg.py` test that
134
+ * had never executed. The signature is one API call away in every case.
135
+ *
136
+ * Deliberately narrow. A job whose FIRST step failed but which has later steps
137
+ * is a normal failure — only a job that never got past setup counts, so a gate
138
+ * that legitimately fails fast is not laundered into an infrastructure excuse.
139
+ *
140
+ * @param {Array<{ conclusion?: string, steps?: Array<{ name?: string, conclusion?: string }>, runner_name?: string | null }> | null | undefined} jobs
141
+ * @returns {boolean}
142
+ */
143
+ export function neverRan(jobs) {
144
+ const stalled = (jobs ?? []).filter(
145
+ (job) => job.conclusion === 'failure' || job.conclusion === 'cancelled'
146
+ )
147
+ if (stalled.length === 0) return false
148
+ return stalled.every((job) => {
149
+ const steps = job.steps ?? []
150
+ // Never scheduled: cancelled with nothing to show for it.
151
+ if (steps.length === 0) return job.conclusion === 'cancelled'
152
+ // Died in setup: `Set up job` is the only step, and it failed.
153
+ return (
154
+ steps.length === 1 &&
155
+ steps[0]?.name === 'Set up job' &&
156
+ steps[0]?.conclusion === 'failure'
157
+ )
158
+ })
105
159
  }
106
160
 
107
161
  /**
@@ -187,6 +241,10 @@ export function evictionKilledJob(job, evictedAt) {
187
241
  * @returns {{ verdict: string, jobs: Array<{ name: string, instance: string | null, verdict: string, evictedAt: string | null }> }}
188
242
  */
189
243
  export function adjudicateRun(jobs, evictions) {
244
+ // Asked BEFORE `isRunnerKill`, which answers "did a runner die mid-job?" — a
245
+ // job that never started cannot have, so it would otherwise fall straight
246
+ // through to `real-failure` and send someone to read code that never ran.
247
+ if (neverRan(jobs)) return { verdict: VERDICT.NEVER_RAN, jobs: [] }
190
248
  if (!isRunnerKill(jobs)) return { verdict: VERDICT.REAL_FAILURE, jobs: [] }
191
249
 
192
250
  const lost = (jobs ?? []).filter((job) => job.conclusion === 'failure')
@@ -334,6 +392,10 @@ function main() {
334
392
  // because **2 is never a pass** here as everywhere else. "The runner died for
335
393
  // reasons unknown" must not read as "safe to re-run and move on"; that is the
336
394
  // fail-open this whole tool exists to close.
395
+ if (result.verdict === VERDICT.NEVER_RAN) {
396
+ console.log(' the job never started — GitHub could not schedule or set it up. Re-run it.')
397
+ process.exit(0)
398
+ }
337
399
  if (result.verdict === VERDICT.REAL_FAILURE) process.exit(1)
338
400
  if (result.verdict === VERDICT.SPOT_RECLAIMED) process.exit(0)
339
401
  process.exit(2)