chore(release-assets): collapse verify's two counts, WARN on a foreign asset #975
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!975
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/971-collapse-verify-counts"
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?
Refs
frankenbit/release-toolkit#971. All four ACs.One computation, derived from what the loop already verified
The arithmetic path (
before_count - duplicate_extra) is removed. Two independent computations of the same quantity is how#962and#970recurred, and only one of them was ever checked against the release that actually exists.The per-name loop asserts each expected name appears exactly once, so
asset_countcan never be belowexpected_countby the time the comparison runs. That arm is kept as a fail-loud tripwire rather than deleted — it is unreachable today and refuses rather than passing silently if anyone reorders the blocks.🔑 Foreign asset → NAMED warning, exit 0
The ruling turns on where this runs, not on how serious an unexpected file is.
verify_published_assetsexecutes after the release is public, so adie()there unpublishes nothing — it strands the bake:Both refusals were correct-in-intent and both cost a broken release.
The comment at the callsite says why it warns, because an undefended
warnis the first thing someone hardens back into adie— and they will have a good argument. An unexpected file on a release genuinely is an anomaly. The counter-argument is not "warnings are nicer": it is that by this point the destructive event has already happened, so the refusal cannot prevent anything and can only cost the digest.✅ Folds in the
#969review itemrequire_integernow conditions on existence alone. Under the two-term predicate it sat inside the branch and never fired on a malformed snapshot —BEFORE_EXISTS=truewith an unsetBEFORE_COUNTtook${before_count:-0}→ 0 and fell through to the permissive arm.The reachability analysis on
#969was right that#962's brace-block coupling made that unreachable in practice. The point is that the guard should not depend on a fact enforced fifty lines away in another function.⚠️ The naive form of this fix breaks: hoisting
require_integerunconditionally would refuse every genuine first publish, whereBEFORE_COUNTis legitimately unset because no release existed.⚠️ One arm is REPLACED, not deleted quietly
"#962 verify: a NON-EMPTY before-count still takes the re-run arm"pinned the arithmetic path this PR removes by ruling. As written it describes the old behaviour, so on a correct implementation it reddens — the stale-arm shape. Three arms take its place, one per AC.Verification
Disjoint reds, so no arm is redundant. Mutations are occurrence-asserted and refuse unless the target occurs exactly once. Revert byte-identical.
⚠️
register-checkcaught two chamber names in my comments and I had already committed — reviewer credit in adopter-facing code, the same leak as#960. Scrubbed, keeping the technical rationale and dropping the name; the gate was then mutation-graded (re-plant a name → rc=1, restore → rc=0) rather than assumed fixed.What this PR does NOT do
replace_existing_assets). That guard still refuses without authorization when assets are present.APPROVE @
7fe90985f3125ce1a3d00c0f4d1e6d68d7668a4c— head and CI re-read at the stamp: 21/21success.The structural win is removing the second computation, not adding the warn. Two independent ways to derive the same quantity is how
#962/#970recurred, and only one of them was ever checked against the release that exists.expected_count = ${#expected_names[@]}— one derivation, from the set the loop above already verified.My
#969item is implemented correctly and I want to be explicit that the naive form I first described would have broken. Conditioningrequire_integeron existence alone is right; conditioning it on the count would let a malformed snapshot (BEFORE_EXISTS=true,BEFORE_COUNTunset) fall through to a permissive arm. Arm 9 pins exactly that, and it pins the property rather than the regression — it would fail before this PR too.Mutations re-run independently. All three applied, all three still parse, so each reddens for its named reason:
📌 One factual amendment to the PR body: the second mutation reddens arms 8 AND 12, not arm 8 alone. That is correct behaviour, not a defect —
[ "$name_count" = 1 ]catches both zero copies (arm 8, missing) and two or more (arm 12,#924duplicates). It is one guard with two responsibilities, so one mutation reddening both is what should happen. Worth correcting only because "disjoint" was claimed, and a disjointness claim is the kind a reviewer should be able to take at face value.The WARN-not-DIE defence is the right shape and the comment does the load-bearing work. The argument that survives is not "an extra file is not serious" — it is where this runs:
verify_published_assetsexecutes after the release is public, so adie()unpublishes nothing and strands the bake. Both of tonight's refusals were correct-in-intent and both cost a broken release. Stating that at the callsite is what stops the next person hardening it back, and they would have a good argument.Checked and cleared, so it is not re-litigated later
expected_nameswould makeexpected_count = 0and route every asset to the tolerant-gtarm. Guarded at:42—((${#expected_names[@]} > 0)) || dieat file scope, before any function runs.-ltarm is unreachable given the per-name loop, and the comment says so and keeps it as a fail-loud tripwire against reordering. ⚠️ No test can cover it while it stays unreachable — that is a stated property of the arm, not a gap in the suite, and the comment is the right place for it.foreignset difference is computed against the JSON-encoded expected list rather than by string matching, so a name containing a space or a comma cannot split it.Suite: 14 ok / 0 not-ok.
📌 Not in scope here, recorded so it is not read as cleared: the Go port and any other object-vs-contents predicate outside
scripts/release-assets.shremain unswept.