syncver, relcore, gomodThree tools covering one arc: keep the version string consistent
everywhere it's written, run a release as a sequence of journaled,
resumable steps rather than one big script that has to succeed end to end
or leave you guessing where it stopped, and — for Go projects specifically
— catch a class of go.mod mistake that only breaks for whoever isn't the
machine that wrote it.
syncver: one version, everywhere it needs to appearversion_targets in .repoman.json names every file (and the regex to
find the version string inside it) that has to agree with VERSION:
$ repoman syncver show
version: 0.1.0 sync: ok
$ repoman syncver bump-minor
version set to 0.2.0
bump-minor writes VERSION and every configured target in one motion.
bump-patch and bump-major work the same way; set <version> sets an
exact version directly; check verifies everything still agrees and exits
non-zero if it doesn't — the check to wire into CI or a pre-release gate.
show and check both take --format (repoman-055-format.md) for
scripted consumption — a per-target breakdown, not just the flattened
message the default text form shows:
$ repoman syncver show --format json
{
"tool": "syncver",
"object": "syncver-status",
"schema_version": 1,
"data": {
"version": "1.4.2",
"in_sync": false,
"targets": [
{"file": "docs/README.md", "value": "1.4.0", "match": false}
]
},
"generated_at": "2026-08-30T14:00:00Z"
}
check --format json's exit code carries the same meaning as the
default text form's — non-zero when out of sync — so it drops into a CI
step exactly like check always has, just with a real payload alongside
the exit code instead of only a status line.
relcore: a release is a sequence of steps, not one script.repoman.json's release.steps list names each step as either a
builtin (syncver, archive) or a run shell command, and marks it
always (re-runs every time, resumed or not — syncver should be one of
these) or resumable (journaled; skipped on --resume if it already
succeeded).
A run where a step genuinely fails halts cleanly, rather than continuing past a broken step:
$ repoman relcore 0.2.0
relcore 0.2.0 at rel_doc2 (log: release-0.2.0.log)
-- sync
ok sync (0s)
-- build
FAIL build: exit 1; see the release log
run halted; fix and re-run with --resume
Fix whatever actually broke, then resume — steps already recorded as succeeded aren't re-run; the ones that weren't, are:
$ repoman relcore 0.2.0 --resume
relcore 0.2.0 at rel_doc2 (log: release-0.2.0.log, resuming)
-- sync
ok sync (0s)
-- build
ok build (0s)
-- archive
ok archive (0s)
release v0.2.0 prepared (0s)
sync re-ran both times — it's marked always. build ran once, failed,
then ran again and succeeded on resume, because a failed step was never
recorded as done. Had build succeeded the first time, --resume would
have skipped straight past it to whatever came next.
The archive builtin packages release.archive.sources into a
<project>-v<version>-checkpoint.zip, with an auto-generated
MANIFEST.sha256 and any release.archive.exclude patterns genuinely left
out — not just expected to be absent. Its own exclusion list also covers
its own generated output (MANIFEST.sha256, .release-state.json,
release-*.log) by name specifically because a stale copy of one of those
sitting in the source tree once got swept in as an ordinary file and
produced a duplicate entry — see repoman-020-failure-modes.md #7.
Never pipe relcore's own output through head/tail/grep before
checking its exit code. The log file exists for reading afterward; the
exit code is the only thing to trust in the moment — see
repoman-020-failure-modes.md #3 for what happens when that rule gets
skipped under debugging pressure.
relcore's mandatory pre-flight: badcode and provenance, before any step runsBefore release.steps is even read — not a step itself, not resumable,
not skippable by --resume — relcore runs two gates unconditionally:
badcode check (repoman-065-badcode.md) against the whole tree, and a
provenance check (T-03, repoman-088-provenance.md) confirming every
journal-tracked file still matches what repoman itself last wrote. Both
show up in the output even on a clean run, so a real pass is visibly
distinct from "nothing to check yet":
$ repoman relcore 0.1.0
-- badcode (mandatory pre-flight, not skippable)
ok badcode (3 pattern(s) checked)
-- provenance (mandatory pre-flight, not skippable)
ok provenance (no out-of-band edits detected)
relcore 0.1.0 at myproject (log: release-0.1.0.log)
-- sync
ok sync (0s)
release v0.1.0 prepared (0s)
An out-of-band edit — a journal-tracked file changed by something other
than ed/strreplace since repoman last touched it — blocks the entire
release, the same way a badcode match does, with no release.steps
entry ever reached:
$ echo "tampered" > tracked.txt
$ repoman relcore 0.2.0
-- badcode (mandatory pre-flight, not skippable)
ok badcode (3 pattern(s) checked)
-- provenance (mandatory pre-flight, not skippable)
ERROR provenance-mismatch: tracked.txt changed outside repoman since 2026-09-10T02:15:26Z -- recorded 428c84d4521e..., now 92e78d0b0329...
FAIL provenance: 1 mismatch(es) -- this gate has no override; run 'repoman provenance sanction FILE --reason "..."' for each one first, then re-run
There is no flag to bypass either gate. repoman provenance sanction
FILE --reason "..." (accepting the current content as the new
known-good state, with a mandatory audit trail) is the only way through
a real mismatch — exactly the same shape as fixing a badcode match by
removing the matched content. Both pre-flights re-run identically on
--resume, because neither is part of the resumable-steps journal at
all; there is nothing for --resume to skip.
gomod: a go.mod/go.sum sanity gateBuilt specifically for one incident shape: a replace directive pointing
at an absolute local filesystem path — something that only exists on the
machine that wrote it — reaching a committed go.mod. gomod check fails
on exactly that:
$ repoman gomod check .
GOMOD CHECK OK
$ echo 'replace example.com/internal-lib => /home/dev/internal-lib' >> go.mod
$ repoman gomod check .
ERROR replace-absolute-path: `replace example.com/internal-lib => /home/dev/internal-lib` is an absolute local filesystem path -- it will not exist on any machine but the one that wrote it. Remove before release.
GOMOD CHECK FAIL: 1 error(s)
A relative-path replace (./foo, ../foo) is a legitimate monorepo
pattern, so it only warns by default — --strict-relative-replace
promotes that to a failure too, for projects that want the stricter rule.
A separate, best-effort check catches an incomplete go.sum via go
list's own dependency resolution (never go build, so a CGO package
missing system libraries can't be mistaken for a go.sum problem) — and
degrades to a warning rather than a false failure when the environment
itself can't finish the check (no network, no go on PATH), rather than
either silently skipping or falsely failing.
Wire it into a project's own relcore steps as an ordinary run step:
{"name": "go-sanity", "run": "repoman gomod check", "always": true}
check --format json (repoman-055-format.md) turns the same finding
into a structured payload — errors/warnings as real arrays a CI
system can act on individually, not lines to re-parse:
$ repoman gomod check --format json
{
"tool": "gomod",
"object": "gomod-check",
"schema_version": 1,
"data": {
"toolchain_available": true,
"errors": [
"replace-absolute-path: `replace example.com/internal-lib => /home/dev/internal-lib` is an absolute local filesystem path -- it will not exist on any machine but the one that wrote it. Remove before release."
],
"warnings": [],
"ok": false
},
"generated_at": "2026-08-30T14:00:00Z"
}
The toolchain-unavailable soft-pass is visible in the payload too
("toolchain_available": false, "ok": true) rather than only as an
absence of output — a consumer can tell "nothing to check here" apart
from "checked, and it's clean" without re-deriving it from silence.
A full release gate — see repoman-060-register-and-guards.md for the
register and dormant-guard half of this — checks all of it at once: version
strings in sync, the register clean of closed items, every dormant guard
current or its skip recorded, and, for Go projects, gomod check clean.
relcore itself never needs remembering to run badcode/provenance —
both are unconditional pre-flights baked into relcore regardless of
what any release-hygiene script does or does not check. Where a
release-hygiene script exists for a project, the rest of these checks
belong in it rather than in anyone's memory of the steps.