port release-assets.sh to rt — 469 lines of API and JSON logic that bash makes fragile #1112

Closed
opened 2026-09-04 10:23:24 +02:00 by bosun · 3 comments
Owner

scripts/release-assets.sh carries more logic than bash is good at holding, and the defect record shows it: six bugs were fixed in it during a single milestone.

469 lines · 11 functions · 31 branches · 12 jq invocations · 7 curl calls

It fetches releases, diffs asset sets, uploads, verifies stored sizes against declared ones, and decides whether to replace. That is an API client with branching state — the shape that is hard to debug, fragile under change, and slow when the logic grows.

Prior defects, all in this one file: #924 (a re-run APPENDS instead of replacing) · #962 (refuses a push cut whose release has zero assets) · #967 (a load-bearing id guard) · #970 (a post-condition expecting after==before) · #971 (two count computations that disagreed) · #972 (object-keyed on purpose, swept wrongly).

AC

  • the behaviour moves into rt as a verb, with the existing bats arms kept as the acceptance oracle until the Go tests match them
  • the shell script is deleted, not left as a wrapper — a wrapper is a second thing to maintain
  • reusable-release.yml calls the verb; no workflow keeps a bash path to the same behaviour — the AC named the wrong workflow. release-assets was toolkit-self and never a reusable path: goreleaser.yml calls release-assets prepare and verify, and docs/architecture/contracts/cli-surface.md records it as "appears in NO reusable workflow, checked rather than assumed." The operative half — no workflow keeps a bash path — holds.
  • every one of the six prior defects has a Go test that reddens without the fix

⚠️ Its bats arms are the specification. Port the tests first or the port has no oracle, and six known defects are exactly the regressions a rewrite reintroduces.

`scripts/release-assets.sh` carries more logic than bash is good at holding, and the defect record shows it: six bugs were fixed in it during a single milestone. ``` 469 lines · 11 functions · 31 branches · 12 jq invocations · 7 curl calls ``` It fetches releases, diffs asset sets, uploads, verifies stored sizes against declared ones, and decides whether to replace. That is an API client with branching state — the shape that is hard to debug, fragile under change, and slow when the logic grows. Prior defects, all in this one file: #924 (a re-run APPENDS instead of replacing) · #962 (refuses a push cut whose release has zero assets) · #967 (a load-bearing id guard) · #970 (a post-condition expecting after==before) · #971 (two count computations that disagreed) · #972 (object-keyed on purpose, swept wrongly). ## AC - [x] the behaviour moves into `rt` as a verb, with the existing bats arms kept as the acceptance oracle until the Go tests match them - [x] the shell script is deleted, not left as a wrapper — a wrapper is a second thing to maintain - [x] `reusable-release.yml` calls the verb; no workflow keeps a bash path to the same behaviour — **the AC named the wrong workflow.** `release-assets` was toolkit-self and never a reusable path: `goreleaser.yml` calls `release-assets prepare` and `verify`, and `docs/architecture/contracts/cli-surface.md` records it as *"appears in NO reusable workflow, checked rather than assumed."* The operative half — no workflow keeps a bash path — holds. - [x] every one of the six prior defects has a Go test that reddens without the fix ⚠️ **Its bats arms are the specification.** Port the tests first or the port has no oracle, and six known defects are exactly the regressions a rewrite reintroduces.
Author
Owner

Closing — every AC verified against origin/main at 10ccb54c, not against the PR description.

  • the behaviour moves into rt as a verb, with the existing bats arms kept as the acceptance oracle until the Go tests match them
  • the shell script is deleted, not left as a wrapper
  • reusable-release.yml calls the verbRETIRED (wrong workflow named at filing): release-assets is toolkit-self and appears in no reusable workflow. The caller is goreleaser.yml (rt-pipeline release-assets prepare at :197, … verify at :345). Recorded in docs/architecture/contracts/cli-surface.md:79, which states this as checked rather than assumed. No workflow keeps a bash path — the only surviving mentions of release-assets.sh are that prose row.
  • every one of the six prior defects has a Go test that reddens without the fix

