Files
gastownhall__beads/scripts/build-examples.sh
T
matt wilkie 935afe2466 fix(examples): tidy both example modules and build them in CI (#5229)
* fix(examples): tidy both example modules and build them in CI

Both Go modules under examples/ fail a plain `go build` on current main:

    go: updates to go.mod needed; to update it:
            go mod tidy

They are separate modules that reach the parent through
`replace github.com/steveyegge/beads => ../..`, so their go.mod and go.sum
record the parent's entire dependency graph. Every root dependency change
therefore invalidates them — and nothing in CI ever compiled them, so the drift
accumulated silently. examples/ is the first code a new user copies, which makes
this a bad first five minutes rather than a cosmetic wart.

Two parts:

1. `go mod tidy` in examples/bd-example-extension-go and examples/library-usage.
   The extension example carries most of the churn (~1.6k lines of go.sum),
   which is inherent to recording the parent graph through the replace directive
   and is why an earlier fix (#4942, the Go 1.26.5 bump) deliberately left it
   out. Both modules now build clean with the project's canonical
   `-tags gms_pure_go`.

2. scripts/build-examples.sh plus a `build-examples` job in the PR workflow, so
   the same drift cannot recur unnoticed. The script discovers example modules
   from `git ls-files 'examples/*/go.mod'` (no hardcoded list), sources
   .buildflags for the canonical CGO/tag settings, builds into a scratch
   directory so it leaves no untracked binaries, reports every failing module
   rather than stopping at the first, and prints the exact `go mod tidy`
   command to run.

The job is deliberately NOT added to ci-gate's required list. Because of the
replace directive it would fail on any root go.mod change not mirrored into the
examples, so making it blocking imposes a "tidy the examples too" step on every
dependency bump. That is a maintainer call about contributor friction; the job
is visible on the checks list either way, and pr.yml records how to promote it.

Verified locally: both modules build with -tags gms_pure_go; the script exits 1
and names the module when an example go.mod is reverted to its pre-tidy state;
scripts/check-build-tags.sh stays clean (the script sources .buildflags).

Agent-Signature: claude-opus-5-high on behalf of matt wilkie

* fix(examples): address dual-vendor review — vet not build, advisory job, portability

Reviewed by a claude reviewer and scripts/codex-agent reviewer (gpt-5.6-sol).
Four fixes, two of them correctness.

1. `go build -o <dir>/ ./...` was a FALSE GREEN: with -o naming a directory, Go
   compiles only the MAIN packages and silently skips every library package.
   Reproduced in an isolated module with a good main package plus a library
   package containing a type error — `go build -o dir/ ./...` exits 0 while
   `go build ./...` and `go vet ./...` both exit 1. The mirror-image bug: a
   library-only example module fails `go build` with "no main packages to
   build", a false red. Switched to `go vet ./...`, which type-checks every
   package INCLUDING test files (examples/library-usage/main_test.go exercises
   a lot of live API, and `go mod tidy` counts its imports, so a build that
   never compiles it left part of the recorded graph unverified), writes no
   artifacts, and still fails on the stale-go.mod condition this exists for.
   That also deletes the mktemp/trap/scratch-dir machinery entirely.

2. "Not in ci-gate" is NOT "non-blocking", which the previous comment claimed.
   Verified in source: pr-preflight.sh gates on every FAILURE in the raw
   statusCheckRollup and calls block(), and pr-babysit requires all rollup
   entries SUCCESS/NEUTRAL/SKIPPED before merging. Both ignore ci-gate
   membership. So the previous state was the one posture that stalls the merge
   patrol repo-wide after any un-mirrored dependency bump while advertising
   itself as optional. The job is now continue-on-error: true — genuinely
   advisory — and the comment records how to promote it to a real gate. Whether
   it SHOULD be a gate remains the maintainers' call.

3. Portability and robustness in the script, all reproduced by the reviewers:
   - `mapfile` does not exist in bash 3.2 (stock macOS) and `xargs -r` is
     GNU-only, so the advertised local check could not run on macOS. Replaced
     with a NUL-delimited read loop, which also fixes module paths containing
     whitespace (`git ls-files | xargs -n1 dirname` turned "examples/has
     space/go.mod" into two bogus entries).
   - `source ./.buildflags` was unguarded under `set -uo pipefail`; a failure
     continued with GOFLAGS unset and would type-check the ICU path while
     check-build-tags.sh still passed, since that only greps for the literal
     string. Now a hard exit.
   - Finding zero modules exited 0. A job that checks nothing must not report
     success; it is now an error.

Verified: both modules vet clean; reverting an example go.mod to its pre-tidy
state still exits 1 with the exact `go mod tidy` command; a broken library
package now fails where it previously passed; check-build-tags.sh clean
(97 files); shellcheck clean.

Agent-Signature: claude-opus-5-high on behalf of matt wilkie

* fix(examples): drop GNU-only sort -z from the module discovery pipeline

BSD sort has no -z, so on stock macOS — the exact platform the Bash-3.2
compatibility block targets — the sort stage emptied the pipeline and the
script exited claiming 'found no example modules'. git ls-files output is
already sorted, so the stage bought nothing. Found by cross-vendor review
(codex gpt-5.6-sol) of this branch.

Agent-Signature: claude-fable-5-high on behalf of maphew

* fix(ci,examples): disable setup-go cache in build-examples; re-tidy examples after merge

TestGoCacheOwnershipTopology requires every setup-go step in pr.yml to set
cache: false (caching is owned by explicit restore/save steps); the new
build-examples job predates that policy landing on main. Also re-run
go mod tidy in both example modules so their recorded dependency graphs
match the merged main — exactly the drift this PR's CI job exists to catch.

Agent-Signature: claude-fable-5-high on behalf of maphew

* ci(examples): bound build-examples with timeout-minutes: 10

continue-on-error keeps a red result advisory, but a hung job at the
6-hour default timeout holds the check pending, stalling merge-lane
consumers the advisory posture was meant to protect. Matches
pr-preflight-platforms' bound.

Agent-Signature: claude-fable-5-high on behalf of maphew

* ci(examples): make the advisory posture real - step-level continue-on-error

Job-level continue-on-error still reports the check run as FAILURE in the
PR rollup (only the workflow-run conclusion flips), and pr-preflight /
pr-babysit gate on per-check-run conclusions - so the job as written was
de facto blocking while advertising itself advisory. Follow the cygwin-leg
precedent: continue-on-error on the build step, outcome-guarded ::warning
annotation on failure. Also restore the repo-wide Go module cache (the
examples' graph is the parent's via the replace directive) so the job
stops cold-downloading the full dependency graph on every PR.

Agent-Signature: claude-fable-5-high on behalf of maphew

* test: register build-examples' module-cache restore in the cache topology registry

TestGoCacheOwnershipTopology keeps an explicit inventory of every cache
step per job; the advisory-lane fix added a restore step to build-examples
without registering it, so the whole scripts package went red on this
head. Register the job (inventory, managed map, ordering, setup-go id)
and drop the step's unused 'restore-cache' id — the registry compares
ids, and nothing references it.

Agent-Signature: claude-fable-5-high on behalf of maphew
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019CwiLhLbAGZdJtKYT76CPp

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 21:16:27 -07:00

96 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# build-examples.sh — type-check every Go module under examples/.
#
# The example modules are separate Go modules that reach the parent through a
# `replace github.com/steveyegge/beads => ../..` directive, so their go.mod and
# go.sum record the parent's full dependency graph. Nothing in CI compiled them,
# which meant they went stale silently: on 2026-08-01 both
# examples/bd-example-extension-go and examples/library-usage failed a plain
# `go build` on main with "updates to go.mod needed; to update it: go mod tidy".
# Examples are the first code a new user copies, so a broken one is a bad first
# five minutes.
#
# Usage: scripts/build-examples.sh
#
# Exits non-zero listing every module that failed, so one broken example does
# not hide another.
#
# Why `go vet` and not `go build`: with `-o <dir>/`, `go build ./...` compiles
# only the MAIN packages and silently skips every library package — a module
# with a broken internal package would pass. Without `-o` it litters the tree
# with binaries. `go vet ./...` type-checks every package INCLUDING test files
# (examples/library-usage/main_test.go exercises a lot of live API, and
# `go mod tidy` counts its imports, so a build that never compiles it leaves
# part of the recorded dependency graph unverified), writes no artifacts, and
# still fails on the stale-go.mod condition this script exists for. It also
# avoids `go build`'s "no main packages to build" false failure on a
# library-only example module.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" || exit 1
cd "$REPO_ROOT" || exit 1
# Canonical build settings (CGO_ENABLED, -tags=gms_pure_go via GOFLAGS). The
# examples link the same go-mysql-server that pulls in ICU under cgo, so they
# need the project tag exactly as the main build does. Guarded: with no errexit,
# a failed source would otherwise continue with GOFLAGS unset and silently
# type-check the ICU path instead, while check-build-tags.sh still passes (it
# only greps for the literal string `.buildflags`).
# shellcheck source=/dev/null
source ./.buildflags || { echo "build-examples: cannot source .buildflags" >&2; exit 1; }
# Bash 3.2 compatible (stock macOS ships 3.2, which has no `mapfile`), and no
# GNU-only `xargs -r` or `sort -z` (BSD sort has no -z; a sort stage would have
# emptied the pipeline on macOS — and `git ls-files` output is already sorted).
# NUL-delimited so a path containing whitespace cannot be split —
# `git ls-files | xargs -n1 dirname` turned "examples/has space/go.mod" into
# two bogus entries.
modules=()
while IFS= read -r -d '' f; do
modules+=("$(dirname "$f")")
done < <(git ls-files -z 'examples/*/go.mod')
if [[ "${#modules[@]}" -eq 0 ]]; then
# Not a pass. Either examples/ was restructured, or this is running outside
# a git checkout (a release tarball), and a job that compiles nothing must
# not report success.
echo "build-examples: found no example modules (git ls-files 'examples/*/go.mod' was empty)" >&2
echo "build-examples: refusing to report success having checked nothing" >&2
exit 1
fi
failed=()
for mod in "${modules[@]}"; do
echo "==> checking $mod"
if ( cd "$mod" && go vet ./... ); then
echo " ok"
else
echo " FAILED" >&2
failed+=("$mod")
fi
done
if [[ "${#failed[@]}" -eq 0 ]]; then
echo "build-examples: ${#modules[@]} example module(s) type-check cleanly"
exit 0
fi
{
echo
echo "build-examples: ${#failed[@]} of ${#modules[@]} example module(s) failed:"
printf ' %s\n' "${failed[@]}"
echo
echo "If the error is \"updates to go.mod needed\", the example's recorded"
echo "dependency graph has drifted from the parent module — usually because a"
echo "change to the root go.mod was not mirrored into the examples. Fix with:"
echo
for mod in "${failed[@]}"; do
echo " (cd $mod && go mod tidy)"
done
echo
echo "then commit the resulting go.mod/go.sum changes."
} >&2
exit 1