| 📦 Turbo Go 3d7798b k33g 11h ago | 1 | # Handoff — 2026-08-31 — The version in the About box |
| 2 | |
| 3 | ## State |
| 4 | |
| 5 | Ticket 0010 is implemented and **uncommitted**, on `main`, on top of PR #7 (`88a4c38`). Nothing was branched: the changes are in the working tree, so `git checkout -b feature/about-version` at any point before committing carries them along. |
| 6 | |
| 7 | `internal/version` is new. `app.Version` is gone. The Makefile, `scripts/install.sh` and a new `make version` target stamp `git describe --tags --dirty`, the short commit and a UTC build time through `-ldflags -X`. About and `-version` show whatever the build recorded and stay silent about the rest. |
| 8 | |
| 9 | Full suite green, green under `-race`. Quality gate PASS — 0/0/0, complexity 1592. Docs in both languages, four READMEs, and the drawio diagram all in sync. |
| 10 | |
| 11 | Also uncommitted, and **not mine**: `.tickets/issues/0004` and `0017` at `state: closed`, from the previous session. |
| 12 | |
| 13 | ## In flight |
| 14 | |
| 15 | Nothing. |
| 16 | |
| 17 | ## Next steps |
| 18 | |
| 19 | 1. **Review and commit.** A commit message was proposed at the end of the session. |
| 20 | 2. **Close ticket 0010** — the user's call, as always. |
| 21 | 3. The first real release is untested by definition: nothing has been tagged since `v0.1.0`. `make version` on a tagged commit should print the bare tag with no `-N-g<hash>` suffix, and that is the one assertion no test in this repository can make for you. |
| 22 | |
| 23 | ## Open questions / blockers |
| 24 | |
| 25 | None. |
| 26 | |
| 27 | ## Watch out for |
| 28 | |
| 29 | - **Go 1.26 does not say `(devel)`.** A plain `go build .` in a checkout reports a *pseudo-version* — `0.1.1-0.20260831165958-88a4c3859bf3+dirty` — derived from the last tag. `isPseudoVersion` catches it and reports `devel`. If a future Go changes that shape, `TestEveryFormOfPseudoVersionIsRecognised` is where it will show. |
| 30 | - **The separator before a pseudo-version's timestamp is a dot, not a dash**, whenever a base tag precedes the commit — the base ends in `-0.` or `-pre.0.`. My first recogniser assumed a dash and silently matched nothing. Three of the four forms in the test caught it. |
| 31 | - **`vcs.time` is not a build date.** It is the commit's timestamp. Anyone tempted to fill the empty `Built:` line for unstamped builds from it will make it wrong on every binary. |
| 32 | - **`resolve` is separate from `Current` on purpose.** A test binary cannot be built with linker stamps, so testing through `Current` would leave every interesting case uncovered. Add new cases to `resolve`, not to `Current`. |
| 33 | - **`unknown` is load-bearing.** It is what `-version` prints when nothing named the build, and `TestTheInstalledBinaryDoesNotReportAnUnknownVersion` uses its absence to prove the installer's ldflags reached the linker. Do not make it a version number. |
| 34 | - Everything under `.memory/handoffs/2026-08-31-tool-menus.md` still applies — in particular the VT-emulator recipe, which verified this feature too, and the rule that `internal/terminal/tmprender/` must be deleted again before the quality gate. |
| 35 | |
| 36 | ## Afterwards — the release script, and what removing a constant costs |
| 37 | |
| 38 | The user ran their own `./03-build-releases.sh` and it failed. Two defects, both mine: |
| 39 | |
| 40 | 1. It read the version with `awk '{print $NF}'`, which took the build timestamp once `-version` grew a parenthetical. |
| 41 | 2. **The cross-compile loop had no `-ldflags` at all.** All five downloadable binaries would have said `devel` while the release announced `v0.2.0`. The host binary was stamped and correct, so nothing but a hand check would have caught it. |
| 42 | |
| 43 | The second is the lesson: **removing a compiled-in constant moves a cost from visible to invisible.** A stale constant at least travelled into every build; a stamp only reaches the builds that ask for it. Every build path has to be found and stamped — `make build`, `scripts/install.sh`, and the five cross-compiles in `03-build-releases.sh`. |
| 44 | |
| 45 | Fixed by adding a `make ldflags` target the script reads, so the `-X` paths exist once. The script's version check is now three plain questions to git and one `grep -F`, none of which parses the `-version` sentence. `release_test.go` covers all of it. |
| 46 | |
| 47 | **If a sixth build path ever appears, stamp it.** `TestTheReleaseScriptStampsTheBinariesItShips` only guards the one that exists. |
| 48 | |
| 49 | ## And then — the tag script was the real culprit |
| 50 | |
| 51 | `03` refused again, and this time it was right but misdiagnosed: it said "HEAD carries no tag" when the tag existed and HEAD had merely moved one commit past it. |
| 52 | |
| 53 | The cause was three steps upstream. **`01-release.tag.sh` had no `set -e`.** Run twice, its `git tag` failed with "already exists", the failure was ignored, and the `git push origin "${TAG}"` on the next line pushed the *old* tag. Everything downstream was then working correctly on a lie. |
| 54 | |
| 55 | Fixed: `set -euo pipefail`, a tag-exists check against the local ref **and** `git ls-remote` (the state the user was in — local tag deleted, remote tag still there — is invisible locally), nothing-to-commit tolerated, and the tag applied only after a successful push. |
| 56 | |
| 57 | **`02-release.publish.sh` still has no `set -e` and must not naively be given one**: its `read -r -d '' DATA <<-EOM` always exits non-zero by design and would kill the script immediately. It also needs the curl HTTP status checked, since curl exits 0 on a 4xx. Left alone deliberately — it publishes to Codeberg and the user was not asking for it. |
| 58 | |
| 59 | **The remote is unreachable from this sandbox** (SSH, no key), so whether `v0.2.0` still exists on Codeberg could not be established. That question was handed back to the user. |
| 60 | |
| 61 | ## Finally — the simplification, and the lesson |
| 62 | |
| 63 | The user stopped me: *"fais quelque chose de plus simple, tu build comme avant avec le tag de release."* They were right. |
| 64 | |
| 65 | I had `03` stamp `git describe` and then **verify** it agreed with `TAG` — three gates that each looked reasonable and together blocked a release three times running. `git describe` answers *where is HEAD*; a release builder is asking *what release is this*. Those are different questions, and the gates existed only to reconcile an answer I should never have been asking for. |
| 66 | |
| 67 | `03` now stamps `TAG` directly (`make ldflags VERSION="${TAG}"`, `make build VERSION="${TAG}"`) and the gates are gone. Building needs no tag at all; only `02` does. **Do not reintroduce those checks** — the mismatch they detected cannot occur once the tag is the single source. |
| 68 | |
| 69 | `01`'s guards stay: they prevent pushing the *wrong* tag and block no build. |
| 70 | |
| 71 | The lesson, for whoever hits something like this: when you find yourself adding checks to reconcile two sources of truth, delete one of the sources instead. |