From 344974fc028bdcf5822fb74fdbf5b2a778263325 Mon Sep 17 00:00:00 2001 From: nicodes Date: Sun, 2 Aug 2026 23:44:32 -0600 Subject: [PATCH 1/2] Run the tests before the cross-compile Neither reads the other's output: the build action cross-compiles and asks the binary for its version, and the test action formats, vets and tests the source. So the order decides one thing only -- which failure you hear about sooner -- and a vet or test failure is a more common way to break this than the cross-compile is. Nothing else changes here. setup-go already keeps a Go build cache and the run log shows it restoring, which is why this repository needs none of the cache work the mise-based ones did. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b81c7b..f8a8db1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,8 +47,8 @@ jobs: # Built first: a compile error should fail before a test suite spends # minutes reaching the same conclusion more slowly. - - name: Build - uses: ./.github/actions/build - - name: Test uses: ./.github/actions/test + + - name: Build + uses: ./.github/actions/build From 58aa7e7ba7a3cc0a1518cd319ee07d45d6145861 Mon Sep 17 00:00:00 2001 From: nicodes Date: Mon, 3 Aug 2026 00:43:22 -0600 Subject: [PATCH 2/2] Install the toolchain with mise, like everything else Every repository in the fleet now installs its toolchain through mise. This one used setup-go, and while that was defensible on its own terms it meant the answer to "which Go builds this" depended on which repository you were standing in. The comment this replaces made the real argument against changing it: reading go-version-file: go.mod kept the version in one place, and a bump could not leave CI testing the old one. That argument is sound and the change gives it up, so it is replaced rather than deleted -- a new .mise.toml is a second place for the version to live. What makes that acceptable is that the drift is now checked instead of hoped for. "Toolchain pins agree" reads both files and fails the run if .mise.toml pins an older Go than go.mod asks for. That failure is not hypothetical: astry pinned 1.24.5 against a go.mod asking for 1.25 and built anyway, because GOTOOLCHAIN=auto fetched the real one behind the pin -- so the pin described nothing and nothing said so. setup-go was also caching the Go build for free, which mise does not do. That cache is now explicit, keyed on go.sum and the toolchain pin, so dropping setup-go does not quietly trade a warm compile for a cold one. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++----- .github/workflows/release.yml | 34 ++++++++++++++++++++++++++++++--- .mise.toml | 13 +++++++++++++ 3 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 .mise.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8a8db1..4ee990e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,12 +38,38 @@ jobs: # whoever can move it can run code in this job. - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + # mise rather than setup-go, so every repository in the fleet installs its + # toolchain the same way. The cost is that the Go version now lives in + # .mise.toml as well as go.mod; the step below is what keeps them honest. + - uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4.2.3 + + - name: Toolchain pins agree + shell: bash + run: | + set -euo pipefail + mod="$(sed -n 's/^go //p' go.mod | head -n1)" + pin="$(sed -n 's/^go = "\(.*\)"$/\1/p' .mise.toml | head -n1)" + if [ -z "$mod" ] || [ -z "$pin" ]; then + echo "::error::could not read the Go version from go.mod ($mod) or .mise.toml ($pin)" + exit 1 + fi + if [ "$(printf '%s\n%s\n' "$mod" "$pin" | sort -V | head -n1)" != "$mod" ]; then + echo "::error::.mise.toml pins Go $pin but go.mod asks for $mod. CI would build against the older one while GOTOOLCHAIN quietly fetched the newer, so the pin would describe nothing." + exit 1 + fi + echo "go.mod wants $mod, .mise.toml pins $pin" + + # What setup-go did for free. mise installs the toolchain and caches + # nothing else, so this is what stops every run recompiling from cold. + - name: Cache Go build + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - # Read from go.mod rather than pinned here, so the version lives in - # one place and a bump cannot leave CI testing the old one. - go-version-file: go.mod - cache-dependency-path: go.sum + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: go-${{ runner.os }}-${{ hashFiles('go.sum', '.mise.toml') }} + restore-keys: | + go-${{ runner.os }}- # Built first: a compile error should fail before a test suite spends # minutes reaching the same conclusion more slowly. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4b0430..35a2da3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,10 +78,38 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" echo "tag=$tag" >> "$GITHUB_OUTPUT" - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + # mise rather than setup-go, so every repository in the fleet installs its + # toolchain the same way. The cost is that the Go version now lives in + # .mise.toml as well as go.mod; the step below is what keeps them honest. + - uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4.2.3 + + - name: Toolchain pins agree + shell: bash + run: | + set -euo pipefail + mod="$(sed -n 's/^go //p' go.mod | head -n1)" + pin="$(sed -n 's/^go = "\(.*\)"$/\1/p' .mise.toml | head -n1)" + if [ -z "$mod" ] || [ -z "$pin" ]; then + echo "::error::could not read the Go version from go.mod ($mod) or .mise.toml ($pin)" + exit 1 + fi + if [ "$(printf '%s\n%s\n' "$mod" "$pin" | sort -V | head -n1)" != "$mod" ]; then + echo "::error::.mise.toml pins Go $pin but go.mod asks for $mod. CI would build against the older one while GOTOOLCHAIN quietly fetched the newer, so the pin would describe nothing." + exit 1 + fi + echo "go.mod wants $mod, .mise.toml pins $pin" + + # What setup-go did for free. mise installs the toolchain and caches + # nothing else, so this is what stops every run recompiling from cold. + - name: Cache Go build + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - go-version-file: go.mod - cache-dependency-path: go.sum + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: go-${{ runner.os }}-${{ hashFiles('go.sum', '.mise.toml') }} + restore-keys: | + go-${{ runner.os }}- # The same steps CI runs, from the same definition. Running them again # against this exact commit is not redundant: CI passing on this SHA diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..5d52bb8 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,13 @@ +[tools] +# Must be >= the `go` directive in go.mod, which is currently 1.25.0. +# +# CI used to read the version straight out of go.mod via setup-go, so there was +# only ever one place for it to live. Moving to mise makes the toolchain +# consistent across the fleet but creates a second place -- and a pin that +# silently disagrees with go.mod is not hypothetical here: astry ran 1.24.5 +# against a go.mod asking for 1.25, and it built only because GOTOOLCHAIN=auto +# downloaded the real one behind the pin, so the pin described nothing. +# +# The "Toolchain pins agree" step in ci.yml is what stops that happening again. +# If you bump go.mod, bump this too; CI will tell you if you forget. +go = "1.25.0"