design(v2.0.0): consider Go rewrite for cross-runner + single-binary distribution + refactor-safety #367

Closed
opened 2026-07-04 08:53:27 +02:00 by bosun · 6 comments
Owner

Motivation

External cold-read round 2 (2026-07-03) verdict language flagged shell-substrate concerns:

  • "It is a large Bash state machine"
  • "Some behavior is encoded in comments, conventions, and environment variables rather than clean interfaces"

The reviewer's evaluation of the shell code itself was positive (pervasive set -euo pipefail, careful quoting, jq for JSON, dry-run paths, 551 bats tests, atomic operations). Complaint was about workflow-shape (YAML doing a lot, state in env-vars, comment archaeology), NOT shell-vs-alternative.

Deferred from v1.0.0 discussion 2026-07-04 per operator ratify: technology shift addresses only 3 of the round-2 findings (runner assumptions #357 partial + tag creation opacity #359 minor + testability/refactoring safety) and does NOT address the other 4 (docs-shape not implementation-shape). Not blocker-class for v1.0.0.

v2.0.0 target rationale

If post-v1.0.0 adoption signal shows genuine friction from:

  • Runner assumptions (apt-get / sudo / yq variant) — single Go binary eliminates all of these
  • Non-Debian/Ubuntu runner support demand — single binary is cross-platform
  • Distribution complexity (adopters need bats + jq + yq + curl on runner) — single binary eliminates
  • Refactoring safety at growing scale — type-checked Go modules > shell state-machine
  • Community contribution barrier from shell (fewer Go contributors than shell for CI tooling — this cuts BOTH ways)

...then a Go rewrite becomes a coherent v2.0.0 initiative.

Language choice rationale (Go over Rust)

Go (recommended if shift happens):

  • Matches internal-project consistency (tmux-tell, Binnacle)
  • Excellent CI-tooling ecosystem (goreleaser, kong, cobra, viper)
  • Single-binary distribution
  • Faster compile cycles than Rust
  • Lower learning curve for shell-familiar developers
  • Standard library has http/yaml/json without external deps
  • Better JSON handling than shell + jq
  • Community around CLI + DevOps tools is genuinely large

Rust (not recommended for this project):

  • Correctness guarantees strong but overkill for release automation
  • Slower compile cycles
  • Steeper learning curve
  • Smaller CI-tooling ecosystem than Go
  • Better fit if concern were performance or memory safety on privileged code, neither applies here

Stay in shell (current default):

  • Dogfooding history + 551 tests is substantial validation
  • "Shell tools any admin can patch" is a positive trust signal in the Forgejo community
  • Iterating on docs + workflow YAML is faster than iterating on a compiled substrate

Migration approach (if v2.0.0 fires)

Not big-bang rewrite. Module-by-module with migration tests validating byte-identical behavior on edge cases:

  1. Port scripts/lib/semver.shsemver.go with bats equivalence tests
  2. Port scripts/lib/conventional-commits.shcc.go
  3. Port scripts/lib/config.shconfig.go
  4. Port scripts/lib/forgejo-api.shforgejo/api.go (folds #334 API resilience naturally)
  5. Port scripts/lib/fragments.sh + changelog.shchangelog.go
  6. Port scripts/release-decide.sh orchestrator → cmd/rt-decide
  7. Port scripts/release-prep.sh orchestrator → cmd/rt-prep
  8. Port scripts/draft-release.sh orchestrator → cmd/rt-release
  9. Update reusable workflows to invoke rt binary instead of shell scripts
  10. Deprecate shell over multiple minor releases (v1.x)
  11. Cut v2.0.0 when shell substrate fully replaced

Decision triggers

Fire this work when ANY of:

  • Multiple adopter feedback points name runner-assumption friction as blocker
  • Non-Debian/Ubuntu runner support becomes explicit demand
  • Community contributor volunteers Go expertise + owns the port
  • Testability degradation from added shell substrate becomes maintainer friction
  • Cross-platform CI (Windows/macOS runners) becomes adopter demand

Do NOT fire on:

  • Cosmetic external-review complaints about "large Bash state machine" without concrete adoption blocker
  • Aspirational technology-preference without evidence of adoption friction
  • Round 2 external cold-read: BookStack release-toolkit book page 210
  • Design discussion analysis: this session's Bosun-operator exchange 2026-07-04
  • v1.1.0 tracker cluster: #334 API resilience (naturally folds into forgejo/api.go if v2.0.0 fires), #336 immutable pinning, #337 ecosystem breadth (could benefit from Go strategy-pattern abstraction)

v1.0.0 relationship

Explicitly OUT of scope for v1.0.0. Documented + parked; ratified by operator 2026-07-04.

Anchor

External ChatGPT cold-read round 2 verdict framing + Bosun-operator design-discussion 2026-07-04 (BookStack release-toolkit book pages 210 + 212).

## Motivation External cold-read round 2 (2026-07-03) verdict language flagged shell-substrate concerns: - "It is a large Bash state machine" - "Some behavior is encoded in comments, conventions, and environment variables rather than clean interfaces" The reviewer's evaluation of the shell code itself was positive (pervasive `set -euo pipefail`, careful quoting, jq for JSON, dry-run paths, 551 bats tests, atomic operations). Complaint was about workflow-shape (YAML doing a lot, state in env-vars, comment archaeology), NOT shell-vs-alternative. Deferred from v1.0.0 discussion 2026-07-04 per operator ratify: technology shift addresses only 3 of the round-2 findings (runner assumptions #357 partial + tag creation opacity #359 minor + testability/refactoring safety) and does NOT address the other 4 (docs-shape not implementation-shape). Not blocker-class for v1.0.0. ## v2.0.0 target rationale If post-v1.0.0 adoption signal shows genuine friction from: - Runner assumptions (apt-get / sudo / yq variant) — single Go binary eliminates all of these - Non-Debian/Ubuntu runner support demand — single binary is cross-platform - Distribution complexity (adopters need bats + jq + yq + curl on runner) — single binary eliminates - Refactoring safety at growing scale — type-checked Go modules > shell state-machine - Community contribution barrier from shell (fewer Go contributors than shell for CI tooling — this cuts BOTH ways) ...then a Go rewrite becomes a coherent v2.0.0 initiative. ## Language choice rationale (Go over Rust) **Go** (recommended if shift happens): - Matches internal-project consistency (tmux-tell, Binnacle) - Excellent CI-tooling ecosystem (goreleaser, kong, cobra, viper) - Single-binary distribution - Faster compile cycles than Rust - Lower learning curve for shell-familiar developers - Standard library has http/yaml/json without external deps - Better JSON handling than shell + jq - Community around CLI + DevOps tools is genuinely large **Rust** (not recommended for this project): - Correctness guarantees strong but overkill for release automation - Slower compile cycles - Steeper learning curve - Smaller CI-tooling ecosystem than Go - Better fit if concern were *performance* or *memory safety on privileged code*, neither applies here **Stay in shell** (current default): - Dogfooding history + 551 tests is substantial validation - "Shell tools any admin can patch" is a positive trust signal in the Forgejo community - Iterating on docs + workflow YAML is faster than iterating on a compiled substrate ## Migration approach (if v2.0.0 fires) **Not big-bang rewrite.** Module-by-module with migration tests validating byte-identical behavior on edge cases: 1. Port `scripts/lib/semver.sh` → `semver.go` with bats equivalence tests 2. Port `scripts/lib/conventional-commits.sh` → `cc.go` 3. Port `scripts/lib/config.sh` → `config.go` 4. Port `scripts/lib/forgejo-api.sh` → `forgejo/api.go` (folds #334 API resilience naturally) 5. Port `scripts/lib/fragments.sh` + `changelog.sh` → `changelog.go` 6. Port `scripts/release-decide.sh` orchestrator → `cmd/rt-decide` 7. Port `scripts/release-prep.sh` orchestrator → `cmd/rt-prep` 8. Port `scripts/draft-release.sh` orchestrator → `cmd/rt-release` 9. Update reusable workflows to invoke `rt` binary instead of shell scripts 10. Deprecate shell over multiple minor releases (v1.x) 11. Cut v2.0.0 when shell substrate fully replaced ## Decision triggers Fire this work when ANY of: - Multiple adopter feedback points name runner-assumption friction as blocker - Non-Debian/Ubuntu runner support becomes explicit demand - Community contributor volunteers Go expertise + owns the port - Testability degradation from added shell substrate becomes maintainer friction - Cross-platform CI (Windows/macOS runners) becomes adopter demand Do NOT fire on: - Cosmetic external-review complaints about "large Bash state machine" without concrete adoption blocker - Aspirational technology-preference without evidence of adoption friction ## Related - Round 2 external cold-read: BookStack release-toolkit book page 210 - Design discussion analysis: this session's Bosun-operator exchange 2026-07-04 - v1.1.0 tracker cluster: #334 API resilience (naturally folds into forgejo/api.go if v2.0.0 fires), #336 immutable pinning, #337 ecosystem breadth (could benefit from Go strategy-pattern abstraction) ## v1.0.0 relationship Explicitly OUT of scope for v1.0.0. Documented + parked; ratified by operator 2026-07-04. ## Anchor External ChatGPT cold-read round 2 verdict framing + Bosun-operator design-discussion 2026-07-04 (BookStack release-toolkit book pages 210 + 212).
Author
Owner

Analysis dispatch — 2026-07-07: branch created, Engineer engaged

Operator directive 2026-07-07: branch out the Go investigation before v1.0.0 cut. Non-blocking; parallel to main-side v1.0.0 approach.

Substrate mutations

  • Branch created: v2/next @ 7fa5ce67 (from current main HEAD post-v0.32.0-cut bookkeeping)
  • Engineer chamber dispatched with analysis scope

Analysis scope

Doc-only at this stage (no Go code yet). Concrete artifacts targeted:

  1. ADR-style architecture document — package structure, config surface, adopter API, runner-integration model, cross-runner story (single-binary distribution)
  2. Effort estimate broken down by phase (parser/lib rewrites, workflow-equivalent surface, adopter migration)
  3. Compatibility/migration story — v1.x ↔ v2.0.0 (drop-in? distinct binary? transition period?)
  4. Risk register — what's uncertain, where blast-radius lives

Location: docs/adr/00XX-go-port-architecture.md on v2/next branch. Pure docs; no Go compilation infrastructure yet.

Roles

  • Engineer: primary analysis + ADR authorship
  • QM: substrate-oracle for current release-toolkit-substrate context (what the bash implementation does that the rewrite must preserve or intentionally break)
  • Bosun: doc-completion review + operator-decision-surface preparation
  • Surveyor: review lane fires when concrete architecture exists to verify against constraints

Tracker home

This tracker (#367) is the analysis tracker. Analysis output = ADR on v2/next branch + summary comment here. Decision outcomes:

  • "Yes, commit to Go rewrite" — spawn new implementation tracker from #367's outcome
  • "Not now" or "different approach"#367 closes with the analysis as substrate-of-record

Timing

Doesn't gate v1.0.0. Multi-week runway. v1.0.0 approach continues on main via #338 memorial-chore fold; analysis proceeds on v2/next in parallel. When v1.0.0 tags, v2/next either continues indefinitely as exploration, converges to implementation, or closes with a "not now" outcome — each substrate-honest.

Anchor: 2026-07-07 post-fold-3-close + #448 AC verification.

## Analysis dispatch — 2026-07-07: branch created, Engineer engaged **Operator directive 2026-07-07**: branch out the Go investigation before v1.0.0 cut. Non-blocking; parallel to main-side v1.0.0 approach. ### Substrate mutations - **Branch created**: `v2/next` @ `7fa5ce67` (from current `main` HEAD post-v0.32.0-cut bookkeeping) - **Engineer chamber dispatched** with analysis scope ### Analysis scope **Doc-only** at this stage (no Go code yet). Concrete artifacts targeted: 1. **ADR-style architecture document** — package structure, config surface, adopter API, runner-integration model, cross-runner story (single-binary distribution) 2. **Effort estimate** broken down by phase (parser/lib rewrites, workflow-equivalent surface, adopter migration) 3. **Compatibility/migration story** — v1.x ↔ v2.0.0 (drop-in? distinct binary? transition period?) 4. **Risk register** — what's uncertain, where blast-radius lives Location: `docs/adr/00XX-go-port-architecture.md` on `v2/next` branch. Pure docs; no Go compilation infrastructure yet. ### Roles - **Engineer**: primary analysis + ADR authorship - **QM**: substrate-oracle for current release-toolkit-substrate context (what the bash implementation does that the rewrite must preserve or intentionally break) - **Bosun**: doc-completion review + operator-decision-surface preparation - **Surveyor**: review lane fires when concrete architecture exists to verify against constraints ### Tracker home This tracker (**#367**) is the analysis tracker. Analysis output = ADR on `v2/next` branch + summary comment here. Decision outcomes: - **"Yes, commit to Go rewrite"** — spawn new implementation tracker from #367's outcome - **"Not now" or "different approach"** — #367 closes with the analysis as substrate-of-record ### Timing Doesn't gate v1.0.0. Multi-week runway. `v1.0.0` approach continues on `main` via `#338` memorial-chore fold; analysis proceeds on `v2/next` in parallel. When `v1.0.0` tags, `v2/next` either continues indefinitely as exploration, converges to implementation, or closes with a "not now" outcome — each substrate-honest. Anchor: 2026-07-07 post-fold-3-close + `#448` AC verification.
Owner

Analysis landed — ADR-0008 in PR #473 (base v2/next)

The v2.0.0 Go-port analysis is complete: docs/adr/0008-go-port-architecture.md. Doc-only; decision surface for Go / Defer / Decline. Does not gate v1.0.0.

Load-bearing finding: the adopter API boundary is the workflow YAML, not the scripts — so the port is adopter-transparent (consumers pin @v2.0.0 with zero code change) but NOT implementation-cheap (all ~5341 LOC still ported + byte-validated). Two independent axes; the favorable one is migration-safety, the effort axis is unchanged.

Effort: ~12–21 engineer-weeks (order-of-magnitude), dominated by the orchestrators (Phase 6) and risk-gated by the byte-exact self-bootstrap/bake machinery (Phase 5, verified against build_bake.sh:84-164). Recommended sequencing if ratified go: bake-spike first, not scaffold — retire the highest-risk unknown before full commitment.

Primary open operator decision: fork 4a — binary-distribution model (download-asset / vendored / build-per-run / composite-action), lean D→A. This one shapes the whole distribution story.

Recommendation (§8): defer the build, land the analysis as substrate-of-record, pre-commit to the bake-spike-first sequencing — the tracker's decision triggers aren't met yet. Ready to execute if the operator judges refactor-safety alone sufficient.

Bosun reviews the analysis (design + effort + decision-surface); Surveyor's lane fires later if concrete architecture gets built. #367 stays open pending the ratification cycle.

## Analysis landed — ADR-0008 in PR #473 (base `v2/next`) The v2.0.0 Go-port analysis is complete: **[docs/adr/0008-go-port-architecture.md](https://git.frankenbit.de/frankenbit/release-toolkit/pulls/473)**. Doc-only; decision surface for **Go / Defer / Decline**. Does not gate v1.0.0. **Load-bearing finding**: the adopter API boundary is the *workflow YAML*, not the scripts — so the port is **adopter-transparent** (consumers pin `@v2.0.0` with zero code change) but **NOT implementation-cheap** (all ~5341 LOC still ported + byte-validated). Two independent axes; the favorable one is migration-safety, the effort axis is unchanged. **Effort**: ~12–21 engineer-weeks (order-of-magnitude), dominated by the orchestrators (Phase 6) and risk-gated by the byte-exact self-bootstrap/bake machinery (Phase 5, verified against `build_bake.sh:84-164`). Recommended sequencing if ratified *go*: **bake-spike first**, not scaffold — retire the highest-risk unknown before full commitment. **Primary open operator decision**: fork 4a — binary-distribution model (download-asset / vendored / build-per-run / composite-action), lean **D→A**. This one shapes the whole distribution story. **Recommendation (§8)**: **defer the build, land the analysis as substrate-of-record, pre-commit to the bake-spike-first sequencing** — the tracker's decision triggers aren't met yet. Ready to execute if the operator judges refactor-safety alone sufficient. Bosun reviews the analysis (design + effort + decision-surface); Surveyor's lane fires later if concrete architecture gets built. #367 stays open pending the ratification cycle.
Author
Owner

Operator ratification (2026-07-08, in-chat) — sequencing decided

Ratification: not the ADR-8 "defer" recommendation; not "fire-full-port" either — Phase-5 bake spike as the next concrete engagement, sequenced post-ember#4 to honor the already-scoped Ember bounded eval window.

Ratified path

  1. Engineer's next assigned engagement: ember#4 POC (per §14 of Ember DESIGN.md — bounded eval, 2-3 days demoable POC + 2 weeks real use, timebox-aggressively)
  2. Post-ember#4-close: Phase-5 bake spike per ADR-0008 §5+§6 recommendation ("Phase-5-first, not Phase-0 scaffold — doing scaffold-first would defer the one phase that could invalidate the whole estimate")
  3. Post-spike outcome: fire-full-port OR revert-to-defer, based on empirical Phase-5 result

Rationale for sequencing (Ember-first)

  • Ember has an explicit bounded eval window; deferring an already-scoped bounded eval to spawn a new bounded work-item without closing the existing one grows work-in-flight rather than driving it to close
  • ember#4 blocks nothing structurally — isolated POC on its own tracker
  • No external Go-port urgency trigger firing right now (adoption friction, non-Debian runner demand, cross-platform CI demand all still un-met per ADR-0008 §8); couple-weeks delay doesn't materially change the go/no-go window
  • Parallelizing would need a second implementer chamber for Go work; none queued for that lane

Rationale for spike-over-deeper-analysis

  • ADR-0008 is at diminishing-returns for paper analysis (Surveyor cross-checked the LOC baseline + validator subsystem correction is landed)
  • Remaining uncertainty is empirical: every R1 claim about byte-exact bake machinery (sed absence, command-substitution newline concerns, yq variance evaporating) is code-verifiable, not paper-refinable
  • Phase-5 wide bar (1.5-3.5 EW) is the single biggest aggregate-estimate uncertainty; bounded spike is the economical retirement
  • Spike-fail is also useful signal — informs the fire-or-defer call substantively

Substrate not-yet-done

  • Spike tracker filing: deferred until ember#4 completes + Engineer engages (avoids weeks-away board pollution)
  • ADR-0008 status flip: stays proposed for now; operator explicitly deferred the "defer" state — the spike-then-decide path leaves ADR at proposed until either spike-succeeds → flip to accepted (go) OR spike-fails → flip to accepted (defer)
  • #367 priority label: priority/low remains stale for a substantive substrate decision; leaving as-is since active work is weeks away

Anchor

Substrate-of-record for the conversation 2026-07-08 (post-alcatraz-infra drive-to-close). Engineer notified for post-ember#4 metabolism awareness.

## Operator ratification (2026-07-08, in-chat) — sequencing decided **Ratification**: not the ADR-8 "defer" recommendation; not "fire-full-port" either — **Phase-5 bake spike** as the next concrete engagement, sequenced **post-ember#4** to honor the already-scoped Ember bounded eval window. ### Ratified path 1. **Engineer's next assigned engagement**: ember#4 POC (per §14 of Ember DESIGN.md — bounded eval, 2-3 days demoable POC + 2 weeks real use, timebox-aggressively) 2. **Post-ember#4-close**: Phase-5 bake spike per ADR-0008 §5+§6 recommendation ("Phase-5-first, not Phase-0 scaffold — doing scaffold-first would defer the one phase that could invalidate the whole estimate") 3. **Post-spike outcome**: fire-full-port OR revert-to-defer, based on empirical Phase-5 result ### Rationale for sequencing (Ember-first) - Ember has an explicit bounded eval window; deferring an already-scoped bounded eval to spawn a new bounded work-item without closing the existing one grows work-in-flight rather than driving it to close - ember#4 blocks nothing structurally — isolated POC on its own tracker - No external Go-port urgency trigger firing right now (adoption friction, non-Debian runner demand, cross-platform CI demand all still un-met per ADR-0008 §8); couple-weeks delay doesn't materially change the go/no-go window - Parallelizing would need a second implementer chamber for Go work; none queued for that lane ### Rationale for spike-over-deeper-analysis - ADR-0008 is at diminishing-returns for paper analysis (Surveyor cross-checked the LOC baseline + validator subsystem correction is landed) - Remaining uncertainty is empirical: every R1 claim about byte-exact bake machinery (sed absence, command-substitution newline concerns, yq variance evaporating) is code-verifiable, not paper-refinable - Phase-5 wide bar (1.5-3.5 EW) is the single biggest aggregate-estimate uncertainty; bounded spike is the economical retirement - Spike-fail is also useful signal — informs the fire-or-defer call substantively ### Substrate not-yet-done - **Spike tracker filing**: deferred until ember#4 completes + Engineer engages (avoids weeks-away board pollution) - **ADR-0008 status flip**: stays `proposed` for now; operator explicitly deferred the "defer" state — the spike-then-decide path leaves ADR at proposed until either spike-succeeds → flip to `accepted (go)` OR spike-fails → flip to `accepted (defer)` - **#367 priority label**: `priority/low` remains stale for a substantive substrate decision; leaving as-is since active work is weeks away ### Anchor Substrate-of-record for the conversation 2026-07-08 (post-alcatraz-infra drive-to-close). Engineer notified for post-ember#4 metabolism awareness.
Owner

Phase-5 bake spike — empirical retirement of §6 R1 (byte-exact self-bootstrap machinery)

Executing the sequencing this ADR pre-committed to (§5, §8: "Phase-5 spike first … so that if a decision trigger fires, the highest-risk unknown is retired before the full commitment"). This cashes that insurance.

Question scoped: is R1 — the byte-exact create_detached_bake_commit / bake_toolkit_ref machinery (scripts/lib/build_bake.sh:84-164) — cleanly Go-portable, or does it reveal substrate-fragility that would gate the port?

Answer: cleanly portable. R1 retired, empirically.

Method — cryptographic equality, not diff-inspection

The #172 bats oracle (tests/build_bake.bats:291-371, 9 tests) pins the bake's invariants as tree/blob-content properties (marker-only diff, +8-byte trailing-newline preservation, parent linkage, HEAD/worktree non-mutation, partial-tree safety) — none pin a commit SHA. Git trees are content-addressed and timestamp-free (unlike commits), so the sharpest possible test is: does a Go port produce the identical tree SHA as the bash oracle for the same base? A matching tree SHA proves the rewritten blobs are byte-identical and arranged identically — no interpretation needed.

The spike ports the byte-exact kernel to pure Go — the marker rewrite is regexp.ReplaceAll over []byte ('[^'\n]*' # release-toolkit-build-ref'<ref>' …), reading bytes and returning bytes. Tree assembly shells to git plumbing (the §6 R2 "shell to plumbing, not porcelain" lean). Blob byte-exactness is additionally cross-checked in-process against a pure-Go git-blob-SHA (sha1("blob <len>\0"+content)) — zero git dependency for the byte-exact claim itself.

Result — 10/10 tree-SHA equality

Case bash tree == go tree
synthetic fixture, ref ∈ {v0.15.1-rc.1, v0.13.0, main, feat/some-branch} ×4
partial-tree safety (one reusable absent from base → skipped)
real production reusables (actual 5-file create_detached_bake_commit default set), ref ∈ {v0.16.0-rc.1, main} ×2
allowlist fail-loud parity (|MALICIOUS, ;rm, extra) — both reject ×3

Highest-fidelity case (real production blobs, not synthetic) is byte-identical.

Mutation-verification (loop closed)

Re-introducing the exact R1 failure mode — bash's $(git show | sed) command-substitution strips the trailing newline — via a spike mutation hook: 7/7 tree-SHA cases go red (bash ≠ go); only the allowlist-rejection cases stay green (they never reach the blob). Revert → 10/10 green. The equality is meaningful, not vacuous: it catches precisely the byte-exactness regression R1 names.

Two-axis framing (§6 R1) — now empirical, not read-verified

  • Implementation-fragility axis — evaporates (confirmed). sed gone (→ bytes/regexp), command-sub newline-strip gone (→ read bytes, return bytes), python-vs-yq variance N/A. These were shell artifacts; the tree-SHA equality proves the concern with a hash, and the mutation proves the test would catch a regression.
  • Design-invariant axis — reproduces mechanically (confirmed). Detached-off-main parent linkage, marker-only diff, partial-tree skip, HEAD/worktree non-mutation, ref-shape allowlist — all reproduce in ~30 lines of git-plumbing orchestration. Substrate architecture, faithfully portable.

Extension (disclose-and-extend): the ref-shape allowlist's role shrinks under the port. In bash it is security-critical — defense-in-depth against sed-delimiter / shell-meta injection into a live sed/command sink (#180). In Go there is no sed and no shell, so the same allowlist degrades to ordinary input validation — the injection sink itself is gone. The port doesn't just make R1 easier; it removes a security surface.

Recommendation

GO on Phase-5 portability — R1, the single highest-risk unknown, is retired. This is exactly the collapse §5 predicted: "If the byte-equivalence contract can be reproduced cleanly in Go, the highest-risk unknown collapses and the rest is well-understood translation."

This does NOT overturn §8's DEFER-the-build recommendation. §8 is trigger-gated (adoption-friction / non-Debian-runner / cross-platform-CI evidence — still unmet), not risk-gated. The spike changes the risk axis, not the trigger axis. Net: the P5 estimate bars collapse from "high, 1.5–3.5 EW risk-uncertainty" to known mechanical translation; the build stays deferred until a trigger fires; the sequencing insurance is now cashed rather than merely promised.

Scope honesty — what this spike does NOT prove

  • repin.sh orchestration (Forgejo API, worktree push, PR-open) not ported — but that is R2/clean-seam translation, explicitly not the R1 risk, and already lower-rated.
  • go-git vs plumbing-shellout for in-process object construction not evaluated — a real open sub-fork for the full port (single-binary purity vs. git-subprocess dependency), but it does not gate GO/DEFER: both reproduce byte-exactness; plumbing-shellout is proven here, go-git is a known-viable alternative to spike at implementation time.
  • check-self-bootstrap.sh read-side (HEAD-vs-pinned NORM_SED blob-compare) not ported — but it is strictly simpler than the write-side bake proven here (read + normalize + compare, consuming the same marker-shape constant), so it inherits the retirement rather than adding risk.

Spike source (reproducible): /srv/claude/engineer/phase5-bake-spike/ (bake.go + harness.sh; bash harness.sh rebuilds + runs the full matrix).

Shipwright adversarial verification in flight — independent reproduction of the tree-SHA equality + gap-probe; confirmation to follow.

## Phase-5 bake spike — empirical retirement of §6 R1 (byte-exact self-bootstrap machinery) Executing the sequencing this ADR pre-committed to (§5, §8: *"Phase-5 spike first … so that if a decision trigger fires, the highest-risk unknown is retired before the full commitment"*). This cashes that insurance. **Question scoped:** is R1 — the byte-exact `create_detached_bake_commit` / `bake_toolkit_ref` machinery (`scripts/lib/build_bake.sh:84-164`) — *cleanly* Go-portable, or does it reveal substrate-fragility that would gate the port? **Answer: cleanly portable. R1 retired, empirically.** ### Method — cryptographic equality, not diff-inspection The `#172` bats oracle (`tests/build_bake.bats:291-371`, 9 tests) pins the bake's invariants as **tree/blob-content** properties (marker-only diff, +8-byte trailing-newline preservation, parent linkage, HEAD/worktree non-mutation, partial-tree safety) — **none pin a commit SHA**. Git *trees* are content-addressed and timestamp-free (unlike commits), so the sharpest possible test is: **does a Go port produce the identical tree SHA as the bash oracle for the same base?** A matching tree SHA proves the rewritten blobs are byte-identical *and* arranged identically — no interpretation needed. The spike ports the byte-exact kernel to **pure Go** — the marker rewrite is `regexp.ReplaceAll` over `[]byte` (`'[^'\n]*' # release-toolkit-build-ref` → `'<ref>' …`), reading bytes and returning bytes. Tree assembly shells to git plumbing (the §6 R2 "shell to plumbing, not porcelain" lean). Blob byte-exactness is additionally cross-checked in-process against a pure-Go git-blob-SHA (`sha1("blob <len>\0"+content)`) — **zero git dependency** for the byte-exact claim itself. ### Result — 10/10 tree-SHA equality | Case | bash tree == go tree | |------|:--:| | synthetic fixture, ref ∈ {`v0.15.1-rc.1`, `v0.13.0`, `main`, `feat/some-branch`} | ✅ ×4 | | partial-tree safety (one reusable absent from base → skipped) | ✅ | | **real production reusables** (actual 5-file `create_detached_bake_commit` default set), ref ∈ {`v0.16.0-rc.1`, `main`} | ✅ ×2 | | allowlist fail-loud parity (`\|MALICIOUS`, `;rm`, ` extra`) — both reject | ✅ ×3 | Highest-fidelity case (real production blobs, not synthetic) is byte-identical. ### Mutation-verification (loop closed) Re-introducing the *exact* R1 failure mode — bash's `$(git show | sed)` command-substitution **strips the trailing newline** — via a spike mutation hook: **7/7 tree-SHA cases go red** (bash ≠ go); only the allowlist-rejection cases stay green (they never reach the blob). Revert → 10/10 green. The equality is meaningful, not vacuous: it catches precisely the byte-exactness regression R1 names. ### Two-axis framing (§6 R1) — now empirical, not read-verified - **Implementation-fragility axis — evaporates (confirmed).** `sed` gone (→ `bytes`/`regexp`), command-sub newline-strip gone (→ read bytes, return bytes), python-vs-yq variance N/A. These were shell artifacts; the tree-SHA equality proves the concern with a hash, and the mutation proves the test would catch a regression. - **Design-invariant axis — reproduces mechanically (confirmed).** Detached-off-main parent linkage, marker-only diff, partial-tree skip, HEAD/worktree non-mutation, ref-shape allowlist — all reproduce in ~30 lines of git-plumbing orchestration. Substrate architecture, faithfully portable. **Extension (disclose-and-extend):** the ref-shape allowlist's *role* shrinks under the port. In bash it is **security-critical** — defense-in-depth against sed-delimiter / shell-meta injection into a live `sed`/command sink (`#180`). In Go there is no sed and no shell, so the same allowlist degrades to **ordinary input validation** — the injection *sink itself is gone*. The port doesn't just make R1 easier; it removes a security surface. ### Recommendation **GO on Phase-5 portability — R1, the single highest-risk unknown, is retired.** This is exactly the collapse §5 predicted: *"If the byte-equivalence contract can be reproduced cleanly in Go, the highest-risk unknown collapses and the rest is well-understood translation."* **This does NOT overturn §8's DEFER-the-build recommendation.** §8 is **trigger-gated** (adoption-friction / non-Debian-runner / cross-platform-CI evidence — still unmet), not **risk-gated**. The spike changes the risk axis, not the trigger axis. Net: the P5 estimate bars collapse from "high, 1.5–3.5 EW *risk*-uncertainty" to *known mechanical translation*; the build stays deferred until a trigger fires; the sequencing insurance is now cashed rather than merely promised. ### Scope honesty — what this spike does NOT prove - **`repin.sh` orchestration** (Forgejo API, worktree push, PR-open) not ported — but that is R2/clean-seam translation, explicitly *not* the R1 risk, and already lower-rated. - **go-git vs plumbing-shellout** for in-process object construction not evaluated — a real open sub-fork for the *full* port (single-binary purity vs. git-subprocess dependency), but it does **not gate GO/DEFER**: both reproduce byte-exactness; plumbing-shellout is *proven here*, go-git is a known-viable alternative to spike at implementation time. - **`check-self-bootstrap.sh` read-side** (HEAD-vs-pinned `NORM_SED` blob-compare) not ported — but it is strictly *simpler* than the write-side bake proven here (read + normalize + compare, consuming the same marker-shape constant), so it inherits the retirement rather than adding risk. Spike source (reproducible): `/srv/claude/engineer/phase5-bake-spike/` (`bake.go` + `harness.sh`; `bash harness.sh` rebuilds + runs the full matrix). **Shipwright adversarial verification in flight** — independent reproduction of the tree-SHA equality + gap-probe; confirmation to follow.
Owner

Phase-5 bake spike — adversarial verification CLOSED (Shipwright), recommendation strengthened

Shipwright independently reproduced the spike and audited all three scope claims. Verdict: GO holds, corroborated. Two items surfaced — both now closed empirically rather than merely documented (spike re-run: 15/15).

Independent reproduction (Shipwright)

  • bash harness.sh → 10/10 tree-SHA equality reproduced; SPIKE_MUTATE=strip-newline → tree-cases red, confirmed non-vacuous.
  • Tree-SHA-is-the-right-invariant, confirmed at source: no build_bake.bats #172 test pins a commit-object SHA/bytes; a commit SHA embeds author/committer timestamps so it cannot be a reproducibility invariant. The commit-SHA-as-timestamp-noise dismissal is correct.

Item 1 — sed first-per-line divergence → CLOSED

Shipwright's independent 11-case edge matrix found 1 divergence: two markers on one physical line. Bash sed (no g flag) rewrites first-per-line; the spike's original regexp.ReplaceAll rewrote all. Production-unreachable (all 5 real reusables carry exactly one marker; check-self-bootstrap's read-side NORM_SED uses g and tolerates either way), but a real faithfulness gap.

Fixed to match the oracle exactly (rewriteMarker now splits on \n, replaces first-per-line, rejoins — bytes.Split/Join is a perfect inverse so trailing-newline byte-exactness is untouched). New harness case two-markers-one-line → bash==Go ; real-repo tree SHAs unchanged (no regression). This is the faithful-reproduce call (match the bash source-of-record incl. its quirk); the alternative (replace-all + a Phase-8 assert-never-two-markers-per-line) is named but not taken.

All other edge cases Shipwright probed already agreed: marker@EOF-without-trailing-newline , CRLF , two-markers-two-lines , empty blob , non-ASCII/UTF-8 body , valid-adversarial refs (.hidden / /// / -rc / a..b / _) .

Item 2 — commit design-invariants → now MEASURED (was reproduced-by-construction)

Shipwright's honesty note: the tree-only spike retired the byte-exactness/fragility axis empirically, but the commit-wrapper design-invariants (bats #172:301-351: parent==base, HEAD-untouched, throwaway-index/worktree-clean) were reproduced by construction (commit-tree -p base), not independently exercised.

Closed: added SPIKE_EMIT=commit (builds the detached commit off base) + harness assertions measuring against the Go-built commit: parent == base , HEAD unmoved , working tree clean , rc ref baked at the commit .

Audited dismissals — both HOLD (Shipwright)

  • check-self-bootstrap read-side (NORM_SED): both HEAD and pinned sides go through the identical git show | sed pipeline, so the command-sub newline-strip that made the write side fragile hits both sides symmetrically and cancels — no byte-preservation-against-external-reference requirement, no trap. Inherits retirement a fortiori (strict subset of the write-side concern).
  • go-git vs plumbing-shellout: non-gating conditionally — plumbing-shellout is a proven floor (this spike), and go-git is a later 4a single-binary optimization carrying its own R2-class edge risk (filemode/empty-tree/index), not a portability gate. The condition: this holds because shelling to plumbing is ADR-R2-endorsed. If a future decision mandated zero-git-subprocess, the sub-fork re-enters as real risk — nothing mandates that today.

Honest scope of "R1 retired" (final, precise)

  • Byte-exactness / implementation-fragility axisretired empirically (tree-SHA equality incl. real reusables, mutation-verified, sed-faithful).
  • Design-invariant axis, single-actor (repin create_detached_bake_commit leg)now measured (parent/HEAD/worktree).
  • Three-actor full self-bootstrap round-tripnot exercised: the other two legs (bake_toolkit_ref at cut-prep via release-prep.sh; the post-cut 'main' reset via reusable-release.yml) reuse the same rewriteMarker kernel proven here, so they inherit byte-exactness — but the full bake→cut→reset→re-pin round-trip is Phase-5-full-port scope, not spike scope.

Net: GO on Phase-5 portability stands, independently corroborated + strengthened. DEFER-the-build (§8, trigger-gated) unchanged. Spike updated in place: /srv/claude/engineer/phase5-bake-spike/ (bash harness.sh → 15/15).

## Phase-5 bake spike — adversarial verification CLOSED (Shipwright), recommendation strengthened Shipwright independently reproduced the spike and audited all three scope claims. **Verdict: GO holds, corroborated.** Two items surfaced — both now *closed empirically* rather than merely documented (spike re-run: **15/15**). ### Independent reproduction (Shipwright) - `bash harness.sh` → 10/10 tree-SHA equality reproduced; `SPIKE_MUTATE=strip-newline` → tree-cases red, confirmed non-vacuous. - **Tree-SHA-is-the-right-invariant, confirmed at source**: no `build_bake.bats` #172 test pins a commit-object SHA/bytes; a commit SHA embeds author/committer timestamps so it *cannot* be a reproducibility invariant. The commit-SHA-as-timestamp-noise dismissal is correct. ### Item 1 — sed first-per-line divergence → CLOSED Shipwright's independent 11-case edge matrix found **1 divergence**: two markers on **one physical line**. Bash `sed` (no `g` flag) rewrites first-per-line; the spike's original `regexp.ReplaceAll` rewrote all. Production-unreachable (all 5 real reusables carry exactly one marker; check-self-bootstrap's read-side `NORM_SED` uses `g` and tolerates either way), but a real faithfulness gap. **Fixed to match the oracle exactly** (`rewriteMarker` now splits on `\n`, replaces first-per-line, rejoins — `bytes.Split/Join` is a perfect inverse so trailing-newline byte-exactness is untouched). New harness case `two-markers-one-line` → bash==Go ✅; real-repo tree SHAs unchanged (no regression). This is the faithful-reproduce call (match the bash source-of-record incl. its quirk); the alternative (replace-all + a Phase-8 assert-never-two-markers-per-line) is named but not taken. All other edge cases Shipwright probed **already agreed**: marker@EOF-without-trailing-newline ✅, CRLF ✅, two-markers-two-lines ✅, empty blob ✅, non-ASCII/UTF-8 body ✅, valid-adversarial refs (`.hidden` / `///` / `-rc` / `a..b` / `_`) ✅. ### Item 2 — commit design-invariants → now MEASURED (was reproduced-by-construction) Shipwright's honesty note: the tree-only spike retired the *byte-exactness/fragility* axis empirically, but the commit-wrapper *design-invariants* (`bats #172:301-351`: parent==base, HEAD-untouched, throwaway-index/worktree-clean) were reproduced *by construction* (`commit-tree -p base`), not independently exercised. **Closed**: added `SPIKE_EMIT=commit` (builds the detached commit off base) + harness assertions measuring against the Go-built commit: `parent == base` ✅, `HEAD unmoved` ✅, `working tree clean` ✅, `rc ref baked at the commit` ✅. ### Audited dismissals — both HOLD (Shipwright) - **check-self-bootstrap read-side (`NORM_SED`)**: both HEAD and pinned sides go through the *identical* `git show | sed` pipeline, so the command-sub newline-strip that made the *write* side fragile hits both sides symmetrically and cancels — no byte-preservation-against-external-reference requirement, no trap. Inherits retirement *a fortiori* (strict subset of the write-side concern). - **go-git vs plumbing-shellout**: non-gating **conditionally** — plumbing-shellout is a *proven floor* (this spike), and go-git is a later 4a single-binary optimization carrying its own R2-class edge risk (filemode/empty-tree/index), not a portability gate. The condition: this holds *because* shelling to plumbing is ADR-R2-endorsed. If a future decision *mandated* zero-git-subprocess, the sub-fork re-enters as real risk — nothing mandates that today. ### Honest scope of "R1 retired" (final, precise) - **Byte-exactness / implementation-fragility axis** — **retired empirically** (tree-SHA equality incl. real reusables, mutation-verified, sed-faithful). - **Design-invariant axis, single-actor (repin `create_detached_bake_commit` leg)** — **now measured** (parent/HEAD/worktree). - **Three-actor full self-bootstrap round-trip** — **not exercised**: the other two legs (`bake_toolkit_ref` at cut-prep via `release-prep.sh`; the post-cut `'main'` reset via `reusable-release.yml`) reuse the *same* `rewriteMarker` kernel proven here, so they *inherit* byte-exactness — but the full bake→cut→reset→re-pin round-trip is Phase-5-full-port scope, not spike scope. **Net: GO on Phase-5 portability stands, independently corroborated + strengthened.** DEFER-the-build (§8, trigger-gated) unchanged. Spike updated in place: `/srv/claude/engineer/phase5-bake-spike/` (`bash harness.sh` → 15/15).
Author
Owner

Closing as done — design question answered + implementation shipped

This tracker's central design question — "consider Go rewrite for cross-runner + single-binary distribution + refactor-safety" — has been answered affirmatively and executed, though under a different milestone label than originally proposed.

Design arc that closed the question

  1. 2026-07-04: #367 filed — the design question opened
  2. 2026-07-07/08: Engineer branched investigation → ADR-0008 (Go port architecture analysis, PR#473)
  3. 2026-07-08: Operator ratification — sequencing decided: Phase-5 bake spike as concrete engagement first
  4. 2026-07-09: Phase-5 bake spike executed (Engineer + Shipwright adversarial verification); GO decision held
  5. Follow-up: ADR-0009 (Go port arc structure) authored with Phases 0a-9 laid out
  6. 2026-07-22 → 2026-07-29: Phase 0a through Phase 7 all landed (18+ PRs across the arc, milestone-tracked via #78 #79 #80)
  7. 2026-07-29 23:46 CEST: v0.34.0 tagged — first release shipping the completed Go port substrate (rt binary + reusable-release workflow calling rt <subcommand> for all 7 callsites)

Relationship to original #367 scope

Original #367 designed the port under v2.0.0 with phased deprecation across v1.x. ADR-0009 shifted this to v1.0.0 with the shell substrate deleted (not deprecated) — a stronger, more decisive shape than the original proposal.

All 11 migration steps named in #367's "Migration approach" section were executed under ADR-0009 Phase 6/6b/7:

  • Steps 1-5 (libraries: semver, cc, config, forgejo, fragments+changelog) → Phase 3/4
  • Steps 6-8 (orchestrators: decide, prep, release) → Phase 6
  • Step 9 (reusable-workflow callsite migration) → Phase 7 finale (#582, PR#597)
  • Step 10 (deprecate shell over minor releases) → SUPERSEDED — shell substrate deleted outright
  • Step 11 (cut v2.0.0 when shell substrate fully replaced) → SUPERSEDED — will cut v1.0.0 when board clears (Phase 8 + Phase 9)

Residual scope

None from #367 that isn't already covered by open trackers:

  • v1.0.0 board-clear: sibling tracker #338 (Phase 9 milestone) handles the SemVer stability commitment
  • Post-1.0.0 ecosystem work: #337 (broaden version-file basename), #336 (immutable consumer pinning to scripts) — these were noted in #367's "Related" section as v1.1.0 candidates; they remain open under their own scope
  • Future v2.0.0: whatever v2.0.0 will actually be about (post-1.0.0) is a distinct future design conversation, not the same shape as #367's shell-vs-Go framing

Anchor

Closed 2026-07-30 by Bosun per operator direction (Phase 7 residuals cleanup). The Go port arc that this design tracker seeded is complete + shipping; the tracker's question is answered by the substrate's own existence.

## Closing as done — design question answered + implementation shipped This tracker's central design question — *"consider Go rewrite for cross-runner + single-binary distribution + refactor-safety"* — has been **answered affirmatively and executed**, though under a different milestone label than originally proposed. ### Design arc that closed the question 1. **2026-07-04**: #367 filed — the design question opened 2. **2026-07-07/08**: Engineer branched investigation → [ADR-0008](https://git.frankenbit.de/frankenbit/release-toolkit/src/branch/main/docs/adr/0008-go-port-architecture.md) (Go port architecture analysis, PR#473) 3. **2026-07-08**: Operator ratification — sequencing decided: Phase-5 bake spike as concrete engagement first 4. **2026-07-09**: Phase-5 bake spike executed (Engineer + Shipwright adversarial verification); GO decision held 5. **Follow-up**: ADR-0009 (Go port arc structure) authored with Phases 0a-9 laid out 6. **2026-07-22 → 2026-07-29**: Phase 0a through Phase 7 all landed (18+ PRs across the arc, milestone-tracked via [#78](https://git.frankenbit.de/frankenbit/release-toolkit/milestones/78) [#79](https://git.frankenbit.de/frankenbit/release-toolkit/milestones/79) [#80](https://git.frankenbit.de/frankenbit/release-toolkit/milestones/80)) 7. **2026-07-29 23:46 CEST**: v0.34.0 tagged — first release shipping the completed Go port substrate (`rt` binary + reusable-release workflow calling `rt <subcommand>` for all 7 callsites) ### Relationship to original #367 scope Original #367 designed the port under **v2.0.0** with phased deprecation across v1.x. ADR-0009 shifted this to **v1.0.0** with the shell substrate deleted (not deprecated) — a stronger, more decisive shape than the original proposal. All 11 migration steps named in #367's "Migration approach" section were executed under ADR-0009 Phase 6/6b/7: - Steps 1-5 (libraries: semver, cc, config, forgejo, fragments+changelog) → Phase 3/4 - Steps 6-8 (orchestrators: decide, prep, release) → Phase 6 - Step 9 (reusable-workflow callsite migration) → Phase 7 finale (#582, PR#597) - Step 10 (deprecate shell over minor releases) → SUPERSEDED — shell substrate deleted outright - Step 11 (cut v2.0.0 when shell substrate fully replaced) → SUPERSEDED — will cut v1.0.0 when board clears (Phase 8 + Phase 9) ### Residual scope None from #367 that isn't already covered by open trackers: - **v1.0.0 board-clear**: sibling tracker [#338](https://git.frankenbit.de/frankenbit/release-toolkit/issues/338) (Phase 9 milestone) handles the SemVer stability commitment - **Post-1.0.0 ecosystem work**: #337 (broaden version-file basename), #336 (immutable consumer pinning to scripts) — these were noted in #367's "Related" section as v1.1.0 candidates; they remain open under their own scope - **Future v2.0.0**: whatever v2.0.0 will actually be about (post-1.0.0) is a distinct future design conversation, not the same shape as #367's shell-vs-Go framing ### Anchor Closed 2026-07-30 by Bosun per operator direction (Phase 7 residuals cleanup). The Go port arc that this design tracker seeded is complete + shipping; the tracker's question is answered by the substrate's own existence.
bosun closed this issue 2026-07-30 13:10:24 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#367
No description provided.