Release process¶
Type: Operational doc. Audience: maintainers cutting a re-frame2 release. Authority: the release workflow is the source of truth; this doc is its narrative companion.
re-frame2 ships as a coordinated set of Maven artefacts, all driven from a single repo-root VERSION file. This doc is the operational guide for how a release flows through CI, what gates exist, and how to recover when something goes wrong mid-deploy.
Two tiers ship from this repo and they move on separate triggers. The framework tier is the fourteen artefacts under implementation/, and a v* tag ships all of them together — that is what §Topological deploy DAG below describes. The tools tier is the jars under tools/, which share the same lockstep VERSION but ship on their own per-tool tags and are not touched by a v* tag at all; §The tools tier is their runbook.
Policy¶
The release pipeline reflects a small set of decisions Mike made up front. They are recorded here so future contributors don't have to re-derive them from the workflow.
- Mechanism — tag-triggered CD, modeled on re-frame v1. Push a tag matching the tag glob; the
releaseworkflow runs end-to-end. Same shape as re-frame v1'scontinuous-deployment-workflow.yml: tag-push trigger, gated test job,clojure -M:clein deploy,softprops/action-gh-releasefor the GitHub Release. The differences are structural — re-frame v1 ships one artefact, re-frame2 ships a coordinated set — and are documented in §Topological deploy DAG. The authoritative artefact roster is thereleaseworkflow's leaf declarations — thedeploy-leafmatrix plus the two post-matrix stages it deliberately excludes,deploy-ssr-ringanddeploy-fresco(§The inter-leaf edges); the enumerations below mirror them. - Channel gating — pre-1.0 = alpha/beta, post-1.0 = stable. Pre-1.0 releases tag as
v0.0.1.alpha(andv0.0.1.alpha-N/v0.0.2.alphaetc. for subsequent alphas; the same pattern with.betaonce we promote). Post-1.0 releases tag asvX.Y.Zper Semantic Versioning. The release workflow flags any tag containingbeta,alpha, orrcas a GitHubprereleaseautomatically. - First publish — manual cut, all artefacts together. Mike triggers the first
v0.0.1.alphadeploy by hand once the policy text and the workflow have been reviewed against an actual tag. After the first cut, subsequent releases run automatically on tag push. The first cut ships the full artefact set — core plus every leaf:schemas,reagent,reagent-slim,uix,machines,routing,flows,http,ssr,ssr-ring,resources,epoch,fresco; no artefact "comes later" — they all ship together at every release per the lockstep contract below. The compiled-view substratere-frame.uiis not in that set and never will be: Mike ruled on 2026-07-22 thatday8/re-frame2-uiis not to be published (rf2-a32r7), and on 2026-08-14 that both it andre-frame.freehandare retired and removed from the tree (rf2-0yp7w). - Atomic rollback — NOT POLICY. Clojars does not support yanking a published version, and re-frame2 does not invest in machinery that would make it look like it does. If a deploy fails part-way through, recovery is bump VERSION + re-tag + re-run (see §Recovery from a partial deploy for the procedure). The partial-release artefacts from a failed run remain on Clojars, tombstoned-by-supersession; consumers pin the bumped version and pull a coherent set. Manual recovery is acceptable; we do not build atomic-rollback or partial-deploy-replay machinery.
- Artefact set ships together at lockstep VERSION. Every framework artefact ships at every release at the same VERSION, sourced from the repo-root
VERSIONfile. The lockstep contract is enforced before any deploy by./.github/scripts/verify-version-lockstep.sh. Independent versioning is revisited post-1.0; until then, every published Maven coord moves in lockstep. The tool jars share that VERSION but not that trigger — they ship on per-tool tags at their own cadence, which is a difference of when, never of what version (see §The tools tier).
Tag format¶
| Channel | Tag pattern | VERSION file content | Example |
|---|---|---|---|
| Stable | vX.Y.Z |
X.Y.Z |
v1.0.0 ↔ 1.0.0 |
| Alpha | v0.0.1.alpha (or v0.0.1.alpha-N for recovery / increments) |
matches the tag minus the leading v |
v0.0.1.alpha-2 ↔ 0.0.1.alpha-2 |
| Beta | v0.0.1.beta (or v0.0.1.beta-N for recovery / increments) |
matches the tag minus the leading v |
v0.0.1.beta-2 ↔ 0.0.1.beta-2 |
| Pre-release (alpha / rc) | vX.Y.Z-alpha.N, vX.Y.Z-rc.N |
matches the tag minus the leading v |
v1.0.0-rc.1 ↔ 1.0.0-rc.1 |
The release workflow's tag glob is v[0-9]+.[0-9]+.[0-9]+*. Tag must match the contents of VERSION (prefixed with v); the release workflow's verify-version-lockstep job hard-fails if they disagree. The prerelease flag on the resulting GitHub Release is set automatically when the tag contains beta, alpha, or rc.
Trigger¶
Tag push. Push a tag matching the pattern above and the release workflow runs end-to-end:
There is no workflow_dispatch trigger by design: a release commit always carries an updated VERSION and a CHANGELOG entry, and the tag-push trigger keeps that coupling tight.
Topological deploy DAG¶
Per the lockstep-versioning policy (every artefact ships at the same version each release), the DAG reflects the published-pom dependency graph (which is much narrower than the in-repo test-classpath graph): every per-feature artefact's published :deps declares only day8/re-frame2 (core) — with two exceptions, ssr-ring and fresco, each of which also declares day8/re-frame2-ssr. Cross-feature references at runtime are otherwise wired through re-frame.late-bind per Conventions §Packaging conventions §Independence rule. Those two exceptions are real edges in the published graph, so the CI graph carries them as real edges too: see §The inter-leaf edges below.
graph TD
V[verify-version-lockstep] --> T[test]
T --> C[deploy-core]
C --> S[deploy-schemas]
C --> R[deploy-reagent]
C --> RS[deploy-reagent-slim]
C --> U[deploy-uix]
C --> M[deploy-machines]
C --> RT[deploy-routing]
C --> F[deploy-flows]
C --> H[deploy-http]
C --> SS[deploy-ssr]
C --> RES[deploy-resources]
C --> E[deploy-epoch]
S --> SR[deploy-ssr-ring]
R --> SR
RS --> SR
U --> SR
M --> SR
RT --> SR
F --> SR
H --> SR
SS --> SR
RES --> SR
E --> SR
S --> HI[deploy-fresco]
R --> HI
RS --> HI
U --> HI
M --> HI
RT --> HI
F --> HI
H --> HI
SS --> HI
RES --> HI
E --> HI
SR --> GR[github-release]
HI --> GR
The diagram shows the ordering constraints in reduced form. github-release actually lists deploy-core, deploy-leaf, deploy-ssr-ring and deploy-fresco in its needs:; the first two are reachable through either post-matrix stage anyway, since each already waits on the whole matrix. The two post-matrix stages are siblings rather than a chain — neither artefact appears in the other's published pom, so they run in parallel.
ASCII fallback:
verify-version-lockstep ──► test ──► deploy-core
│
│ ── deploy-leaf: ONE matrix job,
│ 11 values, all in parallel ──
├── deploy-schemas
├── deploy-reagent
├── deploy-reagent-slim
├── deploy-uix
├── deploy-machines
├── deploy-routing
├── deploy-flows
├── deploy-http
├── deploy-ssr
├── deploy-resources
└── deploy-epoch
│
┌───────────┴───────────┐
▼ ▼
deploy-ssr-ring deploy-fresco
└───────────┬───────────┘
▼
github-release
Why fan-out (not strict serial). A strict topological linearization would suffice; the deps-graph data is wider — every leaf but two has core as its only re-frame2 dependency, so the CI graph realises a valid topological sort that exploits the parallelism: the eleven independent leaves run concurrently after core, cutting wall-clock at the cost of a marginally wider failure surface (see Recovery below). The leaves group into per-feature artefacts (schemas, machines, routing, flows, http, ssr, ssr-ring, resources, epoch), the view layer — the three substrate adapters (reagent, the default; reagent-slim; uix) — and the Fresco view substrate, which is neither. The authoritative roster is always the deploy-leaf matrix in release.yml, plus the two leaves that job deliberately excludes, ssr-ring and fresco.
The view layer, as consumers meet it. The app template (tools/template/, day8/re-frame2-template) scaffolds against this released view layer through its substrate menu: :reagent (the default) and :uix. The menu is deliberately narrower than the adapter set — day8/reagent-slim is published but has no scaffold of its own, so a slim consumer starts from the Reagent variant and swaps the adapter coordinate. Adding a substrate to the deploy-leaf matrix does not automatically add a template variant; the two are decided separately.
The inter-leaf edges¶
ssr-ring and fresco are the exceptions, and the release DAG treats them as such. Each declares two in-repo coordinates in its published :deps — day8/re-frame2 and day8/re-frame2-ssr — and the workflow rewrites both to :mvn/version, so each published pom depends on a day8/re-frame2-ssr version that has to exist on Clojars. They reach it for different reasons: implementation/ssr-ring/deps.edn because the Ring host adapter sits on top of the SSR renderer, implementation/fresco/deps.edn because re-frame.fresco.server hands its payload to the SSR artefact's own fail-closed validator and egress projection rather than re-spelling those helpers locally. Neither depends on the other, so the two stages are siblings and run in parallel.
Two facts make those edges load-bearing on CI rather than merely descriptive. The deploy-leaf matrix runs fail-fast: false, and GitHub Actions cannot express an ordering edge between two values of one matrix — so while ssr-ring was a matrix value, a failed ssr value did not stop it deploying. And each job installs ssr into the runner's local ~/.m2 before packaging, so it never consults Clojars and cannot notice the miss itself. Since Clojars has no yank (see Recovery), the result would have been a permanent public artefact declaring a dependency that resolves to nothing: bump-and-supersede adds a good version but never removes the broken one.
So each ships in a stage of its own — deploy-ssr-ring and deploy-fresco — with needs: [deploy-core, deploy-leaf]. A needs edge onto a matrix job waits for every value to succeed, which is exactly the invariant the DAG needs:
If the
ssrleaf does not publish successfully, neitherssr-ringnorfrescopublishes at all.
Those edges are stronger than the poms require — each also blocks its artefact behind leaves it does not depend on — and deliberately so: under Recovery any leaf failure is resolved by bumping VERSION and re-shipping every artefact, so a leaf skipped because a sibling failed was going to be superseded regardless. Blocking costs nothing; publishing an unresolvable coordinate cannot be undone. implementation/scripts/_release-dag-policy.test.cjs parses the workflow's job graph on every PR and fails if a leaf carrying a second in-repo coordinate is placed back in the matrix, or if either ordering edge disappears (rf2-p4a93; the fresco stage and its own acceptance + teeth cases are rf2-gra70's).
The tools tier¶
tools/ is versioned in lockstep with the framework — every tool jar reads the same repo-root VERSION, and verify-version-lockstep.sh gates them alongside the framework artefacts — but a v* tag publishes none of it. Each tool ships on its own tag prefix, so a Xray-only fix does not republish fourteen framework artefacts and a framework release does not accidentally republish Xray. Five tool jars carry Clojars coordinates:
| Artefact | Coordinate | Tag | Workflow |
|---|---|---|---|
tools/xray/ |
day8/re-frame2-xray |
xray-v* |
release-xray.yml |
tools/story/ |
day8/re-frame2-story |
story-v* |
release-story.yml |
tools/machines-viz/ |
day8/re-frame2-machines-viz |
machines-viz-v* |
release-machines-viz.yml |
tools/story-mcp/ |
day8/re-frame2-story-mcp |
story-v* |
release-story.yml |
tools/mcp-base/ |
day8/re-frame2-mcp-base |
story-v* |
release-story.yml |
Three of the five share one glob. story-v* publishes mcp-base, story and story-mcp together, in that dependency order — rf2-4u3t1 ruled that Story and its MCP surface ship as a set rather than each taking a tag, because they are specified as a shipping pair and a second glob would let them skew; mcp-base joins them because story-mcp depends on it and it must be on Clojars first. It has no in-repo dependency of its own, so it publishes alongside Story rather than behind it.
Two further tools sit outside Clojars entirely and outside this runbook: the pair MCP server ships on npm as @day8/re-frame2-pair-mcp, and the app template ships as a git coordinate on a template-v* tag via template-release.yml. Neither carries a :clein/build alias, which is why the lockstep script's tools inventory excludes them.
Release order¶
Tool jars pin their in-repo siblings at the lockstep VERSION, and those siblings have to be on Clojars already when the tool's tag lands. So for a given VERSION the tags go out in this order:
v<VERSION>— the framework tier: core and every leaf.machines-viz-v<VERSION>— consumes core only.xray-v<VERSION>— consumes ten published siblings,machines-vizamong them, which is why it follows step 2 rather than sharing it. Every one of the ten is published by step 1 or step 2 (rf2-gra70 closed the last gap by publishingday8/re-frame2-fresco), so this step is no longer blocked — it is simply ordered.story-v<VERSION>— publishesmcp-base, thenstory(five siblings, Xray among them), thenstory-mcp(which pins both). Ordered behind step 3:tools/story/deps.edndeclaresday8/re-frame2-xrayas a runtime coordinate andtools/story-mcp/pins Story, so three of the five tool jars follow Xray.machines-vizandmcp-baseare the two that can ship independently of it.
That ordering is enforced structurally rather than by this list. After each workflow rewrites :local/root → :mvn/version, the rewritten graph is resolved cold against Clojars, so a coordinate that has not published yet fails the job at classpath resolution — before clein deploy can touch Clojars. The machines-viz and Story workflows force that resolve with a dedicated clojure -P step; release-xray.yml has none, and gets the same cold resolve as a side effect of the clojure -M:clein pom that preflight-xray-package.sh runs to build the pom it then inspects. Story goes further and adds a package preflight that parses the generated pom and refuses to deploy unless all five coordinates are present at the exact VERSION. That gate reads the artefact rather than the inputs that were supposed to produce it, which is the right posture in front of an irreversible act.
The mcp-base and story-mcp jobs added in rf2-2ii52 do the same, with one difference worth copying: neither the rewrite list nor the preflight's expected set is written down anywhere. rewrite-in-repo-coords.sh reads every :local/root coordinate out of the artefact's own deps.edn, and preflight-tool-package.sh derives the whole expected dependency set from the committed copy of that same file, then asserts the generated pom matches it exactly — present, complete, at the lockstep VERSION, and nothing extra. Add a dependency to either artefact and both gates cover it on the next release with no edit to the workflow. That is the lesson of rf2-5dut1 (the note below) and of rf2-7fxf8: a hand-maintained roster cannot report on what it does not list, so its green is an active false assurance rather than a merely missing check.
Closed — Xray's last unpublishable coordinate (rf2-5dut1, closed by rf2-gra70).
tools/xray/deps.edndeclares ten in-repo runtime coordinates, and for a long time one of them could not be pinned to any version because its target artefact carried no:clein/build.release-xray.ymltherefore rewrote nine and left that one at:local/root, andpreflight-xray-package.shrefused anxray-v*deploy over it — by design, since a pom naming a GAV Clojars does not have moves the failure from our release job to the consumer's build, where there is no yank. The coordinate wasday8/re-frame2-freehandunder rf2-5dut1; rf2-l86mm deleted that edge with the Views panel's Freehand sections, leavingday8/re-frame2-fresco. rf2-gra70 answered it the other way — by publishing the artefact — so all ten coordinates are now rewritten and the refusal is gone. What replaces it is an ordering obligation, not nothing: anxray-v*tag is shippable only after a frameworkv*tag at the same lockstep VERSION, which is step 1 of the release order above. What enforces it here is not aclojure -Pstep — this workflow has none — but theclojure -M:clein pominsidepreflight-xray-package.sh, which resolves the rewritten graph from Clojars as a side effect of building the pom it goes on to inspect.The prohibition that guarded it still stands, and is not discharged by the roster happening to be complete. Do not point this workflow at
rewrite-in-repo-coords.sh: that driver rewrites every declared coordinate without asking whether the target is publishable, so the day a new unpublishable in-repo coordinate appears it would mint a<coord>:<VERSION>that cannot exist and the presence-based preflight would wave it through — the one outcome worse than a refusal.
How story-mcp and mcp-base got their publish path¶
Both carried Clojars coordinates and passed the lockstep gate for months while being unshippable. Both declared day8/de-dupe as a runtime dependency via a git coordinate, and clein pom drops git coordinates silently — so a jar built from either would have published a pom missing a runtime dep, with no yank to undo it. There was no rewrite that repaired it: the library is not on Clojars, and Clojars refuses NEW projects in unverified, non-reverse-domain groups, so day8/de-dupe could not be put there under that name.
Mike ruled route (b) on rf2-2ii52: the 271-line codec was vendored into re-frame.mcp-base.dedup, carrying the upstream MIT notice, and every day8/de-dupe coordinate was removed. mcp-base now has no runtime dependency but Clojure itself, so its pom is complete by construction, and story-mcp's two remaining coordinates are both :local/root — the class the rewrite repairs and the preflight proves.
Verify before the first tag (rf2-2ii52). None of
day8/re-frame2-story,day8/re-frame2-story-mcporday8/re-frame2-mcp-baseexists on Clojars yet, and the verified-group rule that blockedday8/de-dupeapplies to any NEW project in theday8group — the 2026-05-13 attempt got a 403 reading "Group 'day8' isn't verified, so can't contain new projects". Whether the group can still take new projects is a release-wide coordinate-policy question, and it applies to every unpublishedday8/re-frame2-*coordinate, not only these three.
Recovery¶
Identical to the framework tier: Clojars has no yank, so recovery is bump-and-supersede. Do not re-run a failed tool deploy on the same tag — Clojars 409s on the duplicate upload. Bump VERSION, retag with the bumped value (xray-v0.0.1.alpha-1), rerun. See §Recovery from a partial deploy for the full procedure; the only difference is that bumping VERSION for a tool fix puts the framework tier out of lockstep until its next v* tag, which is expected and is what the per-tool cadence buys.
But "out of lockstep" understates it: a tools-tier recovery bump cannot ship alone. Each tool's release workflow rewrites every one of its in-repo :local/root coordinates to the bumped VERSION, and there is no per-coordinate override — the workflow passes the one VERSION for every coordinate it rewrites, so the recovery tag pins siblings at a version nobody has published. xray-v0.0.1.alpha-1 rewrites Xray's ten in-repo coordinates — nine framework artefacts plus machines-viz — to 0.0.1.alpha-1, and none of the ten is on Clojars at that version. The job then fails at dependency resolution before it can deploy, which is the ordering gate of §Release order working as designed. The promise in §The tools tier that a Xray-only fix does not republish fourteen framework artefacts holds only while VERSION stays put; a recovery bump is precisely the case where it does not.
So price the recovery before you bump, not after. To ship the one fixed tool jar you must first publish everything it names at the bumped VERSION, and the framework tier has no partial tag — a v* tag ships all fourteen artefacts. Recovering a bad Xray publish is therefore v0.0.1.alpha-1 (fourteen artefacts), then machines-viz-v0.0.1.alpha-1, then xray-v0.0.1.alpha-1: sixteen artefacts republished to replace one. Add story-v0.0.1.alpha-1 to bring Story's three jars back into lockstep and it is all nineteen. Every one of those is a permanent version that cannot be taken back, so the choice to bump is a whole-repo decision even when the defect is in a single tool jar.
First publish¶
Every tool workflow has the same first-publish protocol as the framework: Mike pushes the first tag by hand once the Clojars secrets are wired and the tags this tool depends on have published. Subsequent releases run automatically on tag push.
The generator template's tools pin¶
The app template carries no tools coordinates since rf2-zq34m (2026-09-02): a generated project names day8/re-frame2 and its substrate adapter only, and Xray and Story attach afterwards through their own installation pages. There is no tools pin to bump in a release commit and nothing under tools/template/ that a framework tag has to keep in step with. The hand-wired route in skills/re-frame2-setup/references/deps-versions.md is the one place that still teaches a git coord for a tool; it flips to :mvn/version when that tool's tag ships.
Pre-flight checklist¶
Before tagging:
- [ ] All checks green on
main(thetestsworkflow + any required reviews). - [ ]
VERSIONfile updated to the target version. Single line, no trailing whitespace. - [ ]
migration/from-re-frame-v1/README.mdcarries a freshM-NNentry if the release contains a breaking change. (The migration corpus stays flat through 1.0; numbering is monotonic.) - [ ]
CHANGELOG.mdupdated for the release, including its artefact roster. The GitHub Release body links to it, so it is the canonical narrative — and that link is pinned to the tag, not tomain, so the CHANGELOG has to be right at the moment of tagging. A later correction onmainwill not reach a release body that has already been cut. - [ ] The tag's commit is the same commit that updates VERSION + the migration corpus + CHANGELOG (one release commit).
- [ ] Locally green:
./.github/scripts/verify-version-lockstep.shpasses. (The CI gate runs the same script; running locally first surfaces drift in seconds.)
Recovery from a partial deploy¶
Clojars does not support yanking a published version. The recovery story is bump-and-replay, not rollback.
If a deploy job fails part-way through (e.g. deploy-core shipped, but deploy-flows failed):
- Diagnose. Read the failing job's logs. Common causes: transient Clojars 5xx (re-runs cleanly on retry), credential rotation (CLOJARS_USERNAME / CLOJARS_PASSWORD secrets stale), pom-validation regression in the leaf's clein descriptor, network outage during the leaf's
clojure -M:clein deploy. - Decide whether the partial set is publishable as-is. A consumer pinning the bumped version expects a coherent set; the partial set the failed run left on Clojars is not coherent. Do not promote it.
- Fix the cause locally. Land the fix on
mainvia the normal PR flow. - Bump VERSION in a release-recovery commit. For pre-releases, increment the suffix:
0.0.1.alpha→0.0.1.alpha-1. For stable, bump the patch:1.0.0→1.0.1. - Re-tag with the bumped VERSION. The workflow ships every artefact at the new version, restoring the lockstep contract on the consumer side: a consumer pinning the bumped version pulls a coherent set; the partial-release artefacts from the failed run are tombstoned-by-supersession (still on Clojars, but nobody pins them).
- Note the abandoned version in CHANGELOG.md so future readers don't try to pin it.
Do NOT attempt to re-run the failed workflow with the same tag. The deploy-core step will 409 on the duplicate jar upload to Clojars and the workflow will appear stuck.
Do NOT ask Clojars support to yank. The platform doesn't expose it; ad-hoc yanks would corrupt downstream caches anyway.
Performance-instrumented prod bundles¶
Per Spec 009 §Performance instrumentation, re-frame2 ships a default-off Performance API channel gated on the re-frame.performance/enabled? goog-define. Releases land both shapes:
- The published artefact (the
day8/re-frame2-*Maven jars driven through this release pipeline) carries the bracket sites in source. Apps consuming the artefact decide at their:advancedbuild time whether to flip the flag. - The release verification in CI runs
npm run test:perf-bundle, which builds two:examples/countervariants under:advanced(one with the flag off, the default; one with it on via:closure-defines {re-frame.performance/enabled? true}) and asserts:- the off bundle carries zero
performance.mark/performance.measure/re-frame.performancestrings (bundle-isolation: shipped binaries that don't ask for timing have no User-Timing cost); - the on bundle carries those strings (bundle-presence: the toggle actually produces the measure entries).
- the off bundle carries zero
The grep methodology mirrors npm run test:elision (the trace-surface elision contract). These gates run by changed surface in PR CI, in the scheduled/manual expensive workflow, and in the release workflow before deploy.
Apps that ship a perf-instrumented prod bundle alongside their default release set their own consumer config:
;; consumer's shadow-cljs.edn — perf-on prod build
{:builds {:app-perf {:target :browser
:output-dir "..."
:compiler-options {:closure-defines {goog.DEBUG false
re-frame.performance/enabled? true}}}}}
goog.DEBUG=false elides the trace surface (per Spec 009 §Production builds); re-frame.performance/enabled?=true keeps the User-Timing brackets live. The two flags are independent — apps freely combine them per build target.
Lockstep verification (drift detection)¶
./.github/scripts/verify-version-lockstep.sh is the single source of truth for the lockstep contract. It is invoked by:
- the
testsworkflow on every PR (verify-version-lockstepjob — fast, runs in parallel with the test jobs); - the
releaseworkflow as the first gate before any deploy (verify-version-lockstepjob — gatestest, which gatesdeploy-core).
It covers nineteen artefacts — the fourteen under implementation/ plus the five tool jars — and prints that count on success, so a summary line naming any other number is itself the drift signal.
The contract:
- Repo root has a non-empty
VERSIONfile. - Every artefact's
:clein/builddefers to that single source through a relative:versionpath. The path differs by tier:"../../VERSION"for core, the per-feature artefacts and the tool jars;"../../../VERSION"for the adapters, which sit one level deeper underimplementation/adapters/. - Every non-core artefact references core by
:local/root—"../core"from a per-feature artefact,"../../core"from an adapter,"../../implementation/core"from a tool. The release workflow rewrites these to:mvn/versionat deploy time. - No artefact's committed
deps.edncarries a literal:mvn/versionfor anyday8/re-frame2-*artefact in a non-comment line. - Every
implementation/*/deps.edndeclaring a:clein/buildalias appears in the lockstep inventory andrelease.yml's deploy jobs (the inventory guard), so a new publishable artefact cannot be omitted silently. - Each tool jar is packageable, not merely version-pinned:
:clein/buildcarries the:mainkey clein's spec requires, and no runtime coordinate is a formclein pomcannot express. Both classes had already shipped unnoticed before rf2-2ii52 added the checks — an artefact can be perfectly pinned and still impossible to build. The second check carries no allowlist: an unpublishable runtime coordinate is not a policy exception to record, it is an artefact that cannot ship a correct pom.
The tools half of the inventory is still written out by hand, in the script's TOOLS_LOCAL_ROOTS, but it can no longer fall behind: rf2-7fxf8 added the converse pass, which derives each tool's real :local/root set from its committed deps.edn and fails on any coordinate the list does not name. That is what turned Xray's ten-versus-one gap from an invisible one into a reported one.
What the lockstep gate still does not assert is that a tool's release workflow rewrites what the inventory lists — the gap Xray's roster sat in for as long as nobody cut a tag. implementation/scripts/_preflight-xray-package.test.cjs closes it for Xray at PR time: it reads tools/xray/deps.edn structurally, partitions the coordinates by whether the target artefact carries a :clein/build, and asserts release-xray.yml rewrites every publishable one and no unpublishable one. The unpublishable partition is EMPTY as of rf2-gra70, and the suite pins that too — in both directions, so a new unpublishable coordinate reds rather than quietly joining a known-bad set. Both directions matter. A missing coordinate ships a pom with a hole in it; a rewritten unpublishable one ships a pom naming a GAV that does not exist, which is worse.
Run locally any time:
Lockstep versioning policy through 1.0¶
Through 1.0, every artefact ships at the same VERSION. Independent versioning is revisited post-1.0. The mechanism:
- single root
VERSIONfile; - every artefact's
:clein/build :versionis the relative path"../../VERSION"; - every non-core artefact references core via
:local/root "../core", swapped to:mvn/version $VERSIONon the throwaway runner checkout at deploy time.
There is intentionally no per-artefact version override. Adding one would break the lockstep contract; the verify script flags it.
Cross-references¶
- .github/workflows/release.yml — the release pipeline for the framework tier.
- .github/workflows/release-xray.yml, release-story.yml, release-machines-viz.yml — the per-tool release pipelines; each header carries the rationale for its own rewrite set.
- .github/scripts/rewrite-in-repo-coords.sh, preflight-tool-package.sh — the derived rewrite + preflight pair, which read their coordinate set out of the artefact's own
deps.edninstead of listing it. - .github/workflows/template-release.yml — the app template's git-coordinate release.
- tools/README.md — what each tool is and which coordinate it publishes under.
- .github/workflows/test.yml — PR-time tests including lockstep drift detection.
- .github/scripts/verify-version-lockstep.sh — the lockstep contract script.
- spec/Conventions.md §Packaging conventions — artefact naming, the independence rule, the bundle-isolation argument.
- migration/from-re-frame-v1/README.md — the migration prompt; flat through 1.0.
- examples/real-apps/realworld_http/README.md — the canonical multi-artefact integration test.