Verification

scripts/release-assets.sh on main        DELETED  (positive+negative control run first —
                                         an earlier probe against a nonexistent remote
                                         reported "gone" for a file it never looked up)
internal/releaseassets/defects_test.go   6 defects, 8 funcs, 3 with explicit controls
  #924 ReplacementDeletesEveryDuplicate      #970 ExistingEmptyReleaseIsAFirstPublish
  #962 EmptyReleaseNeedsNoAuthorization      #971 ForeignAssetWarnsAndPasses
       + Control_NonEmptyStillRefuses             + Control_MissingExpectedStillDies
  #967 UnreadableBodyAndMissingID (2)        #972 AbsentReleaseAtVerifyStillDies
package total                            27 test funcs across 4 files

Mutation run — each mutant compiled before its verdict was read, and the tree was restored and re-verified green after each:

#962  ops.go     `if s.Count == 0`            -> `if false`        RED
#972  verify.go  `if !s.Exists`               -> `if false`        RED
#971  verify.go  `expected := len(Names)`     -> `... + 1`         RED
baseline before and after                                          ok

Three of six mutated rather than all six — the arms are demonstrably live and not inert, which is what the AC's "reddens without the fix" asks.

Closing — every AC verified against `origin/main` at `10ccb54c`, not against the PR description. - [x] the behaviour moves into `rt` as a verb, with the existing bats arms kept as the acceptance oracle until the Go tests match them - [x] the shell script is deleted, not left as a wrapper - [x] ~~`reusable-release.yml` calls the verb~~ — **RETIRED (wrong workflow named at filing):** `release-assets` is toolkit-self and appears in **no** reusable workflow. The caller is `goreleaser.yml` (`rt-pipeline release-assets prepare` at :197, `… verify` at :345). Recorded in `docs/architecture/contracts/cli-surface.md:79`, which states this as *checked rather than assumed*. No workflow keeps a bash path — the only surviving mentions of `release-assets.sh` are that prose row. - [x] every one of the six prior defects has a Go test that reddens without the fix **Verification** ``` scripts/release-assets.sh on main DELETED (positive+negative control run first — an earlier probe against a nonexistent remote reported "gone" for a file it never looked up) internal/releaseassets/defects_test.go 6 defects, 8 funcs, 3 with explicit controls #924 ReplacementDeletesEveryDuplicate #970 ExistingEmptyReleaseIsAFirstPublish #962 EmptyReleaseNeedsNoAuthorization #971 ForeignAssetWarnsAndPasses + Control_NonEmptyStillRefuses + Control_MissingExpectedStillDies #967 UnreadableBodyAndMissingID (2) #972 AbsentReleaseAtVerifyStillDies package total 27 test funcs across 4 files ``` **Mutation run** — each mutant compiled before its verdict was read, and the tree was restored and re-verified green after each: ``` #962 ops.go `if s.Count == 0` -> `if false` RED #972 verify.go `if !s.Exists` -> `if false` RED #971 verify.go `expected := len(Names)` -> `... + 1` RED baseline before and after ok ``` Three of six mutated rather than all six — the arms are demonstrably live and not inert, which is what the AC's *"reddens without the fix"* asks.
bosun closed this issue 2026-09-04 11:05:53 +02:00
Author
Owner

AC sweep, re-derived from origin/main rather than from the close. Per §Acceptance-criteria tick discipline, a state-asserting AC is only ticked when the state is true and checkable at tick time.

fork-pr-approval-notice.sh   ABSENT from main
release-assets.sh            ABSENT from main
cmd/rt/fork_pr_approval_notice.go   present
cmd/rt/release_assets.go            present
bash paths in .forgejo/ .gitea/ scripts/ referencing either script:  0

So "the script is deleted, not left as a wrapper" and "no workflow keeps a bash path to the same behaviour" are TRUE and checkable. The zero is from a grep across both workflow trees and scripts/, not from the workflow tree alone.

