Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 80 additions & 3 deletions scripts/pm/ci-failure.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2264,6 +2264,51 @@ async function selfTest() {
['repo-scope-refused', ['rate:anon', 'repo']],
);

// -- renderFixLines: one remedy is one `fix:` marker (#10362) -------------
// Driven through the REAL producer, not a hand-written array: what needs
// pinning is that this file renders what `classifyTransportProbe` actually
// emits. A fixture would only restate the printer's own assumption about the
// shape, which is the assumption that was wrong.
const refusedFix = classifyTransportProbe({
token: 'ghp_x',
authed: { status: 200, rateLimitRemaining: 4999 },
repo: { status: 403, rateLimitRemaining: 4999 },
}).fix;
t('the producer still wraps this remedy across several lines', refusedFix.length > 1, true);
t(
'a multi-line remedy renders with exactly ONE fix: marker',
renderFixLines(refusedFix).filter((l) => l.startsWith(' fix: ')).length,
1,
);
t(
'its continuations are padded under the marker rather than re-marked',
renderFixLines(refusedFix).slice(1).every((l) => l.startsWith(' ') && !l.includes('fix: ')),
true,
);
t('rendering neither adds a line nor drops one', renderFixLines(refusedFix).length, refusedFix.length);

// The branch that makes re-wrapping unsafe: a copy-pasteable command whose
// annotation carries its own indentation and an arrow pointing back at it.
const anonFix = classifyTransportProbe({
token: 'ghp_x',
authed: { status: 401 },
anon: { status: 200, rateLimitRemaining: 59 },
}).fix;
t(
'the copy-pasteable command keeps the marked line to itself',
renderFixLines(anonFix)[0],
' fix: GITHUB_TOKEN= GH_TOKEN= node scripts/pm/check-half-states.mjs',
);
t(
'the annotation keeps its own indent, so its arrow still points at that command',
renderFixLines(anonFix)[1].startsWith(' ↑'),
true,
);

t('an empty fix renders nothing at all', renderFixLines([]), []);
t('an absent fix renders nothing at all', renderFixLines(undefined), []);
t('a one-line remedy is just the marked line', renderFixLines(['do the thing']), [' fix: do the thing']);

if (failures.length > 0) {
console.error(`✗ ci-failure --self-test (${failures.length} failure(s)):\n`);
for (const f of failures) console.error(` • ${f}`);
Expand DownExpand Up@@ -2295,10 +2340,42 @@ async function selfTest() {
' annotations carried none, a log that anchored nothing is still a shortfall rather than a\n' +
' manufactured answer, a 410 is named as expired retention rather than as an absence of\n' +
' evidence, a CONNECT refusal lands as transport with the blocked host to report, and a\n' +
' tail with no `##[error]` in it is labelled a window rather than an anchor.',
' tail with no `##[error]` in it is labelled a window rather than an anchor. And a `fix`\n' +
' renders as the ONE remedy it is: a single `fix:` marker with its continuations padded\n' +
' under it, keeping a copy-pasteable command on a line of its own.',
);
}

/**
* Render a verdict's `fix` as the ONE remedy it is: the marker goes on the first
* line, and every continuation is padded to sit under it (#10362).
*
* `fix` arrives as the wrapped lines of a single sentence — never as a list of
* independent remedies (measured across every branch of
* `classifyTransportProbe`). Marking each element `fix:` rendered one remedy as
* three, and a reader counting "how many things do I have to do" counted wrong.
*
* The lines are padded rather than re-flowed because the producer's breaks are
* load-bearing in at least one branch: `bad-credential-anon-reachable` is a
* copy-pasteable command followed by an annotation carrying its own
* indentation, whose `↑` points at the command above it. Joining and
* re-wrapping would swallow the command into the prose and leave the arrow
* pointing at nothing, so continuations keep their own leading whitespace.
*
* This is the idiom `check-half-states.mjs` — the file that PRODUCES these
* verdicts — already prints with. Both of this file's printers go through here
* so the two cannot drift apart again, which is how the second one came to
* exist (#10155 matched the entry point's spelling rather than fixing it in an
* unrelated PR).
*
* Presentation only: no exit code, no verdict, no claim about the tree.
*/
function renderFixLines(fix) {
const lines = fix ?? [];
if (lines.length === 0) return [];
return [` fix: ${lines[0]}`, ...lines.slice(1).map((l) => (l ? ` ${l}` : ''))];
}

/**
* The mid-walk net's printer (#10155). The DECISION is `midWalkVerdict`, pure
* and self-tested; what lives here is the one thing it cannot be handed — the
Expand All@@ -2320,7 +2397,7 @@ async function reportMidWalkFailure(error, stage) {
const decision = midWalkVerdict({ probe, error, read: error?.read ?? null, stage });
console.error(`\nci-failure: ${decision.verdict} — ${decision.headline}\n`);
for (const line of decision.detail) console.error(line ? ` ${line}` : '');
for (const line of decision.fix ?? []) console.error(` fix: ${line}`);
for (const line of renderFixLines(decision.fix)) console.error(line);
console.error(" Piping reports the PIPE's status, so `... | tail` reads green either way. Use `echo \"EXIT=$?\"`.");
process.exit(decision.exit);
}
Expand DownExpand Up@@ -2377,7 +2454,7 @@ if (!invokedDirectly) {
if (probe.kind !== 'reachable') {
console.error(`ci-failure: PREREQUISITE NOT MET — ${probe.headline}`);
for (const line of probe.detail ?? []) console.error(` ${line}`);
for (const line of probe.fix ?? []) console.error(` fix: ${line}`);
for (const line of renderFixLines(probe.fix)) console.error(line);
console.error(` (Exit ${EXIT_PREREQUISITE_NOT_MET}. This classifies the ENVIRONMENT, not the tree.)`);
process.exit(EXIT_PREREQUISITE_NOT_MET);
}
Expand Down
Loading