From da3ac2fda3547d6661b65cdf8f7e564337073be6 Mon Sep 17 00:00:00 2001 From: nicodes Date: Sat, 1 Aug 2026 20:58:43 -0600 Subject: [PATCH 1/2] One build path: extract the cross-compile into a shared action CI ran scripts/cli_build.sh -- one host binary, no version stamped in. release.yml cross-compiled six targets inline with its own loop. Only the CI path ran on a pull request, so a break in any of the other five -- a build tag, a platform-specific import, a syscall that does not exist on windows -- surfaced first at release time, on the one build nobody is watching. Both now call .github/actions/build. A pull request builds exactly what a release builds; the only difference is the version stamped in, and that one of them goes on to publish. Build runs before Test in release.yml too, matching CI and every other workflow in the organisation. scripts/cli_build.sh stays. It is the fast host build for the person at the terminal, and it is no longer what CI checks. Verified locally: all six targets compile and archive, the extracted Linux binary reports `gdam 1.2.3` when built with that version, and windows/arm64 -- a target CI never checked -- cross-compiles clean. Co-Authored-By: Claude Opus 5 (1M context) --- .github/actions/build/action.yml | 107 +++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 13 ++-- .github/workflows/release.yml | 51 ++------------- 3 files changed, 119 insertions(+), 52 deletions(-) create mode 100644 .github/actions/build/action.yml diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 0000000..5a718fb --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,107 @@ +name: Build +description: > + Everything a release ships: the binary for every supported platform, archived, + with checksums. + + A composite action rather than a reusable workflow so it runs in the caller's + job, under the caller's name -- CI / Build, Release / Build -- rather than as + a nested "caller / callee" check. + + This replaces two different builds. CI ran scripts/cli_build.sh, which + produces ONE host binary with no version stamped into it; release.yml + cross-compiled six targets inline with its own loop. Only the CI path ran on a + pull request, so a break in any of the six -- a build tag, a + platform-specific import, a syscall that does not exist on windows -- surfaced + first at release time, on the one build nobody is watching. + + Now a pull request builds exactly what a release builds. The only difference + is the version stamped in, and that one of them goes on to publish. + + scripts/cli_build.sh stays. It is the fast host build for the person at the + terminal, and it is not what CI checks any more. + + NOTHING LEAVES THE RUNNER. The archives are left in ./dist. Creating the + release is a step the caller writes out, so nothing named "build" can publish + as a side effect of being reused -- and it has no token to publish with. + + Assumes Go is already set up. See ../test. + +inputs: + version: + description: > + What the binary reports for `gdam version`, baked in with -X. + + Defaults to "dev" rather than being required, because a pull request has + no version and should not have to invent one -- and "dev" is the honest + answer for a binary built from a working tree that may match no tag. + required: false + default: dev + commit: + description: The commit stamped into the binary alongside the version. + required: false + default: "" + +runs: + using: composite + steps: + - name: Cross-compile + shell: bash + env: + VERSION: ${{ inputs.version }} + COMMIT: ${{ inputs.commit }} + run: | + set -euo pipefail + DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + rm -rf dist + mkdir -p dist + + build() { + goos="$1"; goarch="$2"; os_label="$3"; arch_label="$4"; archive_ext="$5" + exe="" + if [ "$goos" = "windows" ]; then exe=".exe"; fi + + name="gdam_${os_label}_${arch_label}" + out_dir="dist/$name" + mkdir -p "$out_dir" + out_path="$PWD/$out_dir/gdam$exe" + # CGO off so the binary is static and runs on any distro. + GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=0 go build \ + -ldflags "-s -w -X main.version=$VERSION -X main.commit=$COMMIT -X main.date=$DATE" \ + -o "$out_path" . + cp README.md "$out_dir/README.md" + + if [ "$archive_ext" = "zip" ]; then + (cd "$out_dir" && zip -q "../$name.zip" "gdam$exe" README.md) + else + tar -C "$out_dir" -czf "dist/$name.tar.gz" "gdam$exe" README.md + fi + echo " $name" + } + + build linux amd64 Linux x86_64 tar.gz + build linux arm64 Linux arm64 tar.gz + build darwin amd64 Darwin x86_64 tar.gz + build darwin arm64 Darwin arm64 tar.gz + build windows amd64 Windows x86_64 zip + build windows arm64 Windows arm64 zip + + (cd dist && sha256sum -- *.tar.gz *.zip > checksums.txt) + + # The archive is what people download, so the check runs against what came + # out of it rather than against the tree it was built from. Only the + # runner's own platform can be executed; the other five are proven to link + # by having compiled at all. + - name: The built binary reports its version + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + tmp="$(mktemp -d)" + tar -xzf dist/gdam_Linux_x86_64.tar.gz -C "$tmp" + got="$("$tmp/gdam" version 2>&1 || true)" + echo "$got" + case "$got" in + *"$VERSION"*) ;; + *) echo "::error::built binary does not report '$VERSION'"; exit 1 ;; + esac diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3006d8e..3f9d6e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,13 +24,12 @@ jobs: # Built first: a compile error should fail before a test suite reaches the # same conclusion more slowly. # - # NOTE: this builds through scripts/cli_build.sh, and release.yml - # cross-compiles inline with its own loop. Two build paths, and only this - # one runs on a pull request -- so a break in the release cross-compile - # still surfaces first at release time. Unifying them is a real change and - # deserves its own pull request; this one only stops the TEST drifting. - - name: Build CLI - run: ./scripts/cli_build.sh + # The same action Release uses, so a pull request builds exactly what a + # release builds -- all six targets. This used to run scripts/cli_build.sh, + # which produces one host binary: a break in windows/arm64 or darwin/arm64 + # surfaced first at release time. + - name: Build + uses: ./.github/actions/build - name: Test uses: ./.github/actions/test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a55ef82..b54bd49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,54 +66,15 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "tag=$TAG" >> "$GITHUB_OUTPUT" + - name: Build + uses: ./.github/actions/build + with: + version: ${{ steps.release.outputs.version }} + commit: ${{ github.sha }} + - name: Test uses: ./.github/actions/test - - name: Build release artifacts - env: - VERSION: ${{ steps.release.outputs.version }} - COMMIT: ${{ github.sha }} - run: | - set -euo pipefail - DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - mkdir -p dist - - build() { - goos="$1" - goarch="$2" - os_label="$3" - arch_label="$4" - archive_ext="$5" - exe="" - if [ "$goos" = "windows" ]; then - exe=".exe" - fi - - name="gdam_${os_label}_${arch_label}" - out_dir="dist/$name" - mkdir -p "$out_dir" - out_path="$PWD/$out_dir/gdam$exe" - GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=0 go build \ - -ldflags "-s -w -X main.version=$VERSION -X main.commit=$COMMIT -X main.date=$DATE" \ - -o "$out_path" . - cp README.md "$out_dir/README.md" - - if [ "$archive_ext" = "zip" ]; then - (cd "$out_dir" && zip -q "../$name.zip" "gdam$exe" README.md) - else - tar -C "$out_dir" -czf "dist/$name.tar.gz" "gdam$exe" README.md - fi - } - - build linux amd64 Linux x86_64 tar.gz - build linux arm64 Linux arm64 tar.gz - build darwin amd64 Darwin x86_64 tar.gz - build darwin arm64 Darwin arm64 tar.gz - build windows amd64 Windows x86_64 zip - build windows arm64 Windows arm64 zip - - (cd dist && sha256sum *.tar.gz *.zip > checksums.txt) - - name: Create GitHub Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From b15aa2e4c143a379c003e7a6088b0e5178bb3f46 Mon Sep 17 00:00:00 2001 From: nicodes Date: Sat, 1 Aug 2026 21:02:43 -0600 Subject: [PATCH 2/2] Fix the version assertion for unstamped builds The first run of this action failed with "built binary does not report 'dev'", and the check was wrong rather than the binary. main.go falls back to the module version in its build info when nothing was stamped -- the `version == "dev"` branch at main.go:37 -- so in a git checkout the toolchain's VCS pseudo-version is what comes out. A pull request legitimately reports `gdam v0.0.8-0.20260802025843-da3ac2fda354`, never the literal "dev". A requested version is still asserted exactly, because that is what a release depends on. The default path asserts the shape instead: the binary named itself and reported something. This is the same mistake the ormos build action already documents, and carrying that comment across is the point of writing it down. Verified locally on both paths: VERSION=1.2.3 reports `gdam 1.2.3`, and VERSION=dev reports the pseudo-version and passes. Co-Authored-By: Claude Opus 5 (1M context) --- .github/actions/build/action.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 5a718fb..d219031 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -101,7 +101,25 @@ runs: tar -xzf dist/gdam_Linux_x86_64.tar.gz -C "$tmp" got="$("$tmp/gdam" version 2>&1 || true)" echo "$got" - case "$got" in - *"$VERSION"*) ;; - *) echo "::error::built binary does not report '$VERSION'"; exit 1 ;; - esac + + # A requested version must be reported exactly -- that is a release, and + # an archive claiming a different version than its tag is the whole + # failure this guards. + # + # The default is NOT checked against the literal "dev". main.go falls + # back to the module version in its build info when nothing was stamped + # (see the `version == "dev"` branch there), so in a git checkout the + # toolchain's VCS pseudo-version is what comes out -- a pull request + # legitimately reports something like + # `gdam v0.0.8-0.20260802025843-da3ac2fda354`. Asserting "dev" here + # failed the first run of this action, and it was the check that was + # wrong rather than the binary. + if [ "$VERSION" != "dev" ]; then + case "$got" in + *"$VERSION"*) ;; + *) echo "::error::built binary reports '$got', expected it to contain '$VERSION'"; exit 1 ;; + esac + elif [ -z "${got#gdam }" ] || [ "${got#gdam }" = "$got" ]; then + echo "::error::built binary reports '$got', expected 'gdam '" + exit 1 + fi