Skip to content

ci(parity): gate that a required check can be superseded, not only that it can exist - #387

Merged
wenzowski merged 1 commit into
mainfrom
claude/ready-issues-handoff-qnra4q
Aug 13, 2026
Merged

wenzowski merged 1 commit into
mainfrom
claude/ready-issues-handoff-qnra4q

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

CLOUD-503's gate half. The workflow fix landed in 49b89dd; this is the property that stops it regressing.

The property

Every pull_request workflow producing a name in $CI_REQUIRED_CHECKS must declare ready_for_review in its types:.

It sits on the seam properties 5 and 6 already own. Those couple CI_REQUIRED_CHECKS to job names in both directions — a required name that is no job fails, a pull_request job missing from the list fails. Both ask whether a run can exist. This asks whether it can be refreshed, which is the half that was missing:

  • omitting types: defaults the trigger to [opened, synchronize, reopened]
  • every job is draft-gated, because property 1 makes that universal
  • so a PR created as a draft mints a skipped run on opened, and no event remains that could replace it
  • land readies before pushing, and its push moves nothing whenever HEAD is already on the remote, so the ready event is the only one that can arrive
  • checks-green correctly refuses to read a skip as an answer, and the poll never terminates while the branch holds the fleet lease

Measured as a deadlock across #382 and #385 at once, both fully green but for one such name, with main frozen so neither could rebase out of it.

Scoping, deliberately

A workflow whose checks nothing waits on may miss the ready event with no consequence. Demanding the key of every pull_request workflow would assert more than the failure needs, so the property is scoped to workflows producing a required name — which is why it sits beside the job-name collection inside the loop rather than in the want_names comparison at the end, which has the names but no longer knows which file they came from.

The pull_request: sub-block is read on its own, never the whole on: block: a types: belonging to another trigger says nothing about this one, and reading them together would let one satisfy the other.

Shown able to fail, both directions

fixture expected
names a required check, omits ready_for_review refused
produces no required name, omits it not asked for the key

Mutation-checked rather than argued (CLOUD-418): replacing the comparison with true reddens case 3 and only case 3.

A fixture correction that had to ride along

The workflow helper hardcoded types: [opened, synchronize], and fanin's final.yml did the same — both non-compliant under this property. Left alone they would have failed for the new reason rather than their own, which is a suite that no longer tests what its names say. The helper gains a types argument defaulting to the full set, so the refusing case overrides exactly one thing.

Why this is here and not on the earlier PR

It was split out because PR #385 was readied and rewriting these same lines. That PR has landed (CLOUD-494 is Done), so the reason is gone.

Refs: CLOUD-503

@linear-code

linear-code Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
CLOUD-503 A required check whose workflow never subscribes to `ready_for_review` can never be superseded, so a fully green PR wedges `land` and holds the lease

Why

Measured 2026-08-12 ~23:40 UTC on PR #382, head 1789dfb4. land took the fleet landing lease, readied the PR, pushed nothing (HEAD was already on the remote), and then polled ci-wait unbounded until it was stopped by hand. The required set at that moment was entirely green except one name:

check conclusion
ci, cross, commit-lint, msrv, semver, final, SonarCloud Code Analysis SUCCESS
darwin-link (aarch64-apple-darwin) SUCCESS
zizmor SKIPPED, and it is the only zizmor run on the SHA

The cause is types:, which nothing reads. land readies before pushing, so the ready event is what buys the confirming run when the push moves nothing — the CLOUD-247/CLOUD-254 ordering, working as designed. Whether a workflow sees that event is a property of its trigger declaration, and two of the four differ:

workflow required check(s) it produces types:
ci.yml ci, cross, darwin-link (…), msrv, semver, final [opened, synchronize, reopened, ready_for_review]
commit-lint.yml commit-lint same
zizmor.yml zizmor absent → defaults to [opened, synchronize, reopened]
test.yml action absent → same default