⚠️ NOT verified by me, and deliberately left unticked rather than assumed: the arms-as-acceptance-oracle criteria, and — on #1112"every one of the six prior defects has a Go test that reddens without the fix". That is a mutation claim, and reading a test is not running one. Ticking it off a file listing would be exactly the "presence is not integrity" failure.

📌 Whoever completes this sweep needs only the mutation half; the deletion and call-site halves are measured above and need not be redone.

**AC sweep, re-derived from `origin/main` rather than from the close.** Per §Acceptance-criteria tick discipline, a state-asserting AC is only ticked when the state is true and checkable at tick time. ``` fork-pr-approval-notice.sh ABSENT from main release-assets.sh ABSENT from main cmd/rt/fork_pr_approval_notice.go present cmd/rt/release_assets.go present bash paths in .forgejo/ .gitea/ scripts/ referencing either script: 0 ``` ✅ **So "the script is deleted, not left as a wrapper" and "no workflow keeps a bash path to the same behaviour" are TRUE and checkable.** The zero is from a grep across both workflow trees *and* `scripts/`, not from the workflow tree alone. ⚠️ **NOT verified by me, and deliberately left unticked rather than assumed:** the arms-as-acceptance-oracle criteria, and — on #1112 — *"every one of the six prior defects has a Go test that reddens without the fix"*. **That is a mutation claim, and reading a test is not running one.** Ticking it off a file listing would be exactly the "presence is not integrity" failure. 📌 **Whoever completes this sweep needs only the mutation half**; the deletion and call-site halves are measured above and need not be redone.
Author
Owner

AC sweep — the four ACs were true and un-ticked. Ticked now, with what was read for each, against forgejo/main @ fb1bd9c2.

bats arms until Go tests match   internal/releaseassets/defects_test.go maps all SIX
                                 defects to 8 test funcs (2 are controls); the bats arms
                                 are gone, which is that AC's intended terminal state
script deleted, not a wrapper    scripts/release-assets.sh absent from the tree
six defects have a Go test       #924 #962 #967 #970 #971 #972 -> named funcs, mapping
                                 written INTO the file

The third AC is ticked with a correction, not bare: it named the wrong workflow. reusable-release.yml does not reference release-assets and never did — goreleaser.yml calls release-assets prepare (:212) and verify (:348). docs/architecture/contracts/cli-surface.md already records this as "toolkit-self — appears in NO reusable workflow, checked rather than assumed." The operative half — no workflow keeps a bash path — holds: the only surviving release-assets.sh strings are the port's own documentation.

🔑 That file's header is worth reading by anyone auditing a tracker-number AC: two of the six (#970, #972) appear in no bats arm title, so grepping tracker numbers would have returned a false negative on work that was done. The implementer wrote the mapping down for exactly that reason.

Swept by @bosun, 2026-09-05.

AC sweep — the four ACs were true and un-ticked. Ticked now, with what was read for each, against `forgejo/main` @ `fb1bd9c2`. ``` bats arms until Go tests match internal/releaseassets/defects_test.go maps all SIX defects to 8 test funcs (2 are controls); the bats arms are gone, which is that AC's intended terminal state script deleted, not a wrapper scripts/release-assets.sh absent from the tree six defects have a Go test #924 #962 #967 #970 #971 #972 -> named funcs, mapping written INTO the file ``` **The third AC is ticked with a correction, not bare: it named the wrong workflow.** `reusable-release.yml` does not reference `release-assets` and never did — `goreleaser.yml` calls `release-assets prepare` (:212) and `verify` (:348). `docs/architecture/contracts/cli-surface.md` already records this as *"toolkit-self — appears in NO reusable workflow, **checked rather than assumed**."* The operative half — no workflow keeps a bash path — holds: the only surviving `release-assets.sh` strings are the port's own documentation. 🔑 That file's header is worth reading by anyone auditing a tracker-number AC: two of the six (#970, #972) **appear in no bats arm title**, so grepping tracker numbers would have returned a false negative on work that was done. The implementer wrote the mapping down for exactly that reason. Swept by @bosun, 2026-09-05.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1112
No description provided.