Files
gastownhall__beads/scripts
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
..

Beads Scripts

Utility scripts for maintaining the beads project.

ci/

Repository-owned CI command wrappers. These scripts are the source of truth for the target CI tiers; Make targets are aliases for local discoverability.

make ci-pr-core
make ci-pr-policy
make ci-pr-lint
make ci-package-mcp
make ci-package-npm

Each wrapper auto-detects the repository root, sources .buildflags when it invokes Go in the default build mode, and records per-command timing through scripts/ci/lib/timing.sh.

Broad Go test wrappers also source scripts/ci/lib/test-env.sh, which creates a temporary HOME/XDG/Dolt root, isolates Git global/system config, clears runtime Beads/Dolt environment variables, and sets BEADS_TEST_SKIP=dolt before tests run. This keeps local make test and make ci-pr-core results comparable to the fast PR-core contract even on shared agent hosts. Set BEADS_TEST_ENV_RUN_DOLT=1 only when intentionally running the Dolt-dependent tests through these broad wrappers, or BEADS_TEST_ENV_DISABLE=1 when debugging against your real local configuration.

The broad Go wrappers also cap package and test parallelism to 4 by default (GO_TEST_PKG_PARALLEL and GO_TEST_PARALLEL). This avoids turning high-core shared hosts into a different test topology than GitHub Actions.

make ci-pr-policy includes scripts/check-testing-short.sh, which enforces that testing.Short() is only used for runtime, stress, or large-fixture skips. Use build tags, environment checks, or named wrappers for integration/e2e/API boundaries.

Package gate wrappers validate publishable/package-adjacent surfaces:

  • make ci-package-mcp builds or consumes a bd binary, puts it on PATH as bd, then runs locked MCP package uv sync, Ruff, mypy, pytest, and build checks.
  • make ci-package-npm builds or consumes the native binary expected by npm-package/bin/bd, runs the npm package test suite, and checks npm pack --dry-run.

Set BEADS_TEST_BD_BINARY=/path/to/bd for MCP and npm package gates to reuse a prebuilt candidate binary instead of rebuilding it inside the wrapper.

pr-preflight.sh

Read-only PR safety check for agents and maintainers.

# Before implementing or opening a related PR
./scripts/pr-preflight.sh --search "topic keywords" --repo gastownhall/beads

# Before changing, closing, or merging an existing PR
./scripts/pr-preflight.sh 123 --repo gastownhall/beads

It reports contributor/fork status, draft/review/merge/check state, risky diff signals such as .beads/ changes or missing tests, and the required contributor-protection next steps. It does not replace code review or local validation.

Base-branch health is supplemented by a warn-only PR-gate sample: the last 60 completed pull_request-event runs, grouped per workflow. A single workflow whose decisive runs (>= 5, across >= 3 distinct head branches) are all failure-class is reported as a broken PR gate — the case where a job that exists only in the PR workflow is red for every PR while the base branch shows green. This detector never blocks (deliberately: automation classifies unrecognized [block] lines as genuine merge blockers and would park merge lanes on a false positive); it is skipped entirely while the base branch is red.

gh-body-lint

Lint Markdown files before posting them with gh ... --body-file.

./scripts/gh-body-lint body.md
./scripts/gh-body-lint --fix body.md

The lint catches literal \n sequences, which render poorly on GitHub, and GH#123 references, which do not auto-link like #123 or owner/repo#123.

release.sh ( The Easy Button)

One-command release from version bump to local installation.

Usage

# Full release (does everything)
./scripts/release.sh 0.9.3

# Preview what would happen
./scripts/release.sh 0.9.3 --dry-run

What It Does

This master script automates the entire release process:

  1. Stops running Dolt servers (avoids version conflicts)
  2. Runs tests and linting
  3. Bumps version in all files
  4. Commits and pushes version bump
  5. Creates and pushes git tag
  6. Verifies or opens the Homebrew core formula PR
  7. Upgrades local Homebrew installation
  8. Verifies everything works

After this script completes, your system is running the new version!

Examples

