Release Tooling Architecture

July 1, 2026 · View on GitHub

mdsmith-release is the single Go binary the GitHub Actions workflows lean on for runtime logic. The rule is short.

The rule

Workflow steps that need runtime logic invoke go run ./cmd/mdsmith-release <subcommand>. They do not run inline scripts in TypeScript, Python, or any other interpreter.

The CLI is not part of the user-facing mdsmith binary. Its subcommands are release-pipeline plumbing and have no value outside the workflow.

Why one binary

Every alternative was tried at least once.

Inline shell is fine for one-liners. It collapses under conditionals, date math, or YAML parsing.

Per-language helpers (TS via bun, Python) each add a runtime to set up. Each adds a lockfile, a config, a CI job, and a Codecov flag. The repo accumulated three coverage flags before this rule was written.

A second Go binary splits the build matrix. It invites code drift between two internal/release consumers.

A single Go binary co-located with the rest of the release tooling wins on every axis. One go.mod, one test suite, one coverage flag, one workflow setup step.

Subcommands

SubcommandInvoked by
stamp <version>release.yml publishing jobs; pages.yml
publish-releaserelease.yml release job
checkci.yml version-guard
check-release-gatesci.yml release-gate-guard
check-release-smokeci.yml release-gate-guard
build-npm <art> <out>release.yml npm job
build-wheels <art> <out>release.yml pypi job
build-flatpak <art> <out>release.yml flatpak job
package-obsidian <d> <o>ci.yml + release.yml obsidian jobs
sync-docs <src> <dst>composed by build-website
build-website [flags]pages.yml deploy job
check-secret-rotationssecret-rotation-reminder.yml
record-rotation <t> <d>record-secret-rotation.yml
merge-coverage -o <o> <p>ci.yml test job
test-summaryci.yml test job
bench [workdir]benchmark.yml record; release.yml benchmark-publish; run.sh
bench-check <base> <fresh>release.yml benchmark-publish + bench-regression-gate
render-bench-page <out>release.yml benchmark-publish
pull-site-assetspages.yml deploy job
sync-messaging [--check]ci.yml messaging-drift; local sync
sync-channels [--check]ci.yml channels-drift; local sync

Each subcommand lives under cmd/mdsmith-release/. It delegates to a function in internal/release/. The split keeps argument parsing thin. The logic rides the same go test suite as the rest of the package.

What goes where

mdsmith is for content operations. Any command a contributor runs locally lives here.

mdsmith-release is for pipeline plumbing. Only CI runs these. The layout and env are baked in.

For new logic, ask: does a contributor ever run this locally? If yes, it goes in mdsmith. If no, in mdsmith-release.

Channel source of truth

Every list of distribution channels comes from one directory, docs/development/release-channels/. That covers the install-guide table, the website install picker, the release table, the README, and the feature card. There is one file per channel. The frontmatter is the data; the body is documentation.

sync-channels projects that frontmatter into website/data/channels.yaml, the Hugo data file the homepage install picker reads. That file is generated: edit the channel frontmatter, run go run ./cmd/mdsmith-release sync-channels, and never hand-edit channels.yaml. The channels-drift CI job runs sync-channels --check and fails on any hand edit. The install-guide table is a <?catalog?> over the same files, regenerated by mdsmith fix.

The platforms frontmatter field drives the picker's filter chips. List every audience a channel serves. That means the community it belongs to (go, node, python, editor) and every OS it actually runs on (linux, macos, windows).

A cross-platform CLI channel carries both. Take pip install mdsmith: its tags are [python, linux, macos, windows]. So it shows up for a visitor who filters by Python and for one who filters by Windows. Tag a channel by community alone and it drops out of every OS filter. That was the bug that hid go, npm, and pip from anyone filtering by Windows.

Adding a new workflow surface

The pattern:

  1. Add the function under internal/release/. Pure logic gets its own file. Orchestrators that shell out (e.g. gh) keep the command injectable so tests can swap in a fake.
  2. Add a subcommand to cmd/mdsmith-release/main.go. Parse arguments and call the function. Match the existing pattern: pflag with ContinueOnError, a Usage closure, and reportError for exit codes.
  3. Add Go tests under internal/release/. Pure functions get table-driven tests. Filesystem logic gets a t.TempDir() fixture.
  4. Update the workflow YAML to invoke go run ./cmd/mdsmith-release <subcommand>. Drop any prior interpreter setup steps.

The result is one less language in the repo and one less CI job to keep green.

Test fixtures

buildnpm_test.go asserts the matrix matches the canonical npm-channel doc. The bullet list under release-channels/npm.md is the source of truth.

secretrotations_test.go uses t.TempDir() files. A shell stub stands in for the gh binary. No live API calls run in tests.

The pattern for any new subcommand: pure logic plus a small fake for each external command.