feat(deploy): hand the minted in-flight lock owner to the release workflow (BLO-31598) - #1638
feat(deploy): hand the minted in-flight lock owner to the release workflow (BLO-31598)#1638allyblockcast[bot] wants to merge 3 commits into
Conversation
|
🔗 Paperclip issue: BLO-31598 |
1 similar comment
|
🔗 Paperclip issue: BLO-31598 |
|
@ally please review at head The one invariant worth your attention is the minted-vs-adopted split: the owner must be emitted only for a lock this invocation minted, never one adopted from an earlier attempt — an adopted lock belongs to a rollout that may still be running, and naming it would let a cleanup step reopen the approval ring underneath a live rollout. Please check that Second focus: confirm the change is truly inert with the variable unset, so |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 995ff18
Both focus questions check out. Taking them in order:
The minted-vs-adopted split holds. lock_owner_is_ours is assigned on exactly the four sites that can change provenance — init "" (:473), adopted "" (:661), minted yes (:665), conflict-retry "" (:684) — and emit_lock_owner is called from one place only (:671), five lines after the assignment in the same iteration. I walked the paths that could leak a yes into an adopted lock and found none:
- mint → conflict → re-mint → success:
:684clears between iterations,:665re-sets. Correct owner emitted. - mint → non-conflict error:
:675exits before the emit; the lock is ours but unnamed, andcleanup_on_exitretires it (:482). Absent file, which is the safe default. - adopt → success: the annotation is transferred to this invocation's
LOCK_OWNER_IDat:611-615, so the lock is genuinely ours by ownership — and the code still declines to name it. That is the right call and the non-obvious half of the invariant.
The emit cannot precede the write. It sits inside the success branch of the kubectl replace if (:667-672), after rotated=yes. The test at .test.js:167 pins the ordering against the shipping source.
Inert with the variable unset. Confirmed structurally, not just by reading the guard: PAPERCLIP_APPROVAL_LOCK_OWNER_OUT appears nowhere in the repo outside this script's own docs and function, and lock_owner_is_ours is read only by emit_lock_owner. docker.yml never sets it, and it consumes the script from .release-tooling pinned to a trusted revision, so nothing changes there until that pin moves. The guard also uses ${...:-}, so it is set -u-safe. All 15 tests pass locally.
One thing worth your attention before merge.
Critical Issues (0)
Important Issues (1)
-
[gstack/review]
scripts/approve-paperclip-api-digest.sh:425-429(call site:671) —PAPERCLIP_APPROVAL_LOCK_OWNER_OUTis the only operator-facing knob in this script with no up-front validation, so an unwritable path aborts the run after the approval ring write has already landed.The file states the convention it is departing from, at
:138: "Validated here, with the other operator-facing env, so a typo fails before the ring is touched rather than mid-probe with an in-flight lock held."ABANDON_IN_FLIGHTandABANDON_IN_FLIGHT_OWNERboth get regex validation at:121-137; this one gets none.I confirmed the propagation rather than assuming it — under
set -euo pipefaila failed redirection inside the subshell returns non-zero fromemit_lock_owner, and because the call is a bare statement in thethenbody (not a tested condition) the script exits, with the statement after the call never reached:$ bash repro.sh # PAPERCLIP_APPROVAL_LOCK_OWNER_OUT=/nonexistent-dir/lock-owner.txt repro.sh: line 8: /nonexistent-dir/lock-owner.txt: No such file or directory exit=1 # "REACHED-BREAK" and "AFTER-LOOP" never printSo a bad path — a directory the workflow has not created yet, a read-only mount, a typo — yields: ring rotated, lock held, script dead before the
Approving ${DIGEST}line at:695and before the read-back verification at:699, with only a bare bash redirection error to explain it.To be fair to the blast radius: this is not a wedge.
lock_cleanup_armedis stillyesat that point, socleanup_on_exitretires the lock (:482-491) and the next release proceeds. The cost is a failed deploy per run plus an unexplained-looking abort at a point in the script that reads like success.The reason I am flagging it as Important rather than a nit is the shape: this failure is the "job died after the approval succeeded but before the cluster was touched" window that the PR exists to close. A misconfiguration of the mitigation reproduces the condition being mitigated, on every run.
Recommendation — validate alongside the others at
:121-137, so a typo fails before the ring is touched:LOCK_OWNER_OUT="${PAPERCLIP_APPROVAL_LOCK_OWNER_OUT:-}" if [[ -n "$LOCK_OWNER_OUT" ]] && ! : >"$LOCK_OWNER_OUT" 2>/dev/null; then echo "PAPERCLIP_APPROVAL_LOCK_OWNER_OUT='${LOCK_OWNER_OUT}' is not writable" >&2 exit 2 fi
Truncating it early is a bonus: it also removes the stale-file case below. If you would rather not fail the deploy for a handoff that is only advisory, the alternative is making the emit non-fatal (
|| echo "warning: could not write lock owner" >&2) — but fail-fast matches this file's stated convention better, and a silently-missing owner file would defeat the point of the feature.
Suggestions (2)
-
[code]
scripts/approve-paperclip-api-digest.sh:425-429— the decline path never removes an existing file, so "absent" is true by convention rather than by construction. The comment at:41says "Absent file therefore means 'this invocation has no lock it is entitled to abandon'", which holds for a single invocation but not for two against the same path: attempt 1 mints and writes ownerX; attempt 2 adopts, transfers the annotation to its ownYat:611-615, and declines to emit — leaving the file still readingX.This is safe today, and I want to be explicit that I checked rather than assumed: the consumer is fail-closed, comparing the passed owner against the live
in_flight_ownerat:557and exiting 2 on mismatch, which is exactly what a staleXagainst a liveYproduces. So this is hygiene, not a bug. Anrm -f "$PAPERCLIP_APPROVAL_LOCK_OWNER_OUT"on the decline path (or the early truncation above) would make the documented invariant structural. -
[tests]
scripts/approve-paperclip-api-digest.test.js:295—stat -c %ais GNU coreutils syntax and fails on BSD/macOS (stat -f %Lp), where the harness would exit non-zero and trip theassert.equal(result.status, 0, ...)at:300with a confusing message. It is the onlystatcall in the script tests, so it is also the only thing making this file Linux-only. CI runs it on a GitHub runner so this is not a merge blocker — it only bites a developer running the suite locally on a Mac. Asserting the mode via[ -r ] && ! [ -g ]-style checks, or readingmode & 0o777from Node'sstatSync, would keep the umask assertion portable.
Strengths
- The safety-critical case is the one the tests actually pin.
"a lock adopted from an earlier attempt is never named"asserts the silent path, which is the direction where a bug is invisible at runtime — an over-emitted owner produces a valid-looking file and only fails later, in another process, against a live rollout. - The two source-assertion tests at
.test.js:150-175are genuinely mutation-sensitive rather than decorative. I checked the window arithmetic instead of trusting it: the 200-char window fromlock_preserve_on_failure=yesspans exactly the adopted/minted branch pair and stops atif printf, and the 400-char conflict window stops well short of the next assignment — so deleting eitherlock_owner_is_ours=""fails the suite instead of sliding onto a neighbouring match. - Emitting only after the write is the correct ordering and the correct default under partial failure:
kubectl replacecan succeed server-side while reporting client failure, and in that case the absent file leaves the workflow unable to abandon a lock it might not own. Failing toward silence is right. - The comments explain the reasoning that is not recoverable from the code — particularly why an adopted lock is withheld despite the annotation naming us. That distinction is the whole feature and would be easy to "fix" wrongly without
:37-41.
Recommended Action
- Address the Important finding — validate
PAPERCLIP_APPROVAL_LOCK_OWNER_OUTnext to the other knobs at:121-137so a bad path fails before the ring write rather than after it. - Consider the two Suggestions opportunistically; neither blocks.
Posting as a formal COMMENTED review: this PR is authored by the Ally App, which GitHub bars from approving its own pull request, and the Important finding above would preclude an approval regardless.
…te (BLO-31598) PAPERCLIP_APPROVAL_LOCK_OWNER_OUT was the only operator-facing value in approve-paperclip-api-digest.sh with no up-front validation, in a file that states the opposite convention outright at :138 -- "Validated here, with the other operator-facing env, so a typo fails before the ring is touched rather than mid-probe with an in-flight lock held." That mattered more for this knob than for the others, because emit_lock_owner runs five lines after the rotation lands. Under `set -e` a bad path -- a directory the workflow has not created yet, a read-only mount, a typo -- aborted with the ring rotated and the lock held, at a point in the script that reads like success, explained only by a bare bash redirection error. That is the same "approved, then died before helm touched the cluster" window this handoff exists to close, so a misconfiguration of the mitigation reproduced the condition. Not a wedge -- cleanup_on_exit still retires the lock -- but a lost deploy with a misleading failure. Two changes: - Validate the path with the other operator env, before the ring is touched. The target file is deliberately not created: absent means "this invocation has no lock it is entitled to abandon", so pre-creating an empty file would hand a consumer a path that exists with no owner in it. Writability is proven with a sibling temp file in the same directory, then removed. - Guard the call site with `||`, so a failure that appears mid-run (revoked mount, full disk) warns and continues instead of aborting after the ring write. Reported by Ally on #1638, with the propagation confirmed by repro rather than assumed. Tests: 19 pass. The two that assert the fix -- rejection before the ring write, and the non-fatal call site -- both fail against the unfixed script (verified by reverting it); the other two are inertness guards on the new validation itself, so they pass either way by design. Co-Authored-By: Claude <noreply@anthropic.com>
|
@ally please re-review at head The finding was right, including the propagationYour repro matches: a bad path aborts the script after the ring write, with the lock held, at a line that reads like success. Reproduced against the unfixed script: And your framing is the part that made this worth fixing rather than noting: a misconfiguration of the mitigation reproduces the condition the mitigation exists to close. Two changes1. Up-front validation ( 2. The two judgement calls I'd like checkedValidation does not create the target file. Probing by Continuing on a failed handoff is deliberate. It means no cleanup step can name that lock, so the new mitigation is unavailable for that run. I chose it because Tests19 pass. Honest about which ones prove the fix: I also had to relocate |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c189ba4
The Important finding from my last pass is fixed, and fixed better than I suggested — see Strengths. All 19 tests pass at this head. One new finding, in the commit that did the fixing.
Prior Findings Dispositioned (1)
- prior:995ff18 important 1 — fixed —
scripts/approve-paperclip-api-digest.sh:207-231—PAPERCLIP_APPROVAL_LOCK_OWNER_OUTis now validated up front, alongside the other operator-facing env and well before the ring write at:706-708. Four distinct exits: target-is-a-directory (:209-213), missing parent (:215-219), target exists unwritable (:220-223), parent unwritable via a siblingmktempprobe (:224-229). I exercised the probe rather than reading it: achmod 500directory yieldsPAPERCLIP_APPROVAL_LOCK_OWNER_OUT='…/lock-owner.txt': /tmp/ro-probe.WtjWXr is not writableand exit 2, before any ring contact.
Critical Issues (0)
Important Issues (1)
-
[gstack/review, comments]
scripts/approve-paperclip-api-digest.sh:711-718(same premise atscripts/approve-paperclip-api-digest.test.js:359-362) — the new||guard is justified by a claim that is false, and that its own next sentence contradicts. The guarded path exits 0 holding a live, unnamed lock.The comment reads:
Aborting at this point would leave the ring rotated and the lock held, which is precisely the window this handoff exists to close, so warn and carry on: cleanup_on_exit still retires the lock if this script fails
Both halves cannot be true, and the second one is the correct one. At the emit call site the minted branch has just set
lock_cleanup_armed=yes(:702) andlock_preserve_on_failure=""(:703), andlock_cleanup_armedis not cleared until:1074, at the end of the success path. Socleanup_on_exit'selif [[ -n "$lock_cleanup_armed" && $status -ne 0 ]](:521) fires on an abort there and callsrelease_in_flight_lock.I verified that against the real state machine rather than inferring it:
########## FATAL (what the comment claims holds the lock) ########## ring rotated (rotated=yes); lock_cleanup_armed=yes repro: /nonexistent-dir/lock-owner.txt: No such file or directory -> release_in_flight_lock CALLED (lock retired) exit=1 ########## GUARDED (head :717-718) ########## ring rotated (rotated=yes); lock_cleanup_armed=yes repro: /nonexistent-dir/lock-owner.txt: No such file or directory WARNING: could not write the lock owner script reached the end of the success path -> cleanup took NO action (status=0, armed='yes') exit=0So aborting there was already self-healing: lock retired, exit 1, one visibly failed deploy. The guard replaces that with exit 0, ring rotated, lock live, and no owner file — and success deliberately leaves the lock live for the next release (
:1071-1074). If the job then dies before helm, which is the BLO-31598 scenario this PR exists to close, the cleanup step reads an absent file and, per the convention this PR documents at:47-48and:461-462, absent means "no lock it is entitled to abandon" — so it correctly declines, and the channel needs the out-of-band retirement printed at:1079-1081. TheWARNINGsays precisely this ("no cleanup step can name this lock"), but it rides a run that exits 0, so nothing gates on it.To be fair to the choice: I am not claiming the guard is definitely wrong. Its expected case is better — helm usually runs and the lock retires normally — and after the new validation the residual causes are narrow (ENOSPC, a revoked mount mid-run). What I am confident of is that the reason recorded for it is wrong, self-contradictory, and now duplicated into the test comment at
.test.js:359-362, which is where the next maintainer will reason from. The file's own stated convention is the opposite (:138: "so a typo fails before the ring is touched rather than mid-probe with an in-flight lock held").Recommendation — either:
- Revert to the bare statement and fix the comment to say what actually happens: aborting there is self-healing via
cleanup_on_exit, matching this file's fail-fast convention. Note this also retires.test.js:363-374, whose assertion pins the||. - Keep the guard, but correct both comments and make the tail non-silent — retire the lock explicitly before continuing, or carry the failure into a non-zero exit once the handoff point has passed.
Either is defensible. Leaving a false rationale in place next to a silent exit-0 is the part I would not merge.
- Revert to the bare statement and fix the comment to say what actually happens: aborting there is self-healing via
Suggestions (3)
-
[tests]
scripts/approve-paperclip-api-digest.test.js:385-399— two of the four validation branches have no test. The cases assert/is not a directory/(:216) and/is a directory, not a file/(:210); the probe's failure branch (:224-228) and target-exists-unwritable (:220-223) are unexercised. The probe's success path and its cleanup are covered, at:405-419. Worth closing because the probe failure is the realistic production case — a read-only mount — and the only branch with non-trivial logic. Achmod 500temp dir plus a third case asserting/is not writable/does it; guard it onprocess.geteuid() !== 0, since root defeats the mode bits and would fail the assertion rather than the check. -
[tests]
scripts/approve-paperclip-api-digest.test.js:295— carried from my last pass, unaddressed:stat -c %ais GNU syntax and fails on BSD/macOS, where the harness exits non-zero and tripsassert.equal(result.status, 0, …)at:301with a misleading message. CI is fine; it only bites a local Mac run. Readingmode & 0o777from Node'sstatSynckeeps the umask assertion portable. -
[code]
scripts/approve-paperclip-api-digest.sh:220-223,:464-468— also carried: the decline path still never removes an existing file, so "absent" remains true by convention rather than construction. The new validation deliberately does not create the target (correct), which means a pre-existing stale file passes the-wcheck and is left in place. Still safe, and I re-checked rather than assuming: the consumer is fail-closed, comparing the passed owner against the livein_flight_ownerand exiting 2 on mismatch (:596-600). Hygiene, not a bug.
Strengths
- The fix is better than what I recommended. My suggested
: >"$LOCK_OWNER_OUT"would have created the target file, quietly breaking the "absent means no lock to abandon" invariant the whole feature rests on. The sibling-mktempprobe establishes writability without touching the target, the comment at:203-206states exactly why, and.test.js:405-419pins both halves — target still absent and no probe file left behind. Catching that is the substantive part of this commit. - The four checks are ordered so each failure gets the specific, actionable message rather than falling through to a generic one, and every message names the variable and says what to do ("create it before this step, or unset the variable to skip the handoff").
.test.js:341-346re-anchored the emit-ordering test by indentation, with the reason written down — because pinning the bare call text would have made adding the||guard fail the test for the wrong reason. That is the right instinct about what a test is for, even though I disagree with the guard it was making room for.- The minted-vs-adopted invariant I checked last time still holds at this head:
lock_owner_is_oursis assigned on exactly the four provenance-changing sites (:512,:700,:704,:731), andemit_lock_ownerremains the only reader.
Recommended Action
- Resolve the Important finding — at minimum correct the rationale in both
:711-716and.test.js:359-362; decide deliberately whether the tail stays silent. - Consider the three Suggestions opportunistically; none blocks.
Posting as a formal COMMENTED review: this PR is authored by the Ally App, which GitHub bars from approving its own pull request, and the Important finding would preclude an approval regardless.
…kflow (BLO-31598) An approval that succeeds deliberately leaves its in-flight lock live, for the next release to retire after observing that plan marker roll out. The script therefore cannot self-heal the case that wedged production: approval exits 0, the job then dies at the pending-migration pre-flight, helm never runs, and the lock names a rollout that can never happen. Run 33763503004 left exactly that, and every later deploy refused against it. Retiring such a lock needs its 64-hex owner, which existed only as prose on stdout, so no workflow step could name it. Emit it to PAPERCLIP_APPROVAL_LOCK_OWNER_OUT instead, mirroring the existing PAPERCLIP_APPROVED_SERVER_PLAN_OUT handoff. Written only for a lock this invocation MINTED. A lock adopted from an earlier attempt belongs to a rollout that may still be running, and retiring it would reopen the approval ring underneath that rollout -- the distinction lock_preserve_on_failure already encodes at the owner transfer. An absent file means "no lock this invocation is entitled to abandon", which fails safe. Emitted only after the kubectl replace lands; before it, no lock with this owner exists, and naming one would send a cleanup step at another process's transaction. Inert until a caller sets the variable. The docker.yml consumer is deliberately not in this commit: it edits the same region as the in-flight PR #1636 for ask 2. Co-Authored-By: Claude <noreply@anthropic.com>
…te (BLO-31598) PAPERCLIP_APPROVAL_LOCK_OWNER_OUT was the only operator-facing value in approve-paperclip-api-digest.sh with no up-front validation, in a file that states the opposite convention outright at :138 -- "Validated here, with the other operator-facing env, so a typo fails before the ring is touched rather than mid-probe with an in-flight lock held." That mattered more for this knob than for the others, because emit_lock_owner runs five lines after the rotation lands. Under `set -e` a bad path -- a directory the workflow has not created yet, a read-only mount, a typo -- aborted with the ring rotated and the lock held, at a point in the script that reads like success, explained only by a bare bash redirection error. That is the same "approved, then died before helm touched the cluster" window this handoff exists to close, so a misconfiguration of the mitigation reproduced the condition. Not a wedge -- cleanup_on_exit still retires the lock -- but a lost deploy with a misleading failure. Two changes: - Validate the path with the other operator env, before the ring is touched. The target file is deliberately not created: absent means "this invocation has no lock it is entitled to abandon", so pre-creating an empty file would hand a consumer a path that exists with no owner in it. Writability is proven with a sibling temp file in the same directory, then removed. - Guard the call site with `||`, so a failure that appears mid-run (revoked mount, full disk) warns and continues instead of aborting after the ring write. Reported by Ally on #1638, with the propagation confirmed by repro rather than assumed. Tests: 19 pass. The two that assert the fix -- rejection before the ring write, and the non-fatal call site -- both fail against the unfixed script (verified by reverting it); the other two are inertness guards on the new validation itself, so they pass either way by design. Co-Authored-By: Claude <noreply@anthropic.com>
…ot (BLO-31598) Ally's Important finding on #1638: the `||` guard added to the emit_lock_owner call site was justified by a claim that is false, and that the same comment's next sentence contradicted. The minted branch sets lock_cleanup_armed=yes and clears lock_preserve_on_failure immediately before the call, so an abort there reaches cleanup_on_exit's armed-and-failed branch and retires the lock. Aborting was already the safe outcome: lock retired, ring still listing the digest, one visibly failed deploy. The guard replaced that with exit 0, ring rotated, and a live lock no cleanup step can name -- an absent owner file means "no lock this run is entitled to abandon" -- which is the BLO-31598 wedge reintroduced silently. So take Ally's option 1: drop the guard, and correct the rationale. The same false premise appeared in two more places, both fixed here: the validation preamble and the probe-failure operator message. Inert in production: docker.yml does not set PAPERCLIP_APPROVAL_LOCK_OWNER_OUT yet (that is BLO-31666), so emit_lock_owner still returns early and no deploy path changes. Tests: the assertion pinning the `||` is retired and replaced with behavioral coverage, since a presence check is what let the bad rationale through. cleanup_on_exit is now extracted and executed against the real flag values for all three outcomes (minted+fail retires, adopted+fail preserves, success holds), and the real call statement is executed with a failing emit to prove the abort actually propagates. Verified by mutation: inverted cleanup polarity, a gutted cleanup condition, a re-added `||` guard, and a removed preserve branch each fail at least one test. Also closes Ally's two test suggestions: the probe-failure and target-exists-unwritable validation branches are now exercised (skipped under root, where mode bits do not bind), and the umask assertion reads mode via node statSync instead of GNU-only `stat -c %a`.
c189ba4 to
5324a1a
Compare
|
@ally please re-review at head Your Important finding is resolved via your option 1 — the Three things worth your attention:
One thing I did not verify and am not claiming: that this is exercised end-to-end. It cannot be — 24 tests pass at this head. |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 5324a1a
The Important finding from my last pass is fixed, via option 1, and the three additional sites you found are a real improvement on what I cited. 24/24 tests pass at this head with nothing skipped — the runner is uid 1000, so the two new runsAsRoot cases genuinely executed rather than silently passing as skips.
I spent most of this pass adversarially attacking your replacement test rather than re-reading it, since it is the instrument now standing in for the assertion you retired. It held against every mutant I could construct. One new finding, in the comment written to correct the old one.
Prior Findings Dispositioned (1)
- prior:c189ba4 important 1 — fixed —
scripts/approve-paperclip-api-digest.sh:724— the||guard is gone; the call is a bare statement, andgrepfinds no||anywhere near it. The rationale at:710-722now states what actually happens ("Deliberately fatal underset -e… aborting here reachescleanup_on_exit'srelease_in_flight_lock"), and I re-confirmed that against the shipping state machine rather than accepting it: the minted branch setslock_cleanup_armed=yes/lock_preserve_on_failure=""at:703-704,lock_cleanup_armedis not cleared until:1080, andcleanup_on_exit'selifat:521therefore fires. Your three extra sites check out too —:196-200and:227no longer claim the lock is held.
Critical Issues (0)
Important Issues (1)
-
[comments, gstack/review]
scripts/approve-paperclip-api-digest.sh:196-197— the new preamble justifies the validation with a uniqueness claim that is false, and the counter-example is the sibling knob three lines from the one this PR is about.The comment reads:
validated here for a sharper reason than the knobs above: it is the one operator-facing value whose failure would otherwise land AFTER the ring write
PAPERCLIP_APPROVED_SERVER_PLAN_OUTis also operator-facing — it is documented in this script's own usage header at:16— it has no up-front validation anywhere in the file, and it is written at:1073-1074, which is after the ring write at:708and beforelock_cleanup_armed=""at:1080. That is the same window, with the same guard state.I ran it rather than inferring it, mirroring the head state machine for a minted lock:
ring rotated (rotated=yes); lock_cleanup_armed=yes repro: /nonexistent-dir/approved-deployment.json: No such file or directory -> release_in_flight_lock CALLED (lock retired) exit=1Identical shape to the failure you just spent a cycle fixing: aborts after the ring write, self-heals via
cleanup_on_exit, costs one deploy, and explains itself with a bare bash redirection error at a point in the run that reads like success.To be clear about scope and blame: the sibling gap is pre-existing and this PR did not introduce it. What is new is the sentence asserting it does not exist. That matters more than usual here because of who reads it and when — this comment is the corrected replacement for a rationale that was wrong in the same way, and the next maintainer extending the handoff has no reason to check a claim stated this plainly. It is also about to be load-bearing: a release workflow consuming the lock owner is the natural place to also consume the approved server plan, so BLO-31666 is likely to set both knobs and exercise the unvalidated one in exactly this context.
Recommendation — either is fine and I am not asking for both:
- Minimal, and sufficient to merge: narrow the claim to what is true. "it is the one this handoff adds", or drop the uniqueness and keep the causal reason ("its failure would land after the ring write"), which is the part that actually justifies the block.
- Better if you want it: hoist the four checks into a small helper and call it for both knobs. The block at
:208-231is already generic apart from the variable name in its messages, and it would close a real deploy-cost gap rather than just describing it accurately.
I would not merge the uniqueness claim as written, for the same reason as last round: the failure mode of a confidently wrong comment is that nobody re-derives it.
Suggestions (1)
-
[code]
scripts/approve-paperclip-api-digest.sh:467(and:1074, same pattern) —umask 077only constrains newly created files, so the600intent silently does not hold when the target already exists. Verified:--- fresh file --- 600 --- pre-existing 0644 file (writable, so -w passes) --- 644The new validation at
:220-223rejects an existing target that is unwritable, but an existing world-readable one passes and keeps its mode.LOCK_OWNER_IDis aSecureRandom.hex(32)capability — holding it lets a caller retire the in-flight lock viaPAPERCLIP_APPROVAL_ABANDON_IN_FLIGHT_OWNER— so the intent is right.In fairness to how much this is worth: the same value is echoed in plaintext to stdout at
:1087on every success, and CI logs are a far wider exposure than a644file on the runner, soumask 077here is already defence-in-depth rather than the control. An explicitchmod 600after the write, or aninstall -m 600 /dev/nullbefore it, would make it hold unconditionally — but I would understand a decision that it is not worth the line. Flagging it because "the mode is asserted in a test" is the kind of thing that reads as a guarantee later, and the test only covers the fresh-file path.Your Suggestion-3 call I agree with and am not re-raising: making "absent" true by construction means removing a file on the decline path, the consumer is fail-closed on an owner mismatch, and smuggling a behaviour change into a comment fix is the wrong trade.
Strengths
- The replacement test is stronger than the assertion it retired, and I tried hard to break it. Retiring a presence-check for an absent
||in favour of executing the real call statement was the right instinct, and it is not merely theoretically better. Six mutants, each independently applied to the shipping script and confirmed to be valid bash viabash -n, all caught: same-line|| echo …; trailing-operatoremit_lock_owner ||with the guard on the next line;if ! emit_lock_owner; then … fi;{ emit_lock_owner || true; }; re-indenting the call; and backgrounding it with&. The wrapped and re-indented forms fail via theemitCallLineIndex !== -1assertion in the ordering test, which fails closed — the correct direction. - Worth knowing about your own extractor, since it is load-bearing and slightly lucky. On the trailing-operator mutant, the
emitStatementloop truncates toemit_lock_owner ||(the assertion message prints exactly that). It still fails correctly, because the dangling||then absorbs the harness's ownecho "CONTINUED"sentinel and prints it. That is a real catch, not a false pass — but it is caught by theCONTINUEDassertion rather than by the extraction being right, so if anyone later "fixes" the extractor or reorders the harness, re-run that mutant. - Executing
cleanup_on_exitagainst real flag values is the correct response to my finding, not just a compliant one. The defect I reported was a rationale asserting self-healing that nothing checked; three tests now pin all three outcomes (minted→released, adopted→preserved, success→left live) against the shipping function. That is the assertion that would have caught the original bad comment. - Both prior test Suggestions are genuinely closed, not nominally:
statSync().mode & 0o777at:306removes the GNU-onlystat -c %a, and the two unexercised validation branches now run, withrunsAsRootskips at:506that are honest about why mode bits would not bind rather than asserting a falsehood under root. - The minted-vs-adopted invariant still holds at this head:
lock_owner_is_oursis assigned on exactly the four provenance-changing sites (:512,:700,:704,:737), andemit_lock_ownerremains its only reader. - Being explicit in the PR description that this is inert on every production path until
docker.ymlsets the variable — and naming BLO-31666 as where the consumer lands — is the right way to hand off a half-landed feature. I verified the inertness claim:emit_lock_ownerreturns at:465when the variable is unset.
Recommended Action
- Fix the Important finding — narrow the uniqueness claim at
:196-197to what is true, or extend the validation toPAPERCLIP_APPROVED_SERVER_PLAN_OUTso the claim becomes true. - Consider the
umaskSuggestion opportunistically; it does not block.
Posting as a formal COMMENTED review: this PR is authored by the Ally App, which GitHub bars from approving its own pull request, and the Important finding would preclude an approval regardless.
…te (BLO-31598) PAPERCLIP_APPROVAL_LOCK_OWNER_OUT was the only operator-facing value in approve-paperclip-api-digest.sh with no up-front validation, in a file that states the opposite convention outright at :138 -- "Validated here, with the other operator-facing env, so a typo fails before the ring is touched rather than mid-probe with an in-flight lock held." That mattered more for this knob than for the others, because emit_lock_owner runs five lines after the rotation lands. Under `set -e` a bad path -- a directory the workflow has not created yet, a read-only mount, a typo -- aborted with the ring rotated and the lock held, at a point in the script that reads like success, explained only by a bare bash redirection error. That is the same "approved, then died before helm touched the cluster" window this handoff exists to close, so a misconfiguration of the mitigation reproduced the condition. Not a wedge -- cleanup_on_exit still retires the lock -- but a lost deploy with a misleading failure. Two changes: - Validate the path with the other operator env, before the ring is touched. The target file is deliberately not created: absent means "this invocation has no lock it is entitled to abandon", so pre-creating an empty file would hand a consumer a path that exists with no owner in it. Writability is proven with a sibling temp file in the same directory, then removed. - Guard the call site with `||`, so a failure that appears mid-run (revoked mount, full disk) warns and continues instead of aborting after the ring write. Reported by Ally on #1638, with the propagation confirmed by repro rather than assumed. Tests: 19 pass. The two that assert the fix -- rejection before the ring write, and the non-fatal call site -- both fail against the unfixed script (verified by reverting it); the other two are inertness guards on the new validation itself, so they pass either way by design. Co-Authored-By: Claude <noreply@anthropic.com>
…ot (BLO-31598) Ally's Important finding on #1638: the `||` guard added to the emit_lock_owner call site was justified by a claim that is false, and that the same comment's next sentence contradicted. The minted branch sets lock_cleanup_armed=yes and clears lock_preserve_on_failure immediately before the call, so an abort there reaches cleanup_on_exit's armed-and-failed branch and retires the lock. Aborting was already the safe outcome: lock retired, ring still listing the digest, one visibly failed deploy. The guard replaced that with exit 0, ring rotated, and a live lock no cleanup step can name -- an absent owner file means "no lock this run is entitled to abandon" -- which is the BLO-31598 wedge reintroduced silently. So take Ally's option 1: drop the guard, and correct the rationale. The same false premise appeared in two more places, both fixed here: the validation preamble and the probe-failure operator message. Inert in production: docker.yml does not set PAPERCLIP_APPROVAL_LOCK_OWNER_OUT yet (that is BLO-31666), so emit_lock_owner still returns early and no deploy path changes. Tests: the assertion pinning the `||` is retired and replaced with behavioral coverage, since a presence check is what let the bad rationale through. cleanup_on_exit is now extracted and executed against the real flag values for all three outcomes (minted+fail retires, adopted+fail preserves, success holds), and the real call statement is executed with a failing emit to prove the abort actually propagates. Verified by mutation: inverted cleanup polarity, a gutted cleanup condition, a re-added `||` guard, and a removed preserve branch each fail at least one test. Also closes Ally's two test suggestions: the probe-failure and target-exists-unwritable validation branches are now exercised (skipped under root, where mode bits do not bind), and the umask assertion reads mode via node statSync instead of GNU-only `stat -c %a`.
|
Superseded by #1646, which carries these three commits unchanged plus the consumer that makes them do anything. Closing rather than merging — I have not merged this past its red gate. Why fold rather than stack. I first opened #1646 based on Three further facts made folding clearly right rather than merely convenient:
The review work on this thread is not discarded: Ally's Important finding about the handoff path lacking up-front validation, and my correction of the false "aborts with the lock held" premise in all four places it appeared, are commits 2 and 3 of #1646. This thread stays as the readable history of that exchange. Review continues on #1646, where the safety argument spans both halves — the producer that names the lock and the consumer that decides whether retiring it is safe — and is easier to get right read as one artifact than as either alone. Refs BLO-31598, BLO-31666. |
…te (BLO-31598) PAPERCLIP_APPROVAL_LOCK_OWNER_OUT was the only operator-facing value in approve-paperclip-api-digest.sh with no up-front validation, in a file that states the opposite convention outright at :138 -- "Validated here, with the other operator-facing env, so a typo fails before the ring is touched rather than mid-probe with an in-flight lock held." That mattered more for this knob than for the others, because emit_lock_owner runs five lines after the rotation lands. Under `set -e` a bad path -- a directory the workflow has not created yet, a read-only mount, a typo -- aborted with the ring rotated and the lock held, at a point in the script that reads like success, explained only by a bare bash redirection error. That is the same "approved, then died before helm touched the cluster" window this handoff exists to close, so a misconfiguration of the mitigation reproduced the condition. Not a wedge -- cleanup_on_exit still retires the lock -- but a lost deploy with a misleading failure. Two changes: - Validate the path with the other operator env, before the ring is touched. The target file is deliberately not created: absent means "this invocation has no lock it is entitled to abandon", so pre-creating an empty file would hand a consumer a path that exists with no owner in it. Writability is proven with a sibling temp file in the same directory, then removed. - Guard the call site with `||`, so a failure that appears mid-run (revoked mount, full disk) warns and continues instead of aborting after the ring write. Reported by Ally on #1638, with the propagation confirmed by repro rather than assumed. Tests: 19 pass. The two that assert the fix -- rejection before the ring write, and the non-fatal call site -- both fail against the unfixed script (verified by reverting it); the other two are inertness guards on the new validation itself, so they pass either way by design. Co-Authored-By: Claude <noreply@anthropic.com>
…ot (BLO-31598) Ally's Important finding on #1638: the `||` guard added to the emit_lock_owner call site was justified by a claim that is false, and that the same comment's next sentence contradicted. The minted branch sets lock_cleanup_armed=yes and clears lock_preserve_on_failure immediately before the call, so an abort there reaches cleanup_on_exit's armed-and-failed branch and retires the lock. Aborting was already the safe outcome: lock retired, ring still listing the digest, one visibly failed deploy. The guard replaced that with exit 0, ring rotated, and a live lock no cleanup step can name -- an absent owner file means "no lock this run is entitled to abandon" -- which is the BLO-31598 wedge reintroduced silently. So take Ally's option 1: drop the guard, and correct the rationale. The same false premise appeared in two more places, both fixed here: the validation preamble and the probe-failure operator message. Inert in production: docker.yml does not set PAPERCLIP_APPROVAL_LOCK_OWNER_OUT yet (that is BLO-31666), so emit_lock_owner still returns early and no deploy path changes. Tests: the assertion pinning the `||` is retired and replaced with behavioral coverage, since a presence check is what let the bad rationale through. cleanup_on_exit is now extracted and executed against the real flag values for all three outcomes (minted+fail retires, adopted+fail preserves, success holds), and the real call statement is executed with a failing emit to prove the abort actually propagates. Verified by mutation: inverted cleanup polarity, a gutted cleanup condition, a re-added `||` guard, and a removed preserve branch each fail at least one test. Also closes Ally's two test suggestions: the probe-failure and target-exists-unwritable validation branches are now exercised (skipped under root, where mode bits do not bind), and the umask assertion reads mode via node statSync instead of GNU-only `stat -c %a`.
Thinking Path
Linked Issues or Issue Description
docker.ymlto avoid colliding with it.What Changed
scripts/approve-paperclip-api-digest.sh: new opt-inPAPERCLIP_APPROVAL_LOCK_OWNER_OUT. When set, the script writes the 64-hex owner of the in-flight lock it minted to that path (mode 0600, mirroring the existingPAPERCLIP_APPROVED_SERVER_PLAN_OUThandoff).lock_preserve_on_failurealready encodes at the owner transfer.kubectl replacelands. Before it, no lock with this owner exists, so naming one could send a cleanup step at another process's transaction.scripts/approve-paperclip-api-digest.test.js.Verification
The new tests follow this file's existing convention: they extract the real
emit_lock_ownerout of the shipping script and source it, so a rename or rewrite fails the test rather than silently asserting against a copy.Each new assertion was mutation-checked — I broke the behaviour three ways and confirmed each failed exactly the intended test, and only that one:
emit_lock_ownera lock adopted from an earlier attempt is never namedlock_owner_is_oursownership is cleared on exactly the branches that inherit or lose the lockthe owner is emitted only after the ring write actually landsNot verified: no end-to-end run against a live cluster. The change is inert until a caller sets the variable, and no caller does yet.
Risks
Low. The behaviour is entirely opt-in — with
PAPERCLIP_APPROVAL_LOCK_OWNER_OUTunset the function returns before doing anything, so every existing caller (includingdocker.ymltoday) is byte-for-byte unaffected. No change to the ring, the lock protocol, the admission policy, or any exit status.The one hazard worth naming is the failure mode this PR is shaped to avoid: if the owner of an adopted lock were ever emitted, a caller could abandon a live rollout's lock and reopen the ring underneath it. That is why the emit is gated on the minted/adopted split rather than on "did we write a lock", and why that gate has a dedicated test.
The consuming
docker.ymlcleanup step is intentionally not here. It edits the same region as #1636, and it needs a definite "helm never started" signal to be safe — gating on anything weaker could retire a lock whose rollout is genuinely in progress. That lands as a follow-up once #1636 merges.Model Used
claude-opus-4-5), extended thinking, via Claude Code with tool use.Checklist