0005 — Release pipeline, version single-sourcing, and the module path
number: 0005
status: proposed
date: 2026-10-29
issues: #1881 (this design) · split from triage 2774 · implementation is queue task 3114
code: 7d1b081c
mcpx has no release at all: no tags, no GitHub releases, no release workflow, and a version string hard-coded in four places. The install page’s channels are promises nothing can keep until a release exists. This note decides the shape — channel order, the tag and version scheme for a package that lives in a monorepo, the static-build policy, and the module-path question — so the implementation slices have one answer to build to. The module-path recommendation (§4) is the one item Drew may want to overrule; everything else follows from the code as it is.
1. Current state (verified at 7d1b081c)
- No tags and no releases on developing-today/code for mcpx; the only
mcpx workflow builds and tests (
mcpx.yml) plus vendorHash repair. - Version
0.1.0is hard-coded four times:package.nix(version =),package.nix(-X main.version=0.1.0), the fallbackvar version = "0.1.0"incmd/mcpx/main.go, andARG VERSION=0.1.0inContainerfile. - The module path was the last thing naming a repository that does not
exist: slice 3 below renamed it to
github.com/developing-today/code/pkgs/mcpx, where the code lives, and pointedmeta.homepageat the real tree. (Before the rename,go.moddeclared the legacy path andmeta.homepage404’d; the 328 Go files that imported by it moved with it in the same pass.) - A static build works: the SQLite driver is pure Go,
CGO_ENABLED=0cross-compiles, and a fully static Linux binary was verified in #182’s container work. The shipped artifacts are not static by choice:package.nixtakes the nixpkgs cgo default and the Containerfile setsCGO_ENABLED=1deliberately. - Unix only (
meta.platforms = lib.platforms.unix; untaggedSetpgid/Setsiduse). Release targets are therefore the flake’s three systems: x86_64-linux, aarch64-linux, aarch64-darwin. - The nix package is more than the binary: it generates the man page and
completions and wraps the binary with git/deno/bun/node on PATH. A bare
release binary has no JavaScript runtime;
mcpx run/execfail until the user installs one (mcpx doctorsays so). Every non-nix channel must state this in its install text.
2. Channel order
Build in dependency order; each channel consumes the one before it.
- GitHub release binaries + checksums. The foundation: three static
binaries (
mcpx-<version>-<system>.tar.gz) and a SHA256SUMS file on a GitHub release. Everything else downloads these. curl | shinstaller. Per-user by default (installs to~/.local/bin, states the JavaScript-runtime requirement at the end); a system-wide variant (sudo) documented, not default. The script verifies the checksum before installing — an installer that skips verification teaches users to pipe unverified binaries.- Container image. The existing Containerfile, published to GHCR by the same release workflow, version taken from the release (§3), not from a fifth copy of the string.
- Nix. Already works (
nix profile install …#mcpx); the release does not change it beyond the version source (§3).
Later, and not in the first slice: pwsh (needs Windows binaries, which need the untagged-syscall work), rpm/pkg (repackage the tarballs once they exist), the agent-prompt and just-the-plugin channels (they ride on 1–2). Flatpak is not built: a sandboxed mcpx cannot spawn the arbitrary servers, runtimes and binaries that are its entire function; the sandbox would have to be opened until the packaging is a lie. This records #1881’s recommendation against it as a decision.
3. Tags and the version’s single source
- Tag scheme:
mcpx/vX.Y.Zon developing-today/code. A path-prefixed tag is the monorepo-honest scheme: it cannot collide with another package’s releases in the same repository, and the release workflow triggers on exactly this prefix. (If §4’s recommendation is adopted, Go’s own subdirectory-module versioning wantspkgs/mcpx/vX.Y.Ztags for the module; the release workflow can cut both tags on the same commit — one for humans, one for the Go toolchain — rather than letting the Go scheme dictate the public one.) - Single source: a
pkgs/mcpx/VERSIONfile.package.nixreads it for both itsversionand its-X main.version=ldflag; the Containerfile takesVERSIONas a build argument whose workflow callers pass the file’s contents (the in-fileARGdefault becomes the file’s value at release time, and a comment in the Containerfile says the file is the source); the release workflow refuses to publish when the tag andVERSIONdisagree. cmd/mcpx/main.go’s fallback becomes a sentinel (0.0.0-dev), so a binary built without ldflags announces that fact instead of impersonating a release. The fallback is the one place a literal remains, and it is deliberately not a release number.
4. The module path — options and recommendation
The module path used to name a repository that does not exist, so
go install …@latest could not work and meta.homepage 404’d. Three
ways out were recorded:
- A — Create the repository the path names (dezren39/mcpx or
developing-today/mcpx) and move mcpx into it. Cleanest end state:
normal tags, normal
go install, its own Pages and releases. Cost: mcpx leaves the monorepo it shares tooling, CI and history with; the move itself is a project, and this repository’s other packages that reference mcpx paths must follow. This is Drew’s call, not a worker’s. - B — Rename the module to where the code lives:
github.com/developing-today/code/pkgs/mcpx.go install github.com/developing-today/code/pkgs/mcpx/cmd/mcpx@pkgs/mcpx/vX.Y.Zthen works from the monorepo, tags follow Go’s subdirectory scheme, andmeta.homepagepoints at the real tree. Cost: a mechanical rename across 328 Go files pluspackage.nixreferences — large diff, no design content, scriptable in one pass. If A is ever chosen later, the same mechanical rename runs again in the other direction; B does not close A’s door. - C — Keep the path, publish a release-only mirror. Releases are cut
to a mirror repository that carries the path’s name.
go installstays broken (the proxy resolves the module path, not the mirror’s releases), the homepage stays a lie, and two repositories must be kept in sync forever. Recorded for completeness; not recommended.
Decision: B, done in slice 3. It made every other part of this note true with one mechanical change and no new repository. A remains available whenever Drew wants mcpx to stand alone; nothing in B makes A harder.
5. Static-build policy for releases
Release binaries are CGO_ENABLED=0 static builds (the #182-verified
configuration). The caveat is recorded where the install page will read
it: on Linux, pure-Go os/user parses /etc/passwd instead of asking
NSS, so LDAP/SSSD users are not found, and the pure-Go resolver does not
honour nsswitch.conf. On macOS the flag changes nothing observable.
The nix package keeps its own (cgo) build: it targets managed systems
where NSS is the point, and it already exists.
6. Implementation slices (queue task 3114, in order)
- This design note.
- Version single-sourcing (§3): VERSION file, package.nix, Containerfile
build-arg, the
0.0.0-devfallback sentinel. - Rename slice (§4-B), done: module path, imports, package.nix references, meta.homepage.
- Release workflow: tag-triggered (
mcpx/v*) plusworkflow_dispatchsnapshot mode that builds the three artifacts and checksums without publishing; the snapshot run is the slice’s proof. - Installer script + a CI exercise (install a snapshot artifact into a
temp prefix;
mcpx --versionreports the release version). - Container publication wiring in the same workflow; update the #1878 install-page status column to what actually shipped.