Skip to content

ci(policy): fail CI when workflow content escapes its block scalar (BLO-23128) - #1183

Draft
allyblockcast[bot] wants to merge 5 commits into
masterfrom
cto/blo-23128-workflow-yaml-gate
Draft

ci(policy): fail CI when workflow content escapes its block scalar (BLO-23128)#1183
allyblockcast[bot] wants to merge 5 commits into
masterfrom
cto/blo-23128-workflow-yaml-gate

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 9, 2026

Copy link
Copy Markdown

Problem

An unparseable workflow file passes every gate this repo has. GitHub does not surface "your workflow is invalid" — it manufactures a run that fails instantly with zero jobs, so there is no failing job for a required check to catch.

BLO-23128's own fix hit exactly this. f94d521 wrote the alert-on-failure heredoc body and its closing EOF at column 0 inside a run: | block scalar. Column 0 terminates the block scalar, so YAML reads the prose as a new top-level key:

could not find expected ':' while scanning a simple key
  at line 423 column 1

Agent image delivery — toolchain build, Promote verified agent image, Bump agent image refs in cluster — stayed dead. The irony is load-bearing: the alert-on-failure job added to stop a red pipeline going unnoticed can never run, because the file it lives in does not parse. That is the same failure mode BLO-23128 was opened for.

The cheap tell for this class: an instant 0-job run where created_at == updated_at, and the workflow's name reported as its path rather than its name:.

Change

Adds .github/scripts/check-workflow-structure.mjs to the required policy job. Rule: a non-blank line at column 0 must be a valid top-level YAML construct (mapping key, comment, or document marker). Anything else means content has escaped the block scalar meant to contain it.

Why this rule and not a real YAML parse

The policy job never installs node_modules — every validator there is Node-builtins-only — and js-yaml is present in the lockfile only transitively, not as a resolvable direct dependency. This rule needs no parser, is decidable from the text alone, and has zero false positives across the repo.

Stated limit, deliberately: this is a targeted structural gate, not a YAML validator. A dedented line that still looks like a mapping key (Run: ${RUN_URL}, in this very heredoc) is accepted in isolation, because at column 0 it genuinely is valid top-level YAML. It is the file that must be rejected, and the broken file is rejected on its other seven lines. A full actionlint gate is the strictly-stronger follow-up once a pinning/vendoring decision is made.

Verification

  • Rejects docker-agent.yml as it stands on master: 7 lines, first at 423 — the exact location the parser cites.
  • Passes on all 20 workflow files once ci(agent): fix unparseable docker-agent.yml (alert heredoc broke the file) #1180's indentation fix is applied.
  • 8 unit tests (node --test), including the exact broken/fixed heredoc pair.
  • Existing check-github-runner-labels.mjs still green; the rule is dogfooded against pr.yml itself.

Sequencing — why this is a draft

policy on this branch correctly fails right now, because master genuinely is broken. This must rebase onto a master that already contains #1180 (@kkroo's indentation repair). I have deliberately not included that fix here so it stays #1180's to land. Once #1180 merges I'll rebase and mark this ready.

Refs BLO-23128.

🤖 Generated with Claude Code

…LO-23128)

An unparseable workflow file is invisible to every existing gate. GitHub does
not report "your workflow is invalid" — it manufactures a run that fails
instantly with zero jobs, so there is no failing job for a required check to
notice.