# Release version 0.9.3
./scripts/release.sh 0.9.3

# Preview a release (no changes made)
./scripts/release.sh 1.0.0 --dry-run

Prerequisites

  • Clean git working directory
  • All changes committed
  • golangci-lint installed
  • Homebrew installed (for local upgrade)
  • Push access to gastownhall/beads

Output

The script provides colorful, step-by-step progress output:

  • 🟨 Yellow: Current step
  • 🟩 Green: Step completed
  • 🟥 Red: Errors
  • 🟦 Blue: Section headers

What Happens Next

After the script finishes:

  • GitHub Actions builds binaries for all platforms (~5 minutes)
  • PyPI package is published automatically
  • Homebrew core formula is verified or tracked through its canonical PR
  • Users can brew upgrade beads to get the new version after Homebrew merges
  • GitHub Release is created with binaries and changelog

bump-version.sh

Bumps the version number across all beads components in a single command.

Usage

# Show usage
./scripts/bump-version.sh

# Update versions (shows diff, no commit)
./scripts/bump-version.sh 0.9.3

# Update versions and auto-commit
./scripts/bump-version.sh 0.9.3 --commit

What It Does

Updates version in all these files:

  • cmd/bd/version.go - bd CLI version constant
  • plugins/beads/.claude-plugin/plugin.json - Claude plugin version
  • plugins/beads/.codex-plugin/plugin.json - Codex plugin version
  • .claude-plugin/marketplace.json - Claude marketplace plugin version
  • integrations/beads-mcp/pyproject.toml - MCP server version
  • README.md - Alpha status version
  • PLUGIN.md - Version requirements

Features

  • Validates semantic versioning format (MAJOR.MINOR.PATCH)
  • Verifies all versions match after update
  • Shows git diff of changes
  • Auto-commits with standardized message (optional)
  • Cross-platform compatible (macOS and Linux)

Examples

# Bump to 0.9.3 and review changes
./scripts/bump-version.sh 0.9.3
# Review the diff, then manually commit

# Bump to 1.0.0 and auto-commit
./scripts/bump-version.sh 1.0.0 --commit
git push origin main

Why This Script Exists

Previously, version bumps only updated cmd/bd/version.go, leaving other components out of sync. This script ensures all version numbers stay consistent across the project.

Safety

  • Checks for uncommitted changes before proceeding
  • Refuses to auto-commit if there are existing uncommitted changes
  • Validates version format before making any changes
  • Verifies all versions match after update
  • Shows diff for review before commit

sign-windows.sh

Signs Windows executables with an Authenticode certificate using osslsigncode.

Usage

# Sign a Windows executable
./scripts/sign-windows.sh path/to/bd.exe

# Environment variables required for signing:
export WINDOWS_SIGNING_CERT_PFX_BASE64="<base64-encoded-pfx>"
export WINDOWS_SIGNING_CERT_PASSWORD="<certificate-password>"

What It Does

This script is called automatically by GoReleaser during the release process:

  1. Decodes the PFX certificate from base64
  2. Signs the Windows executable using osslsigncode
  3. Timestamps the signature using DigiCert's RFC3161 server
  4. Replaces the original binary with the signed version
  5. Verifies the signature was applied correctly

Prerequisites

  • osslsigncode installed (apt install osslsigncode or brew install osslsigncode)
  • EV code signing certificate exported as PFX file
  • GitHub secrets configured:
    • WINDOWS_SIGNING_CERT_PFX_BASE64 - base64-encoded PFX file
    • WINDOWS_SIGNING_CERT_PASSWORD - certificate password

Graceful Degradation

If the signing secrets are not configured:

  • The script prints a warning and exits successfully
  • GoReleaser continues without signing
  • The release proceeds with unsigned Windows binaries

This allows releases to work before a certificate is acquired.

Why This Script Exists

Windows code signing helps reduce antivirus false positives that affect Go binaries. Kaspersky and other AV software commonly flag unsigned Go executables as potentially malicious due to heuristic detection. See docs/reference/antivirus.md for details.


Future Scripts

Additional maintenance scripts may be added here as needed.