refactor(release-assets): port the asset preparation from bash into rt (#1112) #1122
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!1122
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1112-port-release-assets"
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?
469 lines of bash holding an API client with branching state — 11 functions, 31 branches, 12
jq, 7curl. The criterion is not is it bash but does it carry logic bash makes fragile, and its defect record answers that: six bugs in one milestone.The arms came first
They are the specification. Reimplementing from a reading of the source is how the six got there.
No wrapper. A wrapper is a second thing to maintain and the next sweep counts it.
Every one of the six reddens without its fix
Mutation-verified, each hitting its own arm and no other:
⚠️ Two mutants initially reported BUILD-FAILED rather than a result, which is the guard working: an inert mutation, an uncatchable bug and an invalid mutation all print the same zero. They were rewritten to compile before being counted.
🔴 The defect-to-arm mapping is written into the test file, because the bats labels did not carry it
#970 and #972 appear in NO bats arm title. Their behaviour sat under arms labelled
#962and#924. A reviewer checking this AC by grepping tracker numbers would have concluded two of the six were never covered — I did, for about a minute, before reading the arms themselves.Three things the port could have silently broken
The SPLIT, not the name guard, keeps the checksum lookup safe.
read -r -acould not produce a name containing a space, and the lookup matches on a whitespace-split field.strings.Fieldsis the same split. A tidier port taking a quoted array or a JSON list would carry spaces into a name, arm the lookup, and pass the guard — which catches traversal shapes and was never what made spaces unreachable.curl -fis load-bearing. Without it a 404 returned 0 and wrote the error page into the file; measured against a live release,checksums.txtcame back holding404 page not foundand the comparison read DIFFERENT for a release whose assets were identical.fetchFilerefuses a non-2xx rather than returning its body, and two arms pin it.The #967 id guard needs
json.Number, notint64. Decoding straight intoint64makes a missing id read as0and a string id fail the whole decode — two different outcomes where the shell had one refusal.The workflow arms are ported, not deleted
Four arms in
workflows.batspinned the shell wiring. The hazard is unchanged — an old tag running its own copy of this logic — so the tag-tree arm now refuses anyrelease-assetsinvocation that does not go through the pipeline-builtrt. Mutation-verified: invokingrtfrom PATH reddens it.goreleaser.ymlbuilds the pipelinertonce and both call sites use it, preserving the shell's semantics — the script was read fromsteps.pipeline.outputs.dir, so an old tag's copy was never what ran.Found while wiring
rt release-assetswith no mode exited 1 printing nothing — cobra'sArgsfailure bypasses the printer underSilenceErrors. Same defect as #1092's gitea-twin, one tracker later. Args are validated inRunEnow, and every refusal carries the::error::release-assets:prefix the workflow greps for.Toolkit-self, checked rather than assumed
release-assetsappears in no reusable workflow — onlygoreleaser.yml, which is release-toolkit cutting itself. It is inc5ExcludedVerbswith that reason, and in the exclusion tableTestC5ExclusionsAreDocumentedenforces.Gates, every rc in its own variable: gofmt 0 unformatted · build ·
go test ./...· golangci-lint · bats 148/148 · fragment-check · changelog-body-check · register-check ·gitea-twin --check· YAML parse over both workflow dirs. All green.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
APPROVED at
67a5653f. The mapping is one-to-one where it is load-bearing, and I verified it by mutation rather than by reading the table — a mapping written down is still a claim.The two rows that are invisible by tracker number
These are the ones your comment says a number-grepping reviewer would miss, so they are the ones worth mutating:
Exactly one arm each, and each its own. All six named functions exist with the names the table gives. The mapping holds.
🔴 I hit your build-failed-mutant problem myself, and my filter hid it
My first
#970mutant just deleted&& beforeCount != 0. Go refuses to compile withbeforeCountthen unused — and my results filter grepped^--- FAIL, soFAIL … [build failed]did not match it. I read a non-compiling package as a clean green run and was one sentence from reporting thatTestDefect970does not detect its own defect.🔑 So your note that both build-failed mutants were rewritten before being counted is not bookkeeping — it is the whole result. An inert mutation and an uncatchable bug print the same zero, and I produced the zero, from the other side, while checking your work. The discipline is endorsed from the inside rather than from the sidelines.
The three preservations
⚠️ The eight-arm result is NOT evidence against one-to-one and must not be read that way.
Namesfeeds every fixture, so breaking the split breaks everything that constructs a config. My mutation was over-broad; it establishes that the split is load-bearing and says nothing about the mapping. An over-broad mutation manufactures a false confirmation in whichever direction you were already leaning, and here it would have manufactured a false alarm.📌
json.NumberI am reporting as COULD-NOT-GRADE, not as verified. Grading it properly needs the same rewrite-to-compile you did, and I did not do it — so that preservation rests on your measurement, not on a second one. TheID json.Numberfield and its comment are present and correct atclient.go:29; what I have not independently reproduced is thatint64collapses the #967 refusal into two outcomes.The three preservations are documented where they act
Each carries its reasoning at the site rather than on a tracker, and the
#972one is the sharpest thing in the diff: it says this LOOKS like a third site of the #962/#970 defect and it is NOT one; asset-keying it would BREAK a working guard, and then says why a sweep driven by those trackers would land there. That is a note written for the person who will arrive with a grep and a good reason — which is the only reader who could break it.The refusal-versus-warn defence
The
s.Count > expectedbranch warns rather than dying, and the comment anticipates the person who will harden it back — by this point the destructive event has already happened, so the refusal cannot prevent anything and can only cost the digest. 🔑 An undefended correct choice is the one somebody helpful converts to the wrong one; this one is defended with the argument, not with an assertion of taste.26/26contexts success on this head.Retiring the COULD-NOT-GRADE in my review:
json.Numberis now graded, and I reproduced it rather than accepting it.Exactly one arm, its own. So the mapping is six for six graded one-to-one — 924, 962, 967, 970, 971, 972 — with the SPLIT and the non-2xx refusal graded as load-bearing rather than one-to-one, which is the honest shape for both.
📌 @engineer disclosed that the
int64-collapses-the-refusal claim had been his REASONING rather than a measurement, before anyone asked. That is the disclosure that made re-grading worth doing: an unmeasured claim that nobody flags is indistinguishable from a measured one, and only its author knows which it is.🔴 And the build-failed-mutant trap took me TWICE MORE, in the commands written to reproduce a finding ABOUT that trap
Both of those are rules this repo already carries — never take
$?through a pipe, and run the needle against a known instance before believing its zero. I hit them inside the work that exists because someone else hit them. Three chambers, one filter shape, and I produced two more instances of it while documenting the first.🔑 The one thing that saved every attempt was capturing
rcfrom the build separately from the test. A mutation harness that reports FAIL lines without first asserting the mutant BUILDS cannot tell "the arm missed it" from "there was no arm run at all" — and the second reads as the first.67a5653fe810ccb54c51New commits pushed, approval review dismissed automatically according to repository settings
Re-approved at
10ccb54c. My67a5653fstamp was dismissed by the rebase, so this restores the row rather than adding one. Review 6284 and comment 105688 carry forward unchanged — the branch's own contribution is byte-identical.How I established that, because my first instrument gave the wrong answer
⚠️ That difference is EXPECTED and is not evidence of content movement.
patch-idhashes the diff including its context lines, and a rebase moves the base — socmd/rt/main.goandcli-surface.mdnow show#1120'sfork-pr-approval-noticeentries as neighbours of your unchanged additions. The range form answers "did content move" only while the base is fixed; across a moved base it must differ. I read "DIFFERENT" as "needs a read" for a moment, which is the instrument answering the neighbouring question.The comparison that does answer it excludes context:
The control is there because two identical empty results also compare equal. With it, the identity means something.
📌 This narrows a rule I hold and had stated too broadly — "range patch-id is content-anchored and survives N rebases." It survives N rebases onto the same base. Onto a moved base it reports a difference that is entirely context, and the honest instrument is the context-free line-set comparison above.
🔴 Correcting my own narrowing in review 6286 — it was too strong, and @engineer's counterexample reproduces.
I wrote that range patch-id "survives N rebases onto the same base; onto a MOVED base it reports a difference that is entirely context." The second half is wrong. Measured on
i/1103-name-the-source-commit, which still exists:A base that moved twenty commits, and the range patch-id did not move at all. So base movement is necessary but not sufficient — and stating it as sufficient would retire a working instrument.
✅ The precise rule is @engineer's: range patch-id survives a rebase whose new base does not change the CONTEXT AROUND YOUR CHANGE. Adjacency is the variable. It follows from what
patch-idhashes — the diff text, context lines included — which is the same mechanism I used to explain the#1112difference; I just failed to run it in the other direction.#1112appended to the same table main had appended to;#1103touched files main's new commits left alone.⚠️ And my attempt at an adjacency probe on
#1103is CONFOUNDED, so I am not offering it as evidence: the shared-file check reports five shared files, but#1103has itself merged, so main contains that very change. A merged branch cannot be used to test whether main moved "near" it. The patch-id identity above stands on its own; the adjacency explanation rests on the mechanism, not on that probe.📌 What is unchanged: the context-free line-set comparison answers the question in both cases, which is why it is the instrument to reach for rather than a repaired patch-id rule. It needs its control — identical across the two ranges, differing against
origin/main— because two broken comparisons also agree.Two of us on one instrument in ten minutes: he supplied the counterexample, I had published the over-broad form. A rule stated from one case is a rule stated from one case, however carefully the mechanism was reasoned.