BLO-23128's own fix hit exactly this: f94d521 wrote an alert-on-failure heredoc
body and its closing EOF at column 0 inside a `run: |` block scalar, which
terminates the scalar and corrupts the document ("could not find expected ':'
... at line 423 column 1"). Agent image delivery — build, promote, and the
cluster ref bump — stayed dead until a human found it by hand. The
alert-on-failure job added to catch a red pipeline could never itself run,
because the file it lives in does not parse.

Adds check-workflow-structure.mjs to the required `policy` job: a non-blank
line at column 0 must be a valid top-level YAML construct. The `policy` job
never installs node_modules, so this uses Node builtins only rather than a YAML
library (js-yaml is in the lockfile only transitively).

Verified against every workflow in the repo: rejects the broken docker-agent.yml
on master (7 lines, first at 423 — the exact parse location), and passes on all
20 files once #1180's indentation fix is applied.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 9, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-23128

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 9, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-23128

@allyblockcast

allyblockcast Bot commented Aug 9, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • PR is titled ci: but includes source code changes (.github/scripts/check-workflow-structure.mjs). Please retitle as fix:, feat:, or refactor: so the right gates run, or remove the source code changes if this is genuinely a ci: PR.

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast

allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • PR is titled ci: but includes source code changes (.github/scripts/check-workflow-structure.mjs). Please retitle as fix:, feat:, or refactor: so the right gates run, or remove the source code changes if this is genuinely a ci: PR.

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast

allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • PR is titled ci: but includes source code changes (.github/scripts/check-workflow-structure.mjs). Please retitle as fix:, feat:, or refactor: so the right gates run, or remove the source code changes if this is genuinely a ci: PR.

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@kkroo

kkroo commented Aug 10, 2026

Copy link
Copy Markdown

Picked this up because it was red and untouched for ~17h. The gate is sound — nothing here needed a code change. Both reds were external.

Red #1: policy — the gate was correct, master was broken

policy failed at 00:38 flagging 7 dedented lines in docker-agent.yml. That file was genuinely still broken on master at that moment (BLO-23128); #1180 fixed it at 04:03, ~3.5h after this PR opened. So the gate fired for exactly the right reason, against a tree that no longer exists.

Verified independently before touching anything — I ran this branch's check-workflow-structure.mjs against both trees locally:

target ruby YAML.load_file (independent oracle) this gate
docker-agent.yml @ f94d521 could not find expected ':' … line 409 column 1 exit 1, first offender line 409 (+6 more, incl. the stray EOF)
all 20 workflows @ current master parses exit 0

The gate agrees with a real YAML parser on the exact line, and is clean on today's master. That is the negative control this change needed.

Red #2: ERR_PNPM_LOCKFILE_CONFIG_MISMATCH — a repo-wide window, not this PR

After the first branch update, every job failed on install. Not caused by this PR: its package.json and pnpm-lock.yaml blobs were byte-identical to master's. Master itself was inconsistent:

  • 10b50b1f (08-09 23:02) added the nanoid override to package.json without the lockfile
  • cfe09406 (08-10 03:56) "chore(lockfile): refresh pnpm-lock.yaml" healed it

In that ~5h window four branches and merge-queue entry pr-1236 failed every job the same way. Second branch update picked up the refresh; Build/Typecheck/e2e/shards all green now.

Red #3: one flaky shard — security-audit-overrides

General tests (server 4/4): 1 failed / 1433 passed. The failure is Error: Command failed: pnpm install --lockfile-only --ignore-scripts --no-frozen-lockfile — and that is the whole message, because promisify(execFile) discards pnpm's stderr. Filed BLO-24012 with a fix.

Non-deterministic rather than a regression: pen-2074/release-external-wait-slo was fully green 40 min later, and fix/blo-19123 failed different shards (2/4, 3/4). Master's inputs are correct — override nanoid: ">=5.1.16 <6", lockfile resolves exactly nanoid@5.1.16.

State / next action

  • Branch updated twice → behind_by: 0 (required, since verify is strict: true).
  • Re-ran the failed shard; result pending as of this comment.
  • Still a draft, and I have deliberately not marked it ready or queued it — that is the author's call, not mine.

Owner decision: if the rerun is green, this is ready to un-draft. One substantive review point, not a blocker — the script's own header already says it: this is a targeted structural rule, not a YAML parse, and it accepts a dedented line that happens to look like a mapping key. It rejects the real broken file on its other lines, so it would have caught BLO-23128. actionlint pinned by checksum remains the strictly-stronger follow-up (BLO-23511).

@allyblockcast

allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • PR is titled ci: but includes source code changes (.github/scripts/check-workflow-structure.mjs). Please retitle as fix:, feat:, or refactor: so the right gates run, or remove the source code changes if this is genuinely a ci: PR.

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@kkroo

kkroo commented Aug 10, 2026

Copy link
Copy Markdown

Rerun result: still red, still not this PR — and the second failure has a different cause from the first, which is itself the useful finding.

run shard duration failing test
04:42 805s (normal) security-audit-overrides — live pnpm install in the fixture
06:43 3760s (4.7×) opencode-k8s-seed-transport"simulated 610s idle … and bounds timeout"

The security-audit test passed on the rerun. What failed instead was a timing-bounded test, on a run where import alone went 261s → 1171s. That is a starved runner, not a code defect: a test asserting timing bounds is the first thing to break when the box is ~5× slow. Neither failure touches anything this PR changes (3 files, all under .github/).

The shard took 70m15s wall-clock against a 90-minute job cap, so it came close to being killed outright — which would have produced no vitest summary at all, and therefore no failing-test name to read.

Filed BLO-24012 on the first one (the test discards pnpm's stderr, so its failure message is only Command failed: …).

State

Branch updated again — the 07:33 batch of 5 merges had put it BEHIND, and verify is strict: true, so it needs to be current to merge. Fresh CI running on cfddbfd8.

Next action / owner

This is now blocked on CI capacity, not on anything in the diff. I've spent two full runs establishing that; I'm not going to keep re-running it, because each attempt costs ~70 min of a constrained pool and the failures are drawn from a different random test each time.

Recommendation for whoever owns this: the gate itself is verified correct (it agrees with a real YAML parser on the exact line of the f94d5212 break, and is clean on all 20 current workflows — detail in my earlier comment). It is worth landing on the next green, and worth un-drafting so it stops looking abandoned. Note #1236 "extend PR e2e job cap to 75 minutes" merged today, so the duration pressure is being felt elsewhere too.

@allyblockcast

allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • PR is titled ci: but includes source code changes (.github/scripts/check-workflow-structure.mjs). Please retitle as fix:, feat:, or refactor: so the right gates run, or remove the source code changes if this is genuinely a ci: PR.

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@kkroo

kkroo commented Aug 10, 2026

Copy link
Copy Markdown

Third run, third external failure. I'm stopping here rather than spending a fourth — at this point I'd be rolling dice with scarce runner capacity, not testing anything about this diff.

run shard duration failing test cause
04:42 805s (normal) security-audit-overrides live pnpm install in the test — BLO-24012
06:43 3760s (4.7×) opencode-k8s-seed-transport timing-bounded test on a starved runner
08:09 every job the master lockfile outage (BLO-24150)
11:02 3301s (4.1×) security-audit-overrides again BLO-24012

Four runs, four different external causes, and not one failure in a file this PR touches (3 files, all under .github/).

Two notes on reading that last run: its overall conclusion shows cancelled, which understates it — the jobs were 12 success / 2 failure / 1 cancelled. The cancelled one is e2e, killed at exactly 75m00s on its job cap, and e2e is not in verify's needs, so it isn't the blocker. The blocker is General tests (server 2/4), which failed on security-audit-overrides after 55 minutes — 4.1× the 805s norm.

security-audit-overrides has now failed this PR twice and was also one of the two gates holding shut #1244, the automated fix for the ~3h27m master outage. I've raised BLO-24012 to high with that evidence. Its failure message is Command failed: pnpm install … and nothing else, so I genuinely cannot tell you whether the two failures here share a cause — the test discards .stderr.

Where this leaves the PR

The gate itself is verified correct and I'd merge it on any green run:

  • vs f94d5212's docker-agent.yml: a real Ruby YAML parser reports line 409 column 1; this gate flags line 409 plus the other 6 escaped lines and the stray EOF.
  • vs all 20 workflows on current master: exit 0.

Branch is at behind_by: 0 and Ally-reviewable. It is still a draft — I've deliberately not un-drafted it, since that's your call, but it's worth doing so it stops reading as abandoned work when it is in fact finished and blocked purely on CI capacity (BLO-23845, critical).

Suggested next action for the owner: un-draft, then merge on the first run where verify is green — no code change required. If you want to de-risk it, landing BLO-24012's two-line stderr/timeout fix first would make this PR's next failure (if any) actually readable.

@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

Superseded by #1429 (BLO-23511)

Heads up — not a review request, and no action needed from you.

This PR is CONFLICTING/DIRTY and still draft, untouched since 2026-08-10. A draft PR gets no automatic review (the webhook suppresses opened/synchronize/reopened while draft: true), so the review this was waiting on was never going to arrive.

I have landed the successor this PR itself names: #1429actionlint pinned by version + SHA256 in the required policy job.

Your diagnosis held up completely, and #1429 is built on it:

  • The policy job never installs node_modules — confirmed as setup-pnpm running with the default run_install: false. js-yaml is only a version constraint in package.json, not a dependency, and declaring it would trip Block manual lockfile edits in that same job. That is exactly why feat(ci): fail CI when a workflow file does not parse (BLO-23511) #1429 uses a static binary rather than a parser library.
  • The limit you documented is the reason to go further rather than land this as-is: a dedented line that still looks like a mapping key (Run: ${RUN_URL}, in that very heredoc) is accepted, because at column 0 it genuinely is valid top-level YAML. f94d5212 is caught only because its other seven lines are not. feat(ci): fail CI when a workflow file does not parse (BLO-23511) #1429 parses with a real YAML parser, so that shape is caught too.
  • Your reference target reproduces identically: feat(ci): fail CI when a workflow file does not parse (BLO-23511) #1429 rejects f94d5212's docker-agent.yml at line 409, and a deliberate-break negative control (#1430, throwaway) demonstrates the red end-to-end.

Leaving this open for now — I will not close someone else's PR on the strength of an unmerged one. Once #1429 lands on master I will close this as superseded, unless you would rather close it yourself.

Refs BLO-23511

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant