Release trains: how seven repos ship independently
Runlog ships across seven repos with independent release trains. SemVer pinning with component-scoped tags decouples consumer migration from schema evolution and keeps the cross-repo coordination tax close to zero.
This is the fifth and final post in the Inside Runlog series. The previous posts covered the registry design, verification, sanitization, and the agent client contract. This post is about the engineering discipline behind shipping: how the seven components that make up Runlog each cut releases on their own cadence, and why the tag convention that makes this work is stricter than it first appears.
Seven repos, one system
Runlog is not a monorepo. The seven components — the MCP server, the signed verifier, the submission schema, the domain vocabularies, the client skills, the website, and the architectural docs — are separate repositories under runlog-org. They are related by dependency, not by version: the verifier depends on the schema, the server depends on the schema and vocabularies, the skills depend on the server’s MCP tool signatures. What they do not share is a release clock.
The design forces this. The schema is consumed by the server, the verifier, and the skills. With a single shared version number, a schema-bumping PR cannot land until every consumer has been updated to handle the new shape — a multi-repo lockstep that scales badly with each new consumer added. Independent trains let the schema cut a release first; consumers pull the new version on their own cadence. The CI gate on each consumer repo re-validates against the schema version it is pinned to, so a consumer that drifts past what its pinned schema version supports surfaces as a localized CI failure rather than a silent runtime surprise. The coordination problem becomes a localized CI failure that one repo’s owner can resolve in their own time.
The same logic applies asymmetrically across the other components. Vocabularies are data; skills are instruction text; the website is content. All of them can and do change on timescales that have nothing to do with each other. Forcing coordinated releases would mean the fastest-moving components wait on the slowest, or the slowest components get pulled along by premature bumps.
The tag convention
The canonical tag shape for most components is <component>/v<x.y.z>.
The path scope is part of the convention because the seven repos share a flat tag
namespace from a release-engineering perspective: an aggregator watching every release,
a gh release list command across multiple repos, or a future consolidated
changelog all stay unambiguous when each tag self-identifies its component.
Two repos use plain v* instead of the path-scoped form:
runlog-schema and runlog-verifier. Both carry a
go.mod at the repository root. The Go module proxy resolves only tags of
the shape the module root expects; a path-scoped schema/v0.2.0 tag is
invisible to the proxy and consumers cannot go get against it. The
disambiguation benefit of path-scoped tags does not outweigh making the published Go
module unreachable, so these two repos use plain v* as their canonical
form. Every other component keeps the path-scoped convention.
Each component’s release workflow keys off its own prefix glob. A
skills/v0.2.0 tag triggers the skills workflow and nothing else; a
vocabularies/v0.3.1 tag triggers the vocabularies workflow and nothing
else. Cross-repo coupling lives in CI gates, not in version numbers.
Landing on a moving baseline
The convention did not land on an empty slate. Components were already at different points in their release history when the path-scoped tag discipline was introduced, which means the trains fall into three categories based on how the transition was handled.
Plain v* as canonical for runlog-schema
and runlog-verifier. Driven by the Go-module constraint, not by legacy
compatibility. Both workflows still accept the path-scoped shape as a soft-cut
concession from the brief window when path-scoping was the convention for every repo,
but new releases must use plain v*. The verifier’s pre-convention
v0.1.0 — the version every signed bundle currently in the wild
references — already had the canonical shape and needed no migration.
Soft-cut accepting both for runlog-vocabularies. Its
pre-convention v0.1.0 is what the production server currently pins.
Forcing a migration would break the pin on a flag day for no gain. The workflow
accepts both the legacy v* and the new vocabularies/v*
shapes; new releases use the prefixed form. The unprefixed shape stays accepted
indefinitely — keeping both costs nothing, and removing it would re-introduce
the flag-day risk that was just avoided.
Strict prefixed-only for runlog-skills and
runlog-website. These trains started fresh in M02 with no pre-existing
unprefixed tags to honor and no Go-module constraint. Their workflows match the prefix
glob only; an unprefixed push would be a no-op against the release workflow.
Prerelease behavior by component
Prerelease tags follow the same prefix convention with a semver suffix:
<component>/v<x.y.z>-rc<N>,
-beta<N>, or -alpha<N>. What the prerelease
does on publication differs by component, and the differences map directly to the
artefact’s trust profile.
Vocabularies, skills, and the website ship prerelease tags as GitHub prereleases. The artefacts are text and data; the worst case is that a consumer opts into “latest including prereleases” and pulls something incomplete. Default pinning ignores them, so teams that do not explicitly opt in are not affected. This matches the risk profile: these components change frequently and the cost of a partial update is low.
The verifier ships prerelease tags as GitHub drafts, not published prereleases. The verifier is a signed binary that becomes part of every signed-bundle trust path. An accidentally-public unverified prerelease would be worse than no prerelease at all: it would be a version that looks official, can be downloaded, but has not had its reproducibility confirmed. The draft state forces a maintainer to publish it explicitly after the check has been completed. The cost is a small amount of manual ceremony; the benefit is that nothing lands in the trust chain by accident.
The schema ships prerelease tags as GitHub prereleases but blocks them from the
PyPI publishing path. The Go module side has no equivalent gate — go
get against a prerelease tag works but is non-default — so the asymmetry
between the two ecosystems is accepted rather than papered over. Each component’s
RELEASING.md documents the exact behavior for its own artefact type.
Backwards compatibility across frozen clients
The release-train discipline interacts directly with the backwards-compatibility guarantee described in the MCP interface post. MCP client skills are installed once and are not auto-updated. Whatever tool calls a skill from any past install would make, the server must understand indefinitely. This is permanent by design — the alternative would require the server to be able to update local files on a user’s developer machine, which is not acceptable.
When a schema change is breaking, it ships as a new surface alongside the old one:
a new $id URI, a new schema file, a new version segment. The server
accepts any schema version it still understands. The old surface stays valid for at
least six months after the new one is announced, with a deprecation warning in the
_meta field of old-tool responses for the entire window. Removal requires
a migration note and visible call-count data showing that the long-tail trail-off has
happened.
This means a breaking schema change can accumulate locally on the schema repo’s main branch without touching any consumer. The schema cuts a new release when it is ready. Each consumer bumps its pin on its own schedule, behind its own CI gate. Nothing is held hostage; nothing breaks silently.
What this enables
The release-train framework is the operational foundation for M02, the milestone that establishes Runlog’s operational discipline at scale. When the schema can evolve without blocking the skills, and the skills can update without touching the server, and the server can deploy without coordinating with the verifier, the system can sustain a faster development cadence without accumulating coordination debt.
The two repos deliberately excluded from release-train discipline — the server and the architectural docs — are excluded for concrete reasons. The server deploys continuously from its main branch; versioned releases would add ceremony without adding traceability for a component with no pinning consumers. The docs are architectural prose; there is no consumer that pins documentation, and the published artefact is already the version at main. Both exclusions are by design and both have an explicit upgrade path: if either ever grows a versioned consumer, it joins the convention with its own prefix.
That is the Inside Runlog series. The five posts cover the registry design, the verification mechanism, the sanitization pipeline, the agent client contract, and the release engineering. The blog index has the full list.
Notes by Volker Otto. Comments and corrections welcome at runlog@volkerotto.net.