Isolated on head 1789dfb4: ci.yml has a skipped run at 23:40:18 (the draft opened event) and a success run at 23:41:08. zizmor.yml has only the 23:40:18 skip. No push separates the two — the ready is the only event between them, and zizmor.yml does not subscribe to it.

Every pull_request workflow here is draft-gated, because ci-local-parity property 1 makes that universal. So a workflow with no ready_for_review mints a skipped run on opened and then has no event left that could supersede it. checks-green correctly refuses to read a skip as an answer, so the poll never terminates. Both affected workflows name checks in $CI_REQUIRED_CHECKS (zizmor, action).

Why it is intermittent rather than constant, which is why it has survived. Both are path-filtered, so it needs a PR touching .github/workflows/**, action.yml or the fixture corpus and a landing lap whose push is a no-op. A lap that actually pushes mints a new SHA, fires synchronize, and both workflows do subscribe to that.

Why the existing sensor missed it. mise-tasks/ci-local-parity is the sole authority on CI shape and asserts its properties over the committed workflow text. None of them reads types: — the trigger event set is the one part of a workflow's shape nothing judges.

Distinct from the two issues it looks like. CLOUD-436's latest-run-per-name cannot help — there is exactly one run to select. Its proposed weaker condition ("a definitive red anywhere is terminal") cannot help either — there was no red at all. Both are about reading a set; this is about a set that can never become readable.

Refinement — Ready (a required check's verdict stops depending on which event fired)

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • Source of truth (§1). mise-tasks/ci-local-parity, already the sole authority on CI shape and already wired into the hk gate. $CI_REQUIRED_CHECKS in mise.toml [env] stays the one list of required names. No new task and no second reader of workflow shape — CLOUD-494's §2 argues that case and it is cited rather than re-typed.

  • Computable predicate (§2). Every pull_request-triggered workflow producing a name in $CI_REQUIRED_CHECKS declares ready_for_review in its types:. Pure committed text — no network, no run history — using the same grep/awk-over-YAML idiom the existing properties use.

    It belongs on a seam that already exists. ci-local-parity:282-287 already couples $CI_REQUIRED_CHECKS to job names in both directions: a required name that is no pull_request job fails, and a pull_request job missing from the list fails. This is the third arm of that same coupling — the first two ask whether a run can exist, and this asks whether it can be refreshed.

    The justification, so the predicate is not mistaken for tidiness: a check only synchronize can supersede cannot be confirmed on a head land readies without pushing, and that is land's ordinary path whenever HEAD is already on the remote.

  • Effect (§3). read. No command surface and no effect-table row — a gate over committed YAML, plus the types: keys that satisfy it.

  • Output & exit (§5). Pointer-only: the workflow path, the required name, and the missing key — never file contents. ci-local-parity's existing problem reporting and exit contract, unchanged.

  • Commit / bump (§6). ci → no bump. Nothing under crates/, so release-plz cuts nothing.

  • Test obligation (§7). tests/ci-local-parity.bats, using its existing fixture-manifest idiom so the property is exercised over fixtures rather than over the live tree.

    Shown able to fail, both directions (the CLOUD-418 obligation): a fixture workflow naming a required check and omitting ready_for_review fails; the same fixture carrying it passes. Without the failing half the property cannot discriminate and ships as coverage. Plus the repo's real workflows passing once zizmor.yml and test.yml are fixed — which is the direction that proves the gate was red before the fix.

  • Blockers (§8). None. relatedTo CLOUD-494 — the same task and the same property class; its PR ci(parity): judge concurrency and schedules on every workflow, and measure the loop's divergence #385 rewrites the neighbouring lines, so start after it merges. This is sequencing, not a blocker: nothing here is unspecified until it lands. Also CLOUD-436 (the consumer that polls forever, and the issue this instance answers an open grooming question on), CLOUD-240 (established the property set), and CLOUD-247/CLOUD-254, the ready-before-push ordering this depends on — both Done and both correct here.

The fix costs no CI minutes, which is worth stating because it looks like it should

Adding ready_for_review to a draft-gated, path-filtered workflow spends no extra runner. The job's if: ${{ github.event.pull_request.draft == false }} still skips every draft event, and the only new firing is the single transition that needs one. This is not a trade against the draft economy; it is the event that economy was always missing.

Acceptance

  • A required check whose workflow cannot see ready_for_review fails the gate.
  • zizmor.yml and test.yml declare types: including ready_for_review, and the gate is red before they do — the rule and its mechanism land together (non-negotiable 2).
  • The property is shown able to fail in both directions.
  • A PR readied without a push reaches a verdict on every required name.
  • test.yml's header stops claiming a safety property that is false. It says "ci-wait reads an absent path-filtered check as absent rather than pending, so a required check that produces no run here does not hang a landing" — true for absent, false for present-and-skipped, which is exactly what a draft opened event creates.

Reported first as a comment on CLOUD-494 and split out here rather than folded into it, on that issue's own precedent — it was itself split out of CLOUD-240's property set. Folding a third property into an issue whose PR is already readied would leave its acceptance unmet the moment that PR merges.

Review in Linear

…at it can exist

CLOUD-503's gate half. The workflow fix landed in 49b89dd; this is the property
that stops it regressing, and the reason it was split has gone — CLOUD-494's PR
landed, so `ci-local-parity` and its suite are free.

Property 11: every `pull_request` workflow producing a name in
`$CI_REQUIRED_CHECKS` must declare `ready_for_review` in its `types:`.

It sits on the seam properties 5 and 6 already own. Those couple
`CI_REQUIRED_CHECKS` to job names in both directions — a required name that is
no job fails, a `pull_request` job missing from the list fails. Both ask whether
a run can EXIST. This asks whether it can be REFRESHED, which is the half that
was missing: omitting `types:` defaults to `[opened, synchronize, reopened]`,
every job is draft-gated because property 1 makes that universal, so a PR
created as a draft mints a `skipped` run on `opened` and no event remains that
could replace it. `land` readies before pushing and its push moves nothing
whenever HEAD is already on the remote, so the ready event is the only one that
can arrive. `checks-green` correctly refuses to read a skip as an answer, and
the poll never terminates while the branch holds the fleet lease.

SCOPED TO WORKFLOWS PRODUCING A REQUIRED NAME, deliberately. A workflow whose
checks nothing waits on may miss the ready event with no consequence, and
demanding the key of every `pull_request` workflow would assert more than the
failure needs. That scoping is why the check sits beside the job-name collection
inside the loop rather than in the `want_names` comparison at the end, which has
the names but no longer knows which file they came from.

The `pull_request:` sub-block is read on its own, never the whole `on:` block: a
`types:` belonging to another trigger says nothing about this one, and reading
them together would let one satisfy the other.

SHOWN ABLE TO FAIL, BOTH DIRECTIONS (CLOUD-418). A fixture naming a required
check and omitting `ready_for_review` is refused; one producing no required name
is not asked for the key. Mutation-checked rather than argued: replacing the
comparison with `true` reddens case 3 and only case 3.

The `workflow` fixture helper hardcoded `types: [opened, synchronize]`, which is
non-compliant under this property — so it gains a `types` argument defaulting to
the full set, and the `fanin` fixture's own `final.yml` is corrected the same
way. Without that both would have failed for the new reason rather than their
own, which is a suite that no longer tests what its names say.

Refs: CLOUD-503
@wenzowski
wenzowski marked this pull request as ready for review August 13, 2026 02:38
@wenzowski
wenzowski force-pushed the claude/ready-issues-handoff-qnra4q branch from 2e87d16 to e783ae9 Compare August 13, 2026 02:38
@sonarqubecloud

Copy link
Copy Markdown

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit e783ae9 into main Aug 13, 2026
8 checks passed
@wenzowski
wenzowski deleted the claude/ready-issues-handoff-qnra4q branch August 13, 2026 02:48
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.

2 participants