@biffo/cli 0.250.4 → 0.252.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
|
@@ -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)
|