A prerelease version must never reach a repository's default branch, so a tag cut from it can
never contain one. codefly ci prerelease is the check that enforces it, and this page is
why it exists, what it refuses, and how each repository wires it in.
The rule was a convention before it was a gate. It was written down in
obin-ai/platform-obin's docs/release.md and violated by two of six released modules anyway
— not through carelessness, but because the person releasing a module has no reason to read a
downstream composition's release document.
On 2026-09-30 six repositories were released and five module tags cut. Two of those tags turned out to contain prerelease agent pins:
| released tag | prerelease pin inside it |
|---|---|
codefly-dev/module-saas-starter v0.0.85 |
accounts, auth-gateway, telemetry → go-grpc = 0.1.48-dev.e87db5e08865 |
frontend, marketing → nextjs = 0.0.159-dev.1ed5001fd8b4 |
|
obin-ai/module-runtime v0.1.5 |
runtime/store → postgres = 0.0.139-dev.9785d12f3700 |
All three agents had already been released and verified — service-go-grpc v0.1.50, service-nextjs v0.0.162, service-postgres v0.0.141 — so the pins were stale as well as unreleased.
The consequence was not local. platform-obin's release step 2 requires dropping every
agent-overrides DEV PIN before it tags, on the stated basis that "a module released after the
agent fix pins the released agent itself". Because these modules did not, the composition could
not drop its overrides, and the fix was two more module release rounds. Each pin cost one line at
the pull request that wrote it.
Every one of those pins carried a comment saying to move it before tagging. A comment is not a gate.
Detection is the shape of the version, never the literal dev:
- a semver prerelease component —
0.1.48-dev.e87db5e08865,0.2.0-rc.1,1.0.0-alpha; - a Go pseudo-version —
v0.0.0-20260930123456-abcdef123456,v0.5.11-0.20260927230309-0c1b5db823d5— which is the same defect spelled differently.
Anything that is not an exact semantic version names no build and is left alone: latest is the
agent sentinel, ^0.0.1 and ~1.2 are the range constraints a library dependency carries.
Semver build metadata (1.2.3+build.5) is not a prerelease either.
Only in files git tracks. That is the point rather than an optimisation: the rule is about
what reaches the default branch, and what reaches it is what git tracks. It is also why the local
dev loop is free — codefly.local.yaml is gitignored, so it is outside the gate by construction
and needs no exception written into it.
| source | what is read | refused? |
|---|---|---|
every *.codefly.yaml |
every version: key, at any depth — a service's agent.version, a workspace's modules[].version and solutions[].version, a module's or library's own version |
yes |
workspace.codefly.yaml |
the top-level agent-overrides block, whose values are versions under an agent identity |
only unlabelled, or under --release |
go.mod |
first-party requires (github.com/codefly-dev/, github.com/obin-ai/) |
reported; refused with --go-modules |
The YAML scan walks for the version: key rather than reading a known schema, so a new
versioned field is covered the day it is added instead of the day somebody notices the gate
missed it.
testdata/ trees are skipped — a fixture's job can be to carry a bad pin, and this repository
tracks 138 *.codefly.yaml and go.mod files under testdata against one real go.mod.
--include-testdata reads them.
agent-overrides is the sanctioned prerelease carrier. codefly publish dev publishes an agent
build for iteration, codefly update workspace --agent-override <publisher>/<name>=<version>
writes it into the workspace, and
platform-obin/docs/release.md
documents that loop. A blanket "no - anywhere" would break it.
So the gate has two scopes:
| scope | command | agent-overrides |
everything else |
|---|---|---|---|
| default branch | codefly ci prerelease |
a prerelease is allowed with a label — a comment naming the issue it stands in for | refused |
| release | codefly ci prerelease --release |
refused, however well labelled | refused |
The release scope is what guarantees the property that actually broke. It is the enforcement of
release.md step 2 — "drop every dev override" — which nothing enforced before.
A label is a comment on the entry carrying an issue reference (codefly-dev/service-go#117, or
a bare #117). The condition for removal is prose no gate can check; the issue reference is the
half that can be, and the half that tells the next reader where to look. The convention already
writes both:
agent-overrides:
# DEV PIN, labelled: codefly-dev/service-go#117 — the `go` agent ran
# `go mod download` inside the container, where no credential exists for a
# private module. Removed before anything here is tagged (docs/release.md).
codefly.dev/go: 0.0.63-dev.bd71dd90cc10That was the stricter alternative, and it is the wrong trade for two reasons.
A dev agent build has to reach a shared environment, not just a laptop: platform-obin
deploys dev renders to staging, and a staging render reads committed config. codefly.local.yaml
is machine-local and gitignored, so moving the override there removes the ability to dogfood a dev
agent anywhere but one developer's machine.
And codefly.local.yaml carries only resolve: — module locations. It has no agent-override
carrier, so the stricter option is a Core change plus a CLI change plus a migration of a
documented workflow, for a property the release scope above already guarantees.
A dev loop that genuinely does not need to be committed should still use codefly.local.yaml.
The gate never looks there.
First-party Go pseudo-versions are pervasive in trees whose agent pins are perfectly clean. Measured on the four releases that were verified clean:
| release | first-party go.mod pseudo-versions |
|---|---|
obin-ai/module-robin v0.1.7 |
27 |
obin-ai/module-document-store v0.0.24 |
9 |
obin-ai/module-annotations v0.2.9 |
3 |
obin-ai/module-model-gateway v0.1.12 |
0 |
Refusing them by default would fail three of the gate's own clean fixtures, and many arrive as
// indirect entries that no change in the repository can move — they shift only when the
dependency that requires them releases. A gate that fails every repository in the fleet on the day
it lands is a gate that gets switched off in a week, so they are surfaced in the report and a
repository tightens at its own pace with --go-modules.
The check needs no workspace, no agent and no network: it reads committed text, so it runs in a fresh clone in milliseconds.
prerelease:
name: prerelease
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install codefly
run: | # however this repository already installs the CLI
curl -fsSL https://raw.githubusercontent.com/codefly-dev/cli/main/install.sh | sh
- name: No prerelease version reaches main
run: codefly ci prereleaseMake it a required check on the default branch: a gate that a pull request can merge past is
the convention again. Add --release to whatever cuts the tag, so a release cannot be built over
a labelled dev override:
codefly ci prerelease --release # before tagging--format json emits a schema-versioned report (schema_version, scope, status,
files_scanned, blocking[], allowed[]), each finding carrying its file, line, key, version,
kind, carrier, why it was refused and the remedy.
This repository holds itself to the rule through TestThisRepositoryPassesItsOwnGate in
pkg/prerelease, which rides the test gates already required on every pull request rather than
adding a job that would need a branch-protection change before it could bite.
Agents reach the same check over MCP as check_prerelease_versions, so an agent that writes a pin
can check its own edit before a human reviews it.
5 prerelease versions must not reach the default branch:
config (5) — a prerelease in committed configuration reaches the default branch, and any tag cut from it
module/services/accounts/service.codefly.yaml:12
agent.version = 0.1.48-dev.e87db5e08865 (prerelease-tag)
module/services/frontend/service.codefly.yaml:8
agent.version = 0.0.159-dev.1ed5001fd8b4 (prerelease-tag)
...
-> pin a released version — `codefly agent versions <publisher>/<name>` lists what is published, `codefly update workspace` moves the pin
-> or, for a dev agent build this composition has to run, move it to `agent-overrides` in workspace.codefly.yaml with a comment naming the issue it stands in for
-> a dev loop that does not have to be committed belongs in codefly.local.yaml, which is gitignored and outside this gate
Findings are grouped by the remedy they share and the remedy is stated once per group, because the locations are the part that differs. A passing run still lists what it permitted — an allowed prerelease is one somebody has to remove.
codefly publish dev— how a dev build is published and consumedcodefly update—--agent-overridewrites the committed override,codefly update service --agent-versionwrites a service pincodefly doctor workspace— lists every override in force (agent_override_active) and every service running a dev build (agent_dev_build)obin-ai/handbook#179— the operations page that says which command owns which operation. This is its enforcement half: the doc tells you, the gate stops you.