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
| Subcommand | Invoked by |
|---|---|
stamp <version> | release.yml publishing jobs; pages.yml |
publish-release | release.yml release job |
check | ci.yml version-guard |
check-release-gates | ci.yml release-gate-guard |
check-release-smoke | ci.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-rotations | secret-rotation-reminder.yml |
record-rotation <t> <d> | record-secret-rotation.yml |
merge-coverage -o <o> <p> | ci.yml test job |
test-summary | ci.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-assets | pages.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:
- 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. - Add a subcommand to
cmd/mdsmith-release/main.go. Parse arguments and call the function. Match the existing pattern: pflag withContinueOnError, aUsageclosure, andreportErrorfor exit codes. - Add Go tests under
internal/release/. Pure functions get table-driven tests. Filesystem logic gets at.TempDir()fixture. - 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.