design(v2.0.0): consider Go rewrite for cross-runner + single-binary distribution + refactor-safety #367
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#367
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Motivation
External cold-read round 2 (2026-07-03) verdict language flagged shell-substrate concerns:
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:
...then a Go rewrite becomes a coherent v2.0.0 initiative.
Language choice rationale (Go over Rust)
Go (recommended if shift happens):
Rust (not recommended for this project):
Stay in shell (current default):
Migration approach (if v2.0.0 fires)
Not big-bang rewrite. Module-by-module with migration tests validating byte-identical behavior on edge cases:
scripts/lib/semver.sh→semver.gowith bats equivalence testsscripts/lib/conventional-commits.sh→cc.goscripts/lib/config.sh→config.goscripts/lib/forgejo-api.sh→forgejo/api.go(folds #334 API resilience naturally)scripts/lib/fragments.sh+changelog.sh→changelog.goscripts/release-decide.shorchestrator →cmd/rt-decidescripts/release-prep.shorchestrator →cmd/rt-prepscripts/draft-release.shorchestrator →cmd/rt-releasertbinary instead of shell scriptsDecision triggers
Fire this work when ANY of:
Do NOT fire on:
Related
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).
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
v2/next@7fa5ce67(from currentmainHEAD post-v0.32.0-cut bookkeeping)Analysis scope
Doc-only at this stage (no Go code yet). Concrete artifacts targeted:
Location:
docs/adr/00XX-go-port-architecture.mdonv2/nextbranch. Pure docs; no Go compilation infrastructure yet.Roles
Tracker home
This tracker (#367) is the analysis tracker. Analysis output = ADR on
v2/nextbranch + summary comment here. Decision outcomes:Timing
Doesn't gate v1.0.0. Multi-week runway.
v1.0.0approach continues onmainvia#338memorial-chore fold; analysis proceeds onv2/nextin parallel. Whenv1.0.0tags,v2/nexteither 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 +
#448AC verification.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.0with 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.
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
Rationale for sequencing (Ember-first)
Rationale for spike-over-deeper-analysis
Substrate not-yet-done
proposedfor now; operator explicitly deferred the "defer" state — the spike-then-decide path leaves ADR at proposed until either spike-succeeds → flip toaccepted (go)OR spike-fails → flip toaccepted (defer)priority/lowremains stale for a substantive substrate decision; leaving as-is since active work is weeks awayAnchor
Substrate-of-record for the conversation 2026-07-08 (post-alcatraz-infra drive-to-close). Engineer notified for post-ember#4 metabolism awareness.
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_refmachinery (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
#172bats 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.ReplaceAllover[]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
v0.15.1-rc.1,v0.13.0,main,feat/some-branch}create_detached_bake_commitdefault set), ref ∈ {v0.16.0-rc.1,main}|MALICIOUS,;rm,extra) — both rejectHighest-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
sedgone (→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.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.shorchestration (Forgejo API, worktree push, PR-open) not ported — but that is R2/clean-seam translation, explicitly not the R1 risk, and already lower-rated.check-self-bootstrap.shread-side (HEAD-vs-pinnedNORM_SEDblob-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.shrebuilds + 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 — 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.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(nogflag) rewrites first-per-line; the spike's originalregexp.ReplaceAllrewrote all. Production-unreachable (all 5 real reusables carry exactly one marker; check-self-bootstrap's read-sideNORM_SEDusesgand tolerates either way), but a real faithfulness gap.Fixed to match the oracle exactly (
rewriteMarkernow splits on\n, replaces first-per-line, rejoins —bytes.Split/Joinis a perfect inverse so trailing-newline byte-exactness is untouched). New harness casetwo-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)
NORM_SED): both HEAD and pinned sides go through the identicalgit show | sedpipeline, 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).Honest scope of "R1 retired" (final, precise)
create_detached_bake_commitleg) — now measured (parent/HEAD/worktree).bake_toolkit_refat cut-prep viarelease-prep.sh; the post-cut'main'reset viareusable-release.yml) reuse the samerewriteMarkerkernel 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).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
rtbinary + reusable-release workflow callingrt <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:
Residual scope
None from #367 that isn't already covered by open trackers:
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 referenced this issue2026-07-30 13:14:58 +02:00