SPEC-0027: Release, Version Reporting and Upgrade Contract
SPEC · SPEC-0027 · Status · approved · Date · 2026-09-22 · Implements · ADR-0032 · Requires · SPEC-0005, SPEC-0012, SPEC-0014
Overview
Switchboard reports one build-stamped version on every surface that a human or an agent touches. Each release ships a human-written CHANGELOG section. Each breaking change ships an upgrade note. Until 1.0, superseded surfaces are removed outright, with no deprecation window or transition warning. The docs say which release they describe, and mark what is not released yet. See ADR-0032.
This spec also defines the v0.3.0 release as the first release under the contract. It is the
first to report its own version over MCP, and the first with a CHANGELOG and an upgrade guide for
the shared-receiver removal.
Requirements
REQ-1: Build Information
A single package, internal/buildinfo, MUST expose Version, Commit and Date. These MUST be
set at build time with -X github.com/stump-wtf/switchboard/internal/buildinfo.<Var>=… by the
Makefile, deploy/docker/Dockerfile and .goreleaser.yaml. main.version MUST be removed.
When no ldflags were applied, buildinfo MUST fall back to runtime/debug.ReadBuildInfo():
VersionMUST be the main module's version when it is not(devel), otherwisedev;CommitandDateMUST come from thevcs.revisionandvcs.timesettings when present, otherwise be empty.
Since Go 1.24, go build in a git checkout sets the main module's version from VCS: the tag when
HEAD is exactly a release tag, otherwise a pseudo-version, with +dirty for a modified tree. The
fallback reports that version as it is. The goreleaser build MUST pass -buildvcs=false, so a
release whose -X stamp is missed reports dev rather than the VCS-derived tag, and REQ-13's
assertion can catch it.
Packages other than internal/buildinfo MUST NOT hold a version literal for Switchboard itself. A test MUST fail if a string
literal matching ^v?\d+\.\d+\.\d+ is assigned to an identifier containing version in
internal/mcp or internal/web. Protocol versions, such as the A2A protocol version, are exempt
by name. So are versions of something other than Switchboard, such as the version a persona's
A2A agent card advertises (cardVersion): that is the persona's version, not the build's.
Scenario: Release build
- WHEN goreleaser builds tag
v0.3.0 - THEN
buildinfo.Versionisv0.3.0,Commitis the tagged commit, andDateis its commit date
Scenario: go install
- WHEN a user runs
go install github.com/stump-wtf/switchboard/cmd/switchboard@v0.3.0with no ldflags - THEN
switchboard versionreportsv0.3.0
Scenario: Plain local build
- WHEN a developer runs
go build ./cmd/switchboardin a clean checkout whose HEAD is not a release tag - THEN
Versionis Go's pseudo-version for that commit (for examplev0.3.1-0.20260923185201-4369412033b4), andCommitis the checkout's revision
Scenario: Build without VCS information
- WHEN a developer runs
go build -buildvcs=false ./cmd/switchboard, or builds outside a checkout with no ldflags - THEN
Versionisdev, andCommitandDateare empty
REQ-2: MCP Server Version and Session Instructions
The MCP initialize result MUST carry serverInfo.version = buildinfo.Version.
The session instructions MUST begin with one line, before the existing doorbell contract text:
switchboard <Version> (built <YYYY-MM-DD>), where the date is Date formatted as
YYYY-MM-DD. When Date is unknown, the parenthetical MUST be omitted.
Scenario: Client sees the real version
- WHEN a client initializes against a
v0.3.0server - THEN
serverInfo.versionisv0.3.0, and the instructions' first line isswitchboard v0.3.0 (built 2026-09-2x)
REQ-3: Staleness Warning
When buildinfo.Date is known, Version is not dev, and Date is more than 90 days before the
server's current time, the session instructions MUST add, directly after the version line:
This Switchboard build is more than 90 days old. Features described in current docs may be missing. Tell your human to check for an upgrade.
The web footer and switchboard doctor MUST show an equivalent notice. The check MUST be offline:
no network access is required.
Scenario: Old build
- GIVEN a server whose
Dateis 120 days ago - WHEN a session initializes
- THEN the instructions contain the staleness line, and the footer shows the notice
Scenario: Dev build exempt
- GIVEN a
devbuild with aDate200 days ago - WHEN a session initializes
- THEN no staleness line appears
REQ-4: Opt-In Release Check
When the operator sets SWITCHBOARD_RELEASE_CHECK=1, and not otherwise, the server:
- MUST fetch
https://api.github.com/repos/stump-wtf/switchboard/tags?per_page=100at most once every 24 hours, plus once at startup. It MUST follow theLink: rel="next"pagination, up to 10 pages, because the endpoint is paged and unsorted, and a single page cannot guarantee the highest tag; - MUST send the request with no credentials, a
user-agent: switchboard/<Version>header, and no instance data; - MUST cache the highest semver tag.
When the cached tag is greater than Version, the session instructions, the footer and
switchboard doctor MUST say <tag> is available. A failed check MUST be logged at debug level,
and MUST change no output. Without the setting, the server MUST make no request to any release
endpoint.
Scenario: Newer release available
- GIVEN
SWITCHBOARD_RELEASE_CHECK=1on av0.3.0server, and a public tagv0.4.0 - WHEN a session initializes after the check has run
- THEN the instructions say
v0.4.0 is available
Scenario: Default makes no request
- GIVEN the setting is absent
- WHEN the server runs for 48 hours
- THEN it makes no request to
api.github.com
REQ-5: Health Endpoint
GET /healthz MUST stay public and unauthenticated. On success it MUST answer 200 with the
plain-text body ok <Version>\n. When the request's Accept header prefers application/json, it
MUST answer {"status": "ok", "version", "commit", "date"}. On a database failure it MUST keep
answering 503 with no version information.
When SWITCHBOARD_HIDE_VERSION=1 is set, the body MUST be ok\n (or {"status": "ok"}), and the
footer MUST omit the version. MCP, the CLI and /metrics MUST still carry it, because they are
authenticated.
Scenario: Probe compatibility
- WHEN a load balancer checks that
/healthzreturns200and a body starting withok - THEN the check passes on both old and new servers
Scenario: Hidden version
- GIVEN
SWITCHBOARD_HIDE_VERSION=1 - WHEN an anonymous client requests
/healthzwithAccept: application/json - THEN the body is
{"status": "ok"}
REQ-6: Web Footer
The layout footer (internal/web/templates/layout.html, role="contentinfo") MUST show
Version, linked to that version's CHANGELOG section. The short commit MUST be available as
accessible hover text. The footer MUST show the REQ-3 and REQ-4 notices when they apply.
Scenario: Footer shows the version
- WHEN a signed-in human loads any page of a
v0.3.0server - THEN the footer shows
v0.3.0, linked to thev0.3.0CHANGELOG section
REQ-7: CLI Version Reporting
switchboard version MUST print the CLI's Version, Commit and Date. When the CLI holds live
credentials, it MUST also print the server's version from /healthz (JSON). switchboard status
MUST show both versions. When the two differ in major or minor version, status and doctor MUST
print a warning naming both. --json MUST emit {"cli": {…}, "server": {…} | null}.
Scenario: Skew warning
- GIVEN a
v0.4.1CLI logged in to av0.3.0server - WHEN the human runs
switchboard status - THEN the output warns that the CLI is
v0.4.1and the server isv0.3.0
REQ-8: Build Info Metric
The SPEC-0023 registry MUST gain switchboard_build_info{version,commit} 1. A version and a
commit are bounded per process, so they are acceptable labels under SPEC-0023 REQ-5.
Scenario: Scrape shows the build
- WHEN Prometheus scrapes a
v0.3.0server - THEN
switchboard_build_info{version="v0.3.0",commit="…"} 1is present
REQ-9: CHANGELOG
CHANGELOG.md MUST exist at the repository root in Keep a Changelog 1.1 format, with an
## [Unreleased] section and, per release, ## [vX.Y.Z] - YYYY-MM-DD, using only the headings
Added, Changed, Fixed, Security and Breaking.
A CI job named changelog MUST run on every pull request. It MUST fail when:
- the PR title's type is
feat,fix,secorperf, or the title contains!:; and - the diff does not add at least one line under
## [Unreleased]; and - the PR does not carry the
no-changeloglabel.
The job MUST be a required status check on main. Renovate PRs (chore(deps)) and docs,
toil, test and chore PRs MUST pass without a CHANGELOG change.
At release, the [Unreleased] entries MUST move to the new version's section, and the release's
notes (goreleaser release.body or equivalent) MUST be that section's text.
Scenario: Feature without an entry
- WHEN a PR titled
feat(mcp): add ack_doorbellchanges no line under[Unreleased] - THEN the
changelogcheck fails and names the missing entry
Scenario: Dependency bump
- WHEN Renovate opens
chore(deps): update module … - THEN the
changelogcheck passes
REQ-10: Breaking Changes and Upgrade Notes
A change MUST be treated as breaking when it:
- removes or renames an environment variable, an MCP verb, field or error code, an API route, or a CLI command or flag;
- changes a default that alters behaviour for an existing deployment;
- adds a migration that cannot be reversed (it drops or deletes data).
A breaking PR MUST add:
- a
### Breakingentry under[Unreleased]; - a section in
docs/guides/15-upgrading.mdunder a heading## Upgrading to <next version>(Unreleaseduntil the release), stating what breaks, who is affected, the steps to take, and how to verify.
An irreversible migration MUST be named in that section with a "back up first" instruction and the
pg_dump command.
A CI job named upgrade-note MUST fail a PR whose title contains !:, or whose diff adds a file
under internal/db/migrations/ containing DROP TABLE, DROP COLUMN or DELETE FROM, unless
the PR also changes docs/guides/15-upgrading.md and adds a ### Breaking CHANGELOG entry.
Scenario: Breaking PR without a note
- WHEN a PR titled
sec!: remove Xchanges neitherupgrading.mdnor the Breaking section - THEN the
upgrade-notecheck fails
Scenario: Irreversible migration
- WHEN a PR adds a migration containing
DROP TABLE adapters - THEN the
upgrade-notecheck requires anupgrading.mdsection, which names the migration and gives the backup command
REQ-11: Superseded Surfaces Are Removed Outright
Until v1.0.0, a change that supersedes one of Switchboard's own surfaces (an environment variable,
an instance setting, an MCP verb, field or error code, an API route, or a CLI command or flag) MUST
remove the superseded surface in the same change. It MUST NOT add a deprecation window, a
transition or retired-variable warning, an alias, a dual code path or any other back-compat shim for
it. The removal is breaking under REQ-10, so its ### Breaking entry and upgrade note MUST name the
removed surface and its replacement.
A retired environment variable MUST simply no longer be read. The server MUST NOT keep a table of retired variables or log about them. Migrating existing data to the new shape is not a shim and is unaffected by this requirement.
The v0.3.0 upgrade note MUST name SWITCHBOARD_GITHUB_SECRET, SWITCHBOARD_GITEA_SECRET,
SWITCHBOARD_STRIPE_SECRET, SWITCHBOARD_SLACK_SECRET and
SWITCHBOARD_LEGACY_RECEIVER_ENDPOINT_ID, all retired in v0.3.0.
Scenario: Old secret still set
- GIVEN
SWITCHBOARD_GITEA_SECRETis set in the environment of av0.3.0server - WHEN the server starts
- THEN it starts normally, does not read the variable, and the
v0.3.0upgrade note names the variable and thecreate_webhookreplacement
Scenario: Renamed verb ships without an alias
- WHEN a PR renames an MCP verb
- THEN the old name is gone from
tools/listin the same PR, and the PR carries a### Breakingentry and an upgrade-note section naming both names
REQ-12: Release-Honest Docs
Every docs-site page MUST show a banner, rendered at build time, that names the latest release tag
and the commit the site was built from. A build from an untagged commit MUST say that the docs
track main, and that features marked Unreleased are not in the latest release.
A docs section that describes behaviour added after the latest tag MUST carry an :::unreleased
admonition, which renders as "Unreleased: not in <latest tag>". The release procedure MUST
rewrite every :::unreleased marker to a "Since <version>" badge. The docs build MUST fail when it builds
a tagged commit that still contains :::unreleased.
The docs workflow MUST also build (without publishing) on every pull request that touches docs/
or docs-site/, so a broken page or a broken marker fails before merge.
Scenario: Docs from main
- WHEN the docs are built from
main, 12 commits afterv0.3.0 - THEN every page's banner names
v0.3.0and the build commit, and says the docs trackmain
Scenario: Leftover marker at release
- WHEN a tagged build contains an
:::unreleasedmarker - THEN the docs build fails and names the file
REQ-13: Release Verification
The release workflow MUST assert, for the built linux/amd64 binary:
- that
switchboard versionoutput contains the tag (the existing check); - that a server started against a throwaway database answers
/healthzwithok <tag>; - that an MCP
initializeagainst it returnsserverInfo.versionequal to the tag.
Each assertion MUST fail when the assertion itself cannot run, for example when the binary cannot execute or the server does not start.
Scenario: Stamp silently missed
- WHEN a release build's ldflags path is wrong, so
Versionstaysdev - THEN the release workflow fails before publishing
REQ-14: Release Cadence and the v0.3.0 Release
A release MUST be cut within 48 hours of merging any sec change, and SHOULD be cut whenever
main holds a user-visible change and the last release is more than 14 days old.
v0.3.0 MUST be the first release under this contract. It MUST include:
CHANGELOG.md, withv0.1.0,v0.2.0andv0.3.0sections;- the shared-receiver removal under Breaking;
- the ring-on-connect, delivery-id and board fixes under Added and Fixed;
- a
docs/guides/15-upgrading.mdsection forv0.3.0covering:- the four ignored
SWITCHBOARD_*_SECRETvariables andSWITCHBOARD_LEGACY_RECEIVER_ENDPOINT_ID; - migration
0021_drop_adaptersbeing irreversible, with the backup step; - moving every sender to
create_webhook, with a before and after; - the public
ghcr.io/stump-wtf/switchboard:latesttag now carrying these fixes. It moves only onv*tags, so untilv0.3.0it isv0.2.0.
- the four ignored
Scenario: v0.3.0 is complete
- WHEN
v0.3.0is tagged - THEN the release notes are the CHANGELOG
v0.3.0section, the upgrade guide has av0.3.0section, and the image and binaries reportv0.3.0over MCP and/healthz
REQ-15: Error Handling Standards
A failure to read build info MUST fall back as REQ-1 describes, and MUST NOT fail startup. A release-check failure MUST be logged at debug level with the error wrapped, and MUST NOT change any output (REQ-4). Nothing in this spec may fail a request because version information is unavailable.
Scenario: Release check times out
- GIVEN
SWITCHBOARD_RELEASE_CHECK=1andapi.github.comunreachable - WHEN the daily check runs
- THEN one debug line is logged, and the instructions and footer are unchanged
Security Requirements
Authentication
| Surface | Auth | Description |
|---|---|---|
GET /healthz | Public | Load-balancer and uptime probes must reach it unauthenticated. The version is public by design, and hidden by SWITCHBOARD_HIDE_VERSION |
MCP initialize and session instructions | Required | Endpoint credential (SPEC-0007) |
| Web footer | Required | Signed-in human (every layout page is behind auth.RequireHuman). The landing page footer follows SWITCHBOARD_HIDE_VERSION |
GET /metrics build-info series | Required | SPEC-0023 scrape credential |
Rate Limiting
/healthz stays outside the per-IP limiters, as it is today, for probes. Its body stays constant
and small. The release check makes at most one outbound request per 24 hours, plus one at startup.
Security Headers
Every surface keeps the existing secureHeaders middleware.
Request Body Size Limits
No new request bodies. The release check MUST read at most 1 MiB of the tags response.
CSRF Protection
No state-changing surface is added.
Redirect Validation
The release check MUST NOT follow redirects to a host other than api.github.com. Footer links MUST
be same-origin docs links, or the public release URL.
Outbound Calls
The only outbound call this spec adds is the opt-in release check. It MUST use HTTPS, carry no credentials, include no tenant or instance identifiers, and time out after 10 seconds.