fix: labels/scope writes additively — a label applied mid-job survives #133
No reviewers
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:docs
scope:guards
scope:labels
scope:release-flow
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/ceremony#133
Loading…
Reference in a new issue
No description provided.
Delete branch "build/130-labels-scope-clobber"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #130.
labels / scopeclobbered thereleaselabel on #128 two seconds after the builder set it. Diagnosis (source, ceremony's pinned SHA8558fd7):actions/labeler@v5fetches the PR fresh at step start (pulls.get), computes (fetched labels ∪ derived), and writes the result withPUT /issues/{n}/labels(src/labeler.ts→api.setLabels→issues.setLabels) — a full replace, even undersync-labels: false. Withsync-labels: falsethe v5 code has no DELETE path at all, so #128'sunlabeledtimeline event can only have come from that PUT. Any label written between the fetch and the write is silently removed. v6 and v7 write the same way (setLabelsat both tags), so pinning past it is not available; per the spec, the step is replaced with a direct additive write.The fix:
actions/labels-scope, a ceremony-owned composite. The consumer's.github/labeler.ymlstays exactly as it is — thechanged-files/any-glob-to-any-fileshape every governed repo uses (box, rig, cast, incubator, ceremony all verified), any YAML spelling; other labeler keys refuse loudly rather than half-honour. yq only normalizes YAML→JSON; the shape contract and derivation are jq/bash. The mapping and changed paths are read via the API (still no PR code executed; config read at the base ref so a PR cannot label itself by editing its mapping), and the only write isPOST /issues/{n}/labels— adds, never removes. The guard from the spec also lands: the reconcile sweep warns — never sets — when a non-draft PR carries a bareX.Y.Zversion differing from its base but noreleaselabel.Evidence — scratch repo
ceremony-scratch-130(archived; deletion is the operator's, this token has nodelete_reposcope)Repro, before (labeler v5 at ceremony's pin,
sync-labels: false). PR with 3000 mapped files widens the fetch→write window to ~45s. Run 30091568178; job in_progress 12:01:0x,releaseadded mid-window, timeline:Final labels:
[scope:stuff]—releasegone. Same shape as #128's timeline, same actor sharing the second with the labeler's own writes.Demo, after (candidate workflow via fork ref
drill/130-demo,CEREMONY_SELF_REFpinned to candidated0b857eper the #11-ruled drill deviation). Run 30091675525; scope job ran 12:03:01→12:03:10,releaseadded at 12:03:02 — inside the job, before the machine's write:Final labels:
[release, scope:stuff]— both. Scope log:labels-scope: #2: scopes -> scope:stuff (additive POST; already-present names are no-ops). The reconcile job ran green on the consumer checkout path in the same run.Regression probes (same scratch repo):
scope:manual(not derivable from any glob) survives a scope run — labels after:[release, scope:stuff, scope:manual].VERSION, norelease):##[warning]labels: #4 is release-shaped (version -> 0.1.0 at its head) but carries no release label — the merge door reads that label as declared intent and will refuse without it…— one warning for the sweep, no label written by the machine. (Headce24a1athen polished the empty base-version rendering tounreadable; fixture-tested.)actions: readgets the documented "could not read mergeability/checks — left alone this pass" degradation, and the sweep (guard included) leaves the PR alone — the #101 report-don't-diagnose warning covers it.Acceptance criteria (from #130)
labels / scopeis running keeps it — demonstrated on a real run, with the timeline in the PR bodyscope:*labels are still applied, and a hand-applied scope still survivesreleaselabel draws exactly one warning per sweep (once per PR per pass, fixture-tested), and the reconciler never setsreleaseitselfWorklog
setLabelsPUT on every path; replacement, not repinactions/labels-scope/— additive derive: yq→jq strict-subset config parse, glob→ERE, POST-only writelabels.ymlscope job: swap labeler for labels-scope; comment now describes the additive write and links the incidenttest/labels-scope.test.sh— glob translation, config parse (all family spellings + refusals), derivation fixturesrelease+ fixture testschangelog.d/130.mdRound 1 (codex changes-requested; grok, kimi approved)
labeler.ymlheader rewritten — describes labels-scope's base-ref read + additive POST, names #128; no second contradictory mechanism story (codex-1, grok-1)scope:labelsrow gainsactions/labels-scope/**+test/labels-scope.test.sh; two new fixtures derive against the real mapping (codex-2, grok-2)4002924Round 1 bookkeeping — staled approvals
4002924,94f890a) staled theirce24a1aapprovals; the reconciler bounced the handoff (state:needs-human→state:addressing) because a verdict not bound to the current head is no verdict🔎 reviewing head
ce24a1a3ba🔎 reviewing head
ce24a1a3baBlocking:
.github/labeler.yml:2-6still says the scope job isactions/labeler@v5withsync-labels: false, which is the exact now-removed behavior this PR establishes was not additive. Please update this header to describeactions/labels-scopeand its additive POST so the repository no longer carries a second, contradictory explanation of the mechanism.scope:labelsmapping does not includeactions/labels-scope/**. As written, a future PR changing only the new scope-label implementation will derive noscope:labelslabel, unlike the neighboringactions/labels-reconcile/**implementation. Please add the new action path to that surface (and cover the real mapping row in the fixture).Verification: I reviewed the linked #130 criteria and the complete
a602fd0..ce24a1adiff, rangit diff --check, the full 18-file test suite (all passed; the local image lacksyq, while CI requires it), ShellCheck on the changed shell files (only pre-existing/source-following SC1091 informational messages), andactionlint(clean).Verdict: approve
Reviewed head
ce24a1a3ba075b6714796e254824b59f6cdfca60against #130's acceptance criteria and the load-bearing constraints (additive label writes; reconciler never guessesreleaseintent).Acceptance criteria
labels / scopesurvivesPOST /issues/{n}/labels(labels-scope.sh); by construction removes nothing. Scratch timelines in the PR body (before: clobber via labeler PUT; after:release+scope:stuffboth present).scope:*still applied; hand-applied scope surviveslabels.ymlscope-job comment documents the #128 PUT race, v6/v7 same path, and the additive POST replacement.releaserelease_shape_warningonly emits::warning::; gated on non-draft + missingrelease; pure-function fixtures cover bare/dev/rc/unreadable cases; call site does not write the label.labels.ymlcomment,labels-scope.shheader,CONSUMERS.md, changelog fragment.Verification (this tick)
test/labels-scope.test.shwith yq: 21/21 (glob, derive, parse spellings + loud refusals).shellcheckclean onlabels-scope.shandlabels-reconcile.sh.pull_request_targetsafety preserved: no PR code executed; mapping read at baseCONFIG_REFvia API; ceremony implementation only checked out.Spec fit
Diagnosis → replace write path (not sleep/re-apply/
releaseauto-set) matches the issue. Guard is warning-only per LABELS.md intent rules. Changelog fragment present.Non-blocking nits (builder discretion)
.github/labeler.ymlheader is now stale — still saysactions/labeler@v5+sync-labels off. After this lands, that comment is false; a one-line update (or drop the mechanism claim and keep the mapping-only prose) would match the issue's "a comment that lies is worse than none" bar for the workflow comment.actions/labels-scope/**andtest/labels-scope.test.share not underscope:labelsinlabeler.yml. Advisory only (scopes locate), but a follow-up row would keep the new surface discoverable when touched alone.None of these gate the fix. Approve.
🔎 reviewing head
ce24a1a3baApprove — head
ce24a1a3ba075b6714796e254824b59f6cdfca60.Reviewed the whole PR against #130's spec, verified what could be run, and checked the diagnosis against labeler's source rather than taking it on faith.
Diagnosis — independently confirmed. At the pinned SHA
8558fd7(v5 tag resolves to exactly that),src/labeler.tsmapspullRequest.data.labelsintopreexistingLabelsat job start, unions the derived labels, and callsapi.setLabels→src/api/set-labels.ts→client.rest.issues.setLabels, which isPUT /issues/{n}/labels— a full replace. Withsync-labels: falsethere is no DELETE path, so #128'sunlabeledcould only have come from that PUT, and any label written between the fetch and the PUT is silently dropped. The PR's mechanism claim is accurate, and "replace, don't repin" follows.The fix is additive by construction.
actions/labels-scope/labels-scope.sh's only write isPOST /issues/{n}/labels; the mapping is read via the API atCONFIG_REF(wired togithub.sha, the base commit — a PR cannot label itself by editing its mapping); changed paths come from the API; no PR code is executed, keeping thepull_request_targetsafety property the workflow header promises. The dogfood-vs-consumer checkout split (github.shavsCEREMONY_SELF_REF) keeps script and workflow from skewing, and the self-ref check keeps the consumer pin honest. The acceptedlabeler.ymlsubset is exactly the shape the family uses, with loud refusals for anything else — verified against ceremony's own file in the test suite.Guard matches the spec's scoping.
release_shape_warningwarns, never writes; drafts exempt; unreadable versions are silent rather than nagging on a guess; one call site inreconcile_prmakes "exactly one warning per sweep" true by construction.Ran, in a detached worktree at the head:
test/labels-scope.test.sh— 21/21 pass (with yq; theCEREMONY_REQUIRE_YQCI gate correctly makes a skip a failure there).test/labels-reconcile.test.sh— 146/146 pass, including the full warning-matrix cases..github/scripts/shellcheck-all.shand.github/scripts/actionlint-all.sh(the CI invocations) — clean.main— environmental (no node here), not this PR.Acceptance criteria — all five addressed: mid-job label survival demonstrated with before/after timelines in the body; scopes still derived and hand-applied scopes survive (probes documented); the
labels.yml:44comment now describes what the step does and names the incident; the guard warns once per sweep and never sets; #128 is linked from the workflow comment, the script header, and CONSUMERS.md.One caveat, non-blocking: the scratch-repo evidence (repro run 30091568178, demo run 30091675525, guard probe) is not independently verifiable from my seat —
claude-bot-andresmgsl/ceremony-scratch-130404s for this token. The mechanism it demonstrates is confirmed from labeler's source, and the code-level property (POST-only write) is verifiable in the diff, so this doesn't gate my approval; noting it so the human knows which links the panel could not open.Non-blocking nit:
changelog.d/130.mdand the CONSUMERS.md paragraph are thorough to the point of density, but they match this repo's convention of carrying the war story with the fix, so nothing to change.🔧 addressing round on head
ce24a1aRound analysis — every point, all three reviewers:
codex (blocking)
.github/labeler.yml:2-6header still describesactions/labeler@v5+sync-labels: false. — Agree. The header is now a second, contradictory explanation of the mechanism this PR replaced. I'll rewrite it to describeactions/labels-scopereading this mapping and writing additively (POST, never removes), keeping the incident link.scope:labelsmapping missesactions/labels-scope/**. — Agree. A PR touching only the new implementation would derive noscope:labels, unlike the neighboringactions/labels-reconcile/**. I'll addactions/labels-scope/**andtest/labels-scope.test.shto thescope:labelsrow, and extend the fixture intest/labels-scope.test.shto derive against ceremony's reallabeler.ymlrow so the mapping is covered by test, not just present.grok (non-blocking)
labels-scopein the same pass.kimi (non-blocking)
Plan: one commit covering 1–3 (header rewrite, mapping row + fixture coverage, reconcile comment wording), suite + shellcheck + actionlint, then re-request codex (the round's one non-approver).
Round answered whole on head
4002924— every point from the analysis above landed as planned:.github/labeler.ymlheader no longer describes labeler@v5 +sync-labels— it now states thatactions/labels-scopereads this mapping at the base ref and writes additively (POST, never removes), with the #128 clobber as the war story.scope:labelsnow mapsactions/labels-scope/**andtest/labels-scope.test.sh, and two new fixtures intest/labels-scope.test.shderive against ceremony's reallabeler.yml(a labels-scope-only change and the test file itself both derivescope:labels), so the row is covered by test rather than merely present.Verified at
4002924: full suite 18/18 files,.github/scripts/shellcheck-all.shandactionlint-all.shclean. Re-requesting codex (the round's non-approver).🔎 reviewing head
94f890aa1cApproved at
94f890aa1ceadfc4a75afb66a9f887c3b55f9c09.The two blockers from my prior review are resolved:
.github/labeler.ymlnow accurately describesactions/labels-scopereading the base-ref mapping and using an additive POST, including the #128 incident that bought the rule.scope:labelsnow maps bothactions/labels-scope/**andtest/labels-scope.test.sh, with fixtures deriving against the repository's real mapping row.I re-reviewed the complete PR against #130. The additive-only write preserves concurrently or manually applied labels; derived scopes remain applied; the workflow explanation matches the mechanism; the release-shape guard warns once per eligible PR per sweep without setting
release; and the incident links and changelog fragment are present. The final head commit is an empty check retrigger and introduces no code delta from4002924.Verification at this head:
git diff --check; all 18 test files pass (the local image lacksyq, so the yq-dependent parser cases skip locally, while the required GitHubtestcheck passed); the repository ShellCheck and actionlint sweeps pass; and all current head checks are green, includinglabels / scopeandlabels / reconcile.Round summary — handoff
Round 1 (codex changes-requested; grok, kimi approved): three points, all addressed on
4002924:.github/labeler.ymlheader rewritten — it now describesactions/labels-scope(base-ref config read, additive POST, never removes), with #128 as the incident, instead of the replaced labeler@v5 story (codex-1 / grok-1).scope:labelsmapping now coversactions/labels-scope/**andtest/labels-scope.test.sh; two new fixtures derive against ceremony's real mapping (codex-2 / grok-2).Head
94f890ais an empty retrigger commit — the queued duplicatelabels / reconcilewas cancelled and left a false red (same shape as #138); no tree change over4002924. codex re-approved this head.Verified: full suite 18/18 files, shellcheck and actionlint clean; live before/after evidence on scratch repo
ceremony-scratch-130(clobber repro with timeline, additive-survival demo, hand-applied-scope survival, no-glob no-op, release-shape guard warning) — all linked in the PR body. CI green on94f890a, noblocker:*, mergeable.Panel: codex ✅ (current head), grok ✅, kimi ✅. Over to you, @danmt — requesting your review and setting
state:needs-human.🔁 Re-requesting @grok-bot-andresmgsl and @kimi-bot-andresmgsl on head
94f890a.Your round-1 approvals bound
ce24a1a; the round-1 fixes (4002924— labeler.yml header rewrite,scope:labelsmapping rows + fixtures, reconcile-job comment) and the empty CI-retrigger commit (94f890a, the #138 cancelled-queued-reconcile flap) moved the head past them, so the reconciler treats them as stale and bounced my handoff back tostate:addressing— correctly: no current-head verdict from either of you exists.The diff since your approvals is exactly the round-1 fix you both asked for (grok-1/-2/-3 overlap codex-1/-2) plus one empty commit. Codex has approved
94f890a. Once your verdicts land on the current head I'll re-hand-off to @danmt.🔎 reviewing head
94f890aa1cVerdict: approve
Reviewed head
94f890aa1ceadfc4a75afb66a9f887c3b55f9c09against #130 (full PR, not only the delta since my prior approval atce24a1a).Round-1 delta since my staled approval
The push that staled me (
4002924+ chore94f890a) lands the non-blocking nits from round 1:.github/labeler.ymlheader now namesactions/labels-scope, base-ref read, additive POST, and #128 — no longer claimsactions/labeler@v5/sync-labels off.scope:labelsrows coveractions/labels-scope/**andtest/labels-scope.test.sh; fixtures derive against the real mapping for both paths.Acceptance criteria (#130)
labels / scopesurvivesPOST /issues/{n}/labels(labels-scope.sh); removes nothing by construction. Scratch before/after timelines remain in the PR body.scope:*still applied; hand-applied scope surviveslabels.ymlscope-job comment documents the #128 PUT race and the additive replacement.releaserelease_shape_warningemits::warning::only; call site gated on non-draft + missingrelease; no label write.Verification (this tick)
94f890a).test/labels-reconcile.test.sh: 146/146.test,self-guards, exercises,labels / reconcilepass;labels / scopeskipping on the latest labels run is non-blocking).pull_request_targetsafety preserved: mapping via API at baseCONFIG_REF; no PR code executed.Spec fit
Diagnosis → replace the write path (not sleep/re-apply/
releaseauto-set) matches the issue. Guard is warning-only per LABELS.md intent rules. Changelog fragment present.Approve the current head.
🔎 reviewing head
94f890aa1cApproved.
Head
94f890aa1ceadfc4a75afb66a9f887c3b55f9c09, reviewed whole against #130 (not just the delta since my round-1 approval atce24a1a).Round-1 delta since my staled approval. The push that staled me lands exactly what codex's changes-requested and grok's nits asked for, and nothing else:
.github/labeler.yml's header now describesactions/labels-scope(base-ref read, additive POST, #128 as the incident) instead of the replaced labeler@v5/sync-labels: falsestory;scope:labelsnow mapsactions/labels-scope/**andtest/labels-scope.test.shwith two fixtures deriving against the real mapping; the reconcile-job comment names labels-scope.94f890ais an empty retrigger commit (the #138 cancelled-queued-reconcile flap) — no tree delta over4002924. Codex's two blockers are genuinely resolved.Verified at this head (detached worktree; host lacks jq/yq/node/shellcheck, so I fetched static jq 1.7.1, yq 4.44.3, shellcheck 0.10.0, actionlint 1.7.7 into /tmp for the run):
test/run.sh: 14/18 files pass. The 4 failures (version,facts,changelog-armed,drill-recorded— all thepackage-jsoncases needing node) reproduce identically onmain— environmental, not this PR. Hostedtestcheck is green.test/labels-scope.test.sh: 23/23 with yq — glob translation, all four YAML spellings the family uses, ceremony's real mapping, and the four loud refusals (all-globs-to-all-files, branch matchers, non-map top level, backslashes).test/labels-reconcile.test.sh: 146/146, including the full warning matrix (bare-over-dev warns;-dev, equal, rc, unreadable-head silent; unreadable base still warns).shellcheck-all.sh(39 scripts) andactionlint-all.sh(6 workflows): clean.git diff --check: clean. No awk used in the changed scripts, so mawk-compat is vacuous here.Acceptance criteria (#130).
labels-scope.shisgh api repos/$REPO/issues/$PR_NUMBER/labels -f labels[]=...(labels-scope.sh:152);gh apiwith-ffields defaults to POST (confirmed fromgh api --help), and no PUT/DELETE/--methodexists anywhere in the script. The two other API calls are reads (config atCONFIG_REF, PR file list). Additive by construction.CONFIG_REF: ${{ github.sha }}(the base commit) and checks out only the ceremony implementation (dogfood atgithub.sha, consumers atCEREMONY_SELF_REF). Unsupported labeler keys fail loudly in jq with the label named.pull_request_targetsafety preserved.labels.yml's scope-step comment now states the PUT race, the #128 incident, and the additive replacement — matches the code.release_shape_warningemits one::warning::per call, one call site per PR per sweep (labels-reconcile.sh:620), gated on non-draft + noreleaselabel; the reconciler's only label writes aregh issue editof state labels /merge-next/stale—releaseis never among them.changelog.d/130.mdfollows the one-fragment-per-issue convention; mode/layout match neighbors (labels-scope.shat 100644 like most sibling action scripts — invoked viabashin the composite, so no exec bit needed).CI at head: all required checks green;
labels / scopeskips (event-gated: nopull_request_targeton a push), same as main.Non-blocking caveat (unchanged from round 1): the scratch-repo evidence (repro 30091568178, demo 30091675525, guard probe) is not independently verifiable —
claude-bot-andresmgsl/ceremony-scratch-130404s for this token too. The load-bearing claim (labeler v5 writes PUT even undersync-labels: false) was confirmed from labeler's source in round 1, and the fix's additive property is verifiable in the diff; the human should know which links the panel could not open.Round summary — handoff (round 2)
Round 1 (codex changes-requested; grok, kimi approved
ce24a1a): three points, all addressed on4002924—.github/labeler.ymlheader rewritten to describeactions/labels-scope(base-ref config read, additive POST, never removes, #128 incident) instead of the replaced labeler@v5 story;scope:labelsmapping extended toactions/labels-scope/**andtest/labels-scope.test.shwith two fixtures deriving against ceremony's real mapping; reconcile job comment names labels-scope.Round 2 (zero diff beyond the round-1 fixes;
94f890ais4002924plus the empty #138 CI-retrigger commit): codex approved94f890aat 12:26, grok at 12:48, kimi at 13:02 — all three panel verdicts bind the current head. CI is green on94f890a(labels / reconcilepass, full test/exercise suite pass). Noblocker:*stands.@danmt your review request from the first handoff attempt is still outstanding — over to you. Setting
state:needs-human.