Releasing CEDAR¶
A release turns one immutable build train into a permanent, published version of CEDAR. It moves
every repository's main to the released content, tags it, advances develop to the next
development snapshot, and publishes the Maven release plus the stable frontend npm surfaces to
CEDAR's Nexus.
A release consumes a train rather than building one. The train has already built and verified a coordinated source state, so the release never compiles from a moving target: it takes the exact commits the train recorded, stamps versions onto them, and proves that what it publishes matches what the train built. Creating a train is covered in Publishing Artifacts and Build Trains.
The route is these commands:
cedarcli release plan # read-only rehearsal against a completed train
cedarcli release start # run the release
cedarcli release status # one-shot phase table
cedarcli release status --watch # compact live progress
cedarcli release resume # verify the recorded boundary and continue
cedarcli release abandon # retain and close a local-only superseded attempt
One Modifying Command at a Time¶
release start, release resume, and release abandon hold an exclusive process lock in the
release state directory for their entire operation, including preflight checks and automatic
retries. A second modifying command using the same state directory refuses immediately. Use
cedarcli release status --watch to follow the existing operation; status remains available while
the lock is held.
The operating system releases the lock when the command exits, including after a crash. The
release.lock file remains in the state directory, normally ~/.cedar/train-releases/; its
presence alone does not mean a release is running. Do not delete it while a release command is
running, because replacing the file would break the shared lock boundary. After a failed operation,
follow When a Phase Fails to resume from the recorded state.
The Four Inputs¶
Every release input is stated explicitly. None is derived, defaulted, or read from the environment, because each one names something a release can only get wrong once:
cedarcli release plan \
--version 2.9.3 \
--next-version 2.9.4-SNAPSHOT \
--from-train 2.9.3-dev.20260828.1956 \
--cee-version 2.0.3
--version is the version being released, and --next-version is the snapshot develop moves to
afterwards. Stating both guards against releasing into the wrong series: a release that derived the
next version from the released one could not tell an intended jump from a typing mistake.
--from-train identifies the completed build train the release is cut from. The train's recorded
source commits become the release's source, and its published artifacts become the bytes the
release republishes under the release version.
--cee-version and the Byte-Equivalence Proof¶
--cee-version looks like a version pin, and it is really a proof obligation.
The Embeddable Editor is built by the train as a development version, and it is consumed by CEDAR's frontends from public npmjs as a stable version. Those are two different packages with two different names, so a frontend released against the public package could contain something other than what the train built and tested.
The release closes that gap before it does anything else. It downloads both tarballs, normalizes
each one to remove the differences a publication legitimately introduces, and compares the
remaining payload by SHA-256. Only when the two match does the release pin the public version into
the frontends. cedarcli release plan prints the proof:
CEE equivalence: 2.0.3-dev.20260824.1847.g48283fbabcde -> 2.0.3
CEE payload SHA-256: 6f1c…
CEE executable: identical after declared release-provenance changes
Naming a public version that is not equivalent stops the release at planning time. Nothing is built and nothing is changed.
The normalization is deliberately narrow. It covers package channel metadata, the declared CEE and
model identities, the load trace, the one public-release changelog entry, and CEE's allowScripts
install policy when Angular embeds the root package.json into its bundle. For that last case the
planner reads the exact policy from the train-captured CEE source, requires the corresponding
minified literal exactly once, and removes only that literal. allowScripts controls which npm
dependencies may run install scripts; it is not CEE runtime behavior. An undeclared policy, a second
copy, or a changed byte beside it still fails the executable comparison.
Rehearsing a Release¶
Once the train is complete, rehearse the entire release without changing anything:
cedarcli release plan \
--version 2.9.4 \
--next-version 2.9.5-SNAPSHOT \
--from-train 2.9.4-dev.20260829.1200 \
--cee-version 2.0.3
plan validates the train and runs every machine, source, permission, registry, CEE, and content
check that gates start. It finishes with No changes made.
Plan settles four groups of question.
The machine can run a release. Java 17 and Node 24.19.0 are active, git, mvn, and npm are
on PATH, Git author name and email are configured, the CEDAR profile is sourced, and there is disk
for the train, the attempt tree, and the archives. Plan also inspects the effective npm user
configuration by key name only. It never prints values or registry tokens: obsolete authentication
settings block once, while harmless author-setting deprecations appear as one advisory instead of
being repeated by every install.
An unsourced profile is worth its own mention: the Maven suites read variables the profile defines,
and without them a build fails deep inside Dropwizard configuration rather than at its start.
The source is ready. Every participating repository, including independent CEE consumers, is
clean and pushed. The exact train commit contains every declared wrapper, manifest, lock, build,
preserve, version, and Docker stamp input. CI for that exact commit must be green wherever the
commit defines a workflow; missing, unreadable, queued, or running required checks block. A source
with no workflow is advisory because there is no CI contract to query. The question is asked of the
train commit rather than of whatever develop points at now, which matters because a release
advances develop everywhere at once and the runs those pushes trigger can race the parent snapshot
they resolve against. A whole-stack smoke run recorded by cedarcli test e2e must also have passed
both tiers against exactly the train's source commits. The record is kept per set of heads, so a
rerun against newer heads does not disturb it, and nothing accepts a missing or failed run.
GitHub may briefly return no run while indexing a pushed SHA or transiently return 502/503/504. The same bounded probe used by train preflight retries only those cases, naming its repository, SHA, attempt, and delay. It does not retry 401/403, malformed data, completed red CI, or a persistently absent run. Queued and running CI remain an immediate semantic refusal with the workflow URL.
Every frontend lockfile is checked against the allowScripts policy from that exact captured
package.json. Each dependency with an install script must have an exact package/version true or
false decision. Planning names an unreviewed dependency before any workspace or build exists, and
preparation plus both stamped build variants run with NPM_CONFIG_STRICT_ALLOW_SCRIPTS=true.
When CI is genuinely broken for a reason that must not hold up a release, accept the specific run rather than disabling the check:
cedarcli release start ... --accept-red-develop cedar-repo-server=33211136456
The acceptance names one repository and one run, and it is recorded in the release ledger. There is no flag that skips the check for everything.
The writes will be accepted. Both Nexus credentials are available and authenticate, npm holds an
identity for CEDAR's Nexus registry, the release version is unused in every repository and absent
from the target Maven and npm namespaces, and each remote accepts a dry-run push of every ref it
will later receive: main, develop, release/pre-<version>, the release tag, and, where
applicable, release/post-<next-version>.
Nexus authorization cannot be inferred from the variables being present, because Nexus reads fall
back to anonymous and succeed either way. The CLI prefers BMIR_NEXUS_USERNAME and
BMIR_NEXUS_PASSWORD when both are set; otherwise it reads the bmir-nexus-releases server entry
from ~/.m2/settings.xml. Namespaced and unnamespaced Maven settings files are both supported, and
credential values are never printed. There is no release auth command: credential resolution and
authentication happen automatically during release plan, release start, and release resume.
The Nexus check reads a repository rather than a status endpoint, because the status endpoints stay green while everything behind them fails. One shape of that is worth recognising: when Nexus serves its status endpoints and returns 500 for every repository path, the instance is over its daily request budget rather than broken. Plan says so. Retrying makes it worse, and the budget is a rolling 24-hour window, so the answer is to stop and let it roll off.
The completed train is releasable. Its Maven and npm inventories are complete, and its Docker
plan and completion record contain the same 31 images with immutable sha256 registry digests.
The content is stampable. Every file a Maven build regenerates with the version inside is
declared, every license.txt carries a recognisable copyright line, and each remote's main holds
nothing that develop does not. That last one is reported rather than refused: a release writes
main from the released tree, so anything committed to main alone and never merged back is
replaced.
The Docker source's IMAGE_VERSION, CEDAR_MAVEN_VERSION, and CEDAR_APPLICATION_VERSION must all
equal the train source version. Release stamping advances them together for both the release and
next-development trees, so a new image tag cannot silently retain old Maven or application inputs.
Running a Release¶
cedarcli release start \
--version 2.9.3 \
--next-version 2.9.4-SNAPSHOT \
--from-train 2.9.3-dev.20260828.1956 \
--cee-version 2.0.3
start runs the identical release gate before it touches anything, so a release cannot begin from a
state plan would have refused.
The default foreground view is compact. Raw Maven, npm, embedded-service, and package output is
retained in per-task logs under the numbered attempt rather than streamed into the terminal. Use
--verbose on start or resume when live raw output is needed; it does not change release
semantics.
Transient transport retries are built in. A release runs for hours across two registries and forty remotes, so it retries direct connection failures, HTTP 502/503/504 from resumable Git, Maven, and npm commands, and Git's server-side 5xx failures with bounded backoff. A changed tree, protected-ref refusal, failed byte verification, authentication failure, or Nexus HTTP 500 stops the release at once. Nexus 500 is deliberately not retried because it is also how the daily request-budget failure presents itself.
Before snapshot publication, release publication, and final acceptance, a circuit breaker reads Nexus writable status and one real repository object. It does this before changing the phase ledger or beginning request-heavy verification. A direct connection failure remains retryable; an HTTP refusal opens the circuit.
Phases and the Ledger¶
A release records its own progress in a ledger under ~/.cedar/train-releases/, and every phase
verifies its work before the next one begins:
| Phase | What it does |
|---|---|
preparing-frontends |
Clones the train's exact commits and builds the frontends against the proven CEE |
preparing-versions |
Stamps the release and next-development versions and copyright year, and refreshes train lock baselines changed by those stamps |
validating-builds |
Builds the stamped trees and records proof of their output |
creating-local-refs |
Replaces tracked distributions with the validated builds, removes obsolete generated files, and creates the release commits and tags locally without touching any remote |
publishing-snapshots |
Deploys the next-development snapshots to Nexus, in dependency order |
integrating-remotes |
Writes main, the tag, and develop in each remote |
publishing-artifacts |
Uploads the Maven release and six stable frontend npm packages to Nexus and verifies their published bytes |
accepted |
Proves the finished release from outside its own ledger |
Three properties of that sequence are worth knowing. Nothing reaches a remote until every local ref
has been created and verified, so a release that fails during preparation has changed nothing
outside the machine it ran on. Each integration commit is written from the prepared tree rather
than merged towards it, so main comes to hold exactly the released content.
The third explains why the snapshots are deployed before the remotes rather than after. Integrating
the remotes advances develop to the next version everywhere at once, and the CI those pushes
trigger resolves the parent and the libraries at that version from Nexus. Publishing the snapshots
first means those builds find what they are looking for. Deployed afterwards, as they once were,
they arrived minutes too late and left a tail of red develop builds that said nothing about the
code.
For distribution repositories, the local-ref phase is the boundary at which generated output
becomes release content. The CLI retains the package metadata, replaces everything else in the
tracked distribution with the inventoried production build, and records additions, changes, and
deletions in the release commit. OpenView receives an extra proof: its distributed CEE JavaScript
must equal the public CEE selected by --cee-version after only the declared production endpoint
normalization.
Publication packs the integrated commit rather than overlaying another build afterward. OpenView's
CEE and Web Components runtime files live under node_modules, which npm normally omits; the release
packer explicitly retains those declared runtime assets and verifies their hashes again after
downloading the registry tarball. Final acceptance binds the OpenView distribution and npm package
to the proven CEE bytes. Deployment remains a separate operational boundary, so the environment
smoke check must still confirm what the web server is serving.
Follow a running release with:
cedarcli release status --watch
The watcher prints on state changes and otherwise gives a quiet one-minute heartbeat with elapsed
time, phase counts, the active task, Maven upload counts, scheduled transient retries, and the exact
terminal failure. It remains attached through automatic backoff; Ctrl-C stops only the watcher.
Without --watch, the same command is a one-shot phase table. It says COMPLETE only
after acceptance, highlights the single next or failed phase, and prints the safe commands to run
next. Every Maven file and its disposition are checkpointed in the ledger, so status shows live file
progress while the enclosing artifact task is still open. An interrupted upload resumes by comparing
the immutable remote bytes and continuing. Older ledgers that stored snapshot and release publication
records together are classified by task identity, so their totals remain truthful.
Acceptance¶
Publication is not the end of the route. The acceptance phase asks, from outside the ledger that
recorded the work, whether the release actually holds: every repository carries the release tag,
every remote ref stands where the ledger says, every published artifact still matches its recorded
bytes, and the frontends pin the proven CEE. The answers are recorded, and accepted is the
terminal phase.
The exact-ref check proves each release tag at its recorded commit as part of the same remote read. Acceptance therefore does not repeat a tag-only request across all repositories.
Acceptance also marks the release concluded and frees the active slot. There is no separate
finish command. If the process stops between writing the accepted ledger and concluding its
pointer, cedarcli release resume repairs that bookkeeping step without rerunning any phase.
A release that reports success has therefore been checked rather than assumed. Only COMPLETE
means successfully released. ABANDONED means the retained attempt was closed without releasing
it; every other status remains incomplete.
When a Phase Fails¶
A failure stops the release, records the reason in the ledger, and retains the failed attempt
under ~/.cedar/train-releases/attempts/<version>/. Nothing is rolled back and nothing is deleted,
because a failed attempt is the evidence for diagnosing what happened.
Fix the cause, reconcile without changing anything, then continue from the recorded phase:
cedarcli release resume
Resume starts at the recorded phase, verifies the completed evidence that phase consumes, and
continues with bounded transient retry. It also reruns the checks that remain meaningful at that
phase: source and toolchain checks during local preparation, exact-ref and push checks before remote
integration, and credential, registry, and target-object checks before publication. Conditions
deliberately changed by a completed phase are not tested as though this were a brand-new release.
Completed phases are not re-run, so a release interrupted during publication does not rebuild
anything. This phase-aware gate is automatic; resume takes no additional option.
Never edit a ledger or a release manifest by hand. Those files are the release's own record of what it verified, and a hand-edited record makes every guard downstream of it meaningless.
Sometimes the immutable train is the cause of the failure, so retrying the same evidence cannot
work. If the attempt has not gone beyond local-refs-created, retain it and free the slot with:
cedarcli release abandon \
--version 2.9.4 \
--reason "superseded by corrected train 2.9.4-dev.20260901.0555"
The version must exactly match the active release, and the non-empty reason becomes part of its
ledger. Status then reports ABANDONED; the manifest and numbered attempt workspace remain intact,
and a new attempt may use the same release version.
Abandonment is intentionally limited to local-only work. Once snapshot publication may have begun,
Maven could have changed Nexus even if the deploy failed before recording a completed task. The CLI
therefore refuses abandonment from publishing-snapshots onward, or whenever snapshot,
remote-integration, or artifact-publication evidence exists. Repair and release resume from that
boundary; never use deletion as a substitute.
One release is active at a time. Acceptance closes a successful release, while guarded abandonment closes a local-only attempt that must be replaced by another train.
What a Release Changes¶
In each of the release repositories:
mainholds the released content and carries the tagrelease-<version>.developmoves to the next development snapshot.- The copyright year in
license.txtmoves to the release year on both branches. Only the year changes, and a licence without a recognisable copyright line is left alone.
In CEDAR's Nexus, the Maven release artifacts and six stable frontend surfaces are published under the release version, and the next-development Maven snapshots under the new snapshot version. Workspace and Template Designer receive the stable CEE wiring but retain their independent publication route while migration is in progress. Nothing is published to public npmjs, which is a separate procedure for the Embeddable Editor and the TypeScript model library. Docker images belong to the train and are not promoted by a release.