@biffo/cli 0.250.3 → 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.
@@ -42,7 +42,8 @@
42
42
  "postcss@<8.5.18": ">=8.5.18",
43
43
  "sharp@<0.35.0": ">=0.35.0",
44
44
  "brace-expansion@<5.0.9": ">=5.0.9",
45
- "undici@<7.29.0": ">=7.29.0"
45
+ "undici@<7.29.0": ">=7.29.0",
46
+ "js-yaml@<4.3.1": ">=4.3.1 <5"
46
47
  }
47
48
  }
48
49
  }
@@ -9,6 +9,7 @@ overrides:
9
9
  sharp@<0.35.0: '>=0.35.0'
10
10
  brace-expansion@<5.0.9: '>=5.0.9'
11
11
  undici@<7.29.0: '>=7.29.0'
12
+ js-yaml@<4.3.1: '>=4.3.1 <5'
12
13
 
13
14
  importers:
14
15
 
@@ -1871,8 +1872,8 @@ packages:
1871
1872
  js-tokens@4.0.0:
1872
1873
  resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
1873
1874
 
1874
- js-yaml@4.3.0:
1875
- resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
1875
+ js-yaml@4.3.1:
1876
+ resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
1876
1877
  hasBin: true
1877
1878
 
1878
1879
  jsdom@30.0.1:
@@ -2937,7 +2938,7 @@ snapshots:
2937
2938
  globals: 14.0.0
2938
2939
  ignore: 5.3.2
2939
2940
  import-fresh: 3.3.1
2940
- js-yaml: 4.3.0
2941
+ js-yaml: 4.3.1
2941
2942
  minimatch: 3.1.5
2942
2943
  strip-json-comments: 3.1.1
2943
2944
  transitivePeerDependencies:
@@ -3974,8 +3975,8 @@ snapshots:
3974
3975
  '@typescript-eslint/parser': 8.62.1(eslint@9.39.4)(typescript@5.9.3)
3975
3976
  eslint: 9.39.4
3976
3977
  eslint-import-resolver-node: 0.3.10
3977
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4))(eslint@9.39.4)
3978
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4)
3978
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4)
3979
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4)
3979
3980
  eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4)
3980
3981
  eslint-plugin-react: 7.37.5(eslint@9.39.4)
3981
3982
  eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4)
@@ -3994,7 +3995,7 @@ snapshots:
3994
3995
  transitivePeerDependencies:
3995
3996
  - supports-color
3996
3997
 
3997
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4))(eslint@9.39.4):
3998
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4):
3998
3999
  dependencies:
3999
4000
  '@nolyfill/is-core-module': 1.0.39
4000
4001
  debug: 4.4.3
@@ -4005,22 +4006,22 @@ snapshots:
4005
4006
  tinyglobby: 0.2.17
4006
4007
  unrs-resolver: 1.12.2
4007
4008
  optionalDependencies:
4008
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4)
4009
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4)
4009
4010
  transitivePeerDependencies:
4010
4011
  - supports-color
4011
4012
 
4012
- eslint-module-utils@2.14.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4):
4013
+ eslint-module-utils@2.14.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4):
4013
4014
  dependencies:
4014
4015
  debug: 3.2.7
4015
4016
  optionalDependencies:
4016
4017
  '@typescript-eslint/parser': 8.62.1(eslint@9.39.4)(typescript@5.9.3)
4017
4018
  eslint: 9.39.4
4018
4019
  eslint-import-resolver-node: 0.3.10
4019
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4))(eslint@9.39.4)
4020
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4)
4020
4021
  transitivePeerDependencies:
4021
4022
  - supports-color
4022
4023
 
4023
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4):
4024
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4):
4024
4025
  dependencies:
4025
4026
  '@rtsao/scc': 1.1.0
4026
4027
  array-includes: 3.1.9
@@ -4031,7 +4032,7 @@ snapshots:
4031
4032
  doctrine: 2.1.0
4032
4033
  eslint: 9.39.4
4033
4034
  eslint-import-resolver-node: 0.3.10
4034
- eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4)
4035
+ eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4)
4035
4036
  hasown: 2.0.4
4036
4037
  is-core-module: 2.16.2
4037
4038
  is-glob: 4.0.3
@@ -4480,7 +4481,7 @@ snapshots:
4480
4481
 
4481
4482
  js-tokens@4.0.0: {}
4482
4483
 
4483
- js-yaml@4.3.0:
4484
+ js-yaml@4.3.1:
4484
4485
  dependencies:
4485
4486
  argparse: 2.0.1
4486
4487
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.250.3",
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)