Release Automation
This document is the detailed authority for SecurityDept release automation. It expands the short rules in AGENTS.md and defines how local release commands, versioning, and GitHub workflows are expected to behave.
Scope
The release authority is split into two layers only:
securitydept-metadata.tomlis the checked-in source of truth for the project version and the set of release-managed manifests.scripts/release-cli.tsis the only supported entrypoint for version changes, npm publishing, crates publishing, and Docker tag calculation.
justfile, pre-commit hooks, and GitHub Actions should call release-cli or just release-* recipes. They should not carry a second copy of release-channel or tag-derivation logic.
Supported Version Shapes
SecurityDept release versions are intentionally restricted to three shapes:
X.Y.ZX.Y.Z-alpha.NX.Y.Z-beta.N
Rejected shapes include:
-rc.N- prerelease identifiers other than
alphaorbeta - extra prerelease segments such as
-beta.1.foo - build metadata such as
+build.5
The repository enforces this through release-cli version check and release-cli version set.
Channel Mapping
Release channels are inferred from the version, not passed manually.
| Version shape | Stage | npm dist-tag | Docker channel tag |
|---|---|---|---|
X.Y.Z-alpha.N | early prerelease | nightly | nightly |
X.Y.Z-beta.N | release-candidate track | rc | rc |
X.Y.Z | stable | latest | latest, release |
Rationale:
latestis the standard stable npm/container convention.nightlyis a clear signal that alpha builds are still fast-moving and not for default consumption.rcis a better external signal thanbetafor publish channels because it tells downstream users the build is pre-release but intended for release validation.- stable container images also publish
releaseas an explicit stable alias for human-facing deployment references, whilelatestremains the default ecosystem convention.
Release CLI Commands
Primary commands:
node scripts/release-cli.ts metadata syncnode scripts/release-cli.ts version checknode scripts/release-cli.ts version set X.Y.Z[-alpha.N|-beta.N]node scripts/release-cli.ts npm publish --mode=dry-run --report=temp/release/npm/dry-run-report.jsonnode scripts/release-cli.ts npm publish --mode=publish --provenance --report=temp/release/npm/publish-report.jsonnode scripts/release-cli.ts crates publish --mode=package --report=temp/release/crates/package-report.jsonnode scripts/release-cli.ts crates publish --mode=package --allow-blocked --allow-dirty --report=temp/release/crates/blocked-package-report.jsonnode scripts/release-cli.ts crates publish --mode=publish --report=temp/release/crates/publish-report.jsonnode scripts/release-cli.ts docker publish --ref=refs/tags/vX.Y.Z[-alpha.N|-beta.N]node scripts/release-cli.ts workflow tests-preflight --format=github-outputnode scripts/release-cli.ts workflow release-plan --format=github-output
Behavioral rules:
metadata syncwrites shared publish metadata fromsecuritydept-metadata.tomlinto publishable Rust crates and publishable npm packages, including descriptions, authors, licenses, Rust crate categories, keywords, repository links, and minimal packageREADME.mdfiles.version setupdates every release-managedpackage.jsonandCargo.tomllisted insecuritydept-metadata.toml.version checkalso validates publishable Rustpathdependencies between workspace crates and requires exact internal requirements in the form=X.Y.Z[-alpha.N|-beta.N].version setalso writes those exact internal Rust dependency requirements for publishable crates, so local package verification and publish preparation stay aligned.npm publishinfers the dist-tag from the version unless an explicit override is passed.npm publishdisables pnpm Git branch checks automatically in GitHub Actions tag workflows, so detached release-tag checkouts do not fail onpublish-branchenforcement.npm publish --mode=publishqueries the npm registry first and skips any package version that is already published, so rerunning after a partial publish only continues with the remaining packages.npm publish --report=...writes the package publish/skip result set used by both local release recipes and GitHub Actions artifacts.- npm package tarball manifest cleanup is owned by the root
/.pnpmfile.cjshooks.beforePackinghook rather than ad hoc release-script file rewriting. This hook rewrites@securitydept/*workspace:version specifiers to the package version that is being published, strips monorepo-onlymonorepo-tscexport conditions from all published packages, and strips Angular-only publish-time fields such as root-onlyfilesanddevDependencies. - Angular SDK packages must publish from the package root with
publishConfig.directory = "dist"; do not runpnpm packorpnpm publishdirectly insidedist, because that loses workspace resolution context beforebeforePackingcan sanitize the manifest. - the GitHub Actions npm publish job uses npm trusted publishing via GitHub OIDC and does not inject a long-lived
NPM_TOKEN; both the workflow and the local publish entrypoint now pass--provenanceexplicitly so provenance does not depend on implicit defaults. crates publish --allow-dirtyexists only for local blocked packaging loops where the working tree is intentionally dirty; it is not part of CI publish flows.- the default
crates publish --mode=packagegate packages all publishable workspace crates in onecargo package --workspaceinvocation. This is required for prerelease internal dependencies, because Cargo verifies later crates against the temporary packaged registry instead of looking only at crates.io for versions that have not been published yet. crates publish --mode=packageandcrates publish --mode=publishuse Cargo's default package/publish verification behavior. They do not pass--release; the verification compile therefore uses the dev/debug target directory andcrates-releaserestores the Tests workflow debug cache read-only, not the Docker release-profile cache.crates publish --mode=publishqueries crates.io before each crate upload and skips versions that are already present, so rerunning after a partial publish does not fail on duplicate uploads.temp/release/crates/package-report.jsonis reserved for the real package gate without--allow-blockedand without--allow-dirty; blocked diagnostics must write to a separate report such astemp/release/crates/blocked-package-report.json.- the GitHub Actions crates publish job uses crates.io trusted publishing by exchanging the GitHub OIDC token through
rust-lang/crates-io-auth-action@v1, then passes the short-lived token tocargo publish; it does not read a repository-storedCARGO_REGISTRY_TOKENsecret. docker publishis the authoritative Docker tag planner and can emit human-readable output, JSON, or GitHub Actions outputs.
Just Recipes
The root justfile imports topic modules from justfiles/*.just so local entrypoints remain predictable while the file stays readable:
- bootstrap and environment setup
- local development
- build tasks
- lint and maintenance
- release automation
- tests and verification
- utilities
Imported recipes still execute from the root justfile working directory. Complex cross-platform behavior belongs in TypeScript CLIs or library modules, not in large shell blocks inside the just recipes.
The release block intentionally avoids explicit prerelease tags. The current version already carries the stage, so commands such as just release-npm-dry-run and just release-npm-publish should infer the correct channel automatically.
Rust Kubernetes e2e helpers are routed through scripts/test-cli.ts kube ..., backed by scripts/lib/kube-test-resources.ts and scripts/lib/kube-test-runner.ts. The CLI uses Dockerode for Docker resource management, applies securitydept.test=true labels to SecurityDept-owned local test resources, and provides:
just ensure-kube-test-helperjust e2e-rsjust e2e-rs-hotjust e2e-rs-isolatedjust clean-kube-test-artifactsjust clean-kube-test-images
GitHub Actions Rules
Release-related workflows must follow these rules:
- active workflow entrypoints are limited to
.github/workflows/docs.yml,.github/workflows/tests.yml, and.github/workflows/release.yml. tests.ymlowns repository verification. It runs onmain,release,v*.*.*tags, pull requests tomain, and manual dispatch. It writestests-workflow-reportso release runs can be audited against the source SHA they depend on. After every successfulreleasebranch push run,tests.ymldispatches.github/workflows/release.ymlthroughworkflow_dispatchwith explicit source ref/SHA and publish toggles.release.ymlis the only release/build/publish authority. It only starts throughworkflow_dispatchbecause crates.io trusted publishing does not support theworkflow_runtrigger event; the automated path must therefore be a post-Tests dispatch instead of requesting OIDC fromworkflow_run.release.ymlowns the source publish gate: it resolves the source withrelease-cli workflow release-plan, runsrelease-cli version check, compares the checked-in version to the expected tag, and verifies tag orreleasebranch source lineage before any publish job can run.releasebranch publish is the primary automated path. Its expected tag policy iscreate-after-publish:release-planandvalidate-release-refreport the expected tag status, a missing expected tag is allowed before publish, and an existing expected tag must already point to the selected source SHA or the release fails before publishing.- After all selected publish jobs succeed, the
release-tagjob creates and pushes the expectedvX.Y.Z[-alpha.N|-beta.N]tag forreleasebranch sources. On the release branch path, the tag is the release result and audit anchor; auditing or retrying a tag or another source requires manually dispatchingrelease.ymland passing the same release gate. workflow_dispatchonrelease.ymlmay publish only when the selected source passes the same release gate; manual toggles choose whether npm, crates, and Docker publish jobs run.- Local
actruns are detected byrelease-cli workflow release-planthroughACT=trueorSECURITYDEPT_LOCAL_ACTIONS=trueand emitlocal_run=true. The workflow keeps the same job graph, but publish jobs switch to local-safe behavior: npm uses--mode=dry-run, crates stop at the package gate and copy that report as the publish report, and Docker builds/loads the runtime image locally without logging in or pushing. - Local
actrelease source validation keeps version/tag shape checks but avoids remoteorigin/releasefetches. Real GitHub runs still enforce release-branch reachability againstorigin/releasebefore publishing. - npm publish uses
release-cli npm publishdirectly and does not expose a manual dist-tag selector. - npm publish must preserve the package-root invocation model so pnpm can honor root
publishConfig.directoryand the root.pnpmfile.cjsbeforePackinghook for Angular packages. - real npm OIDC publish runs in the
npm-releasejob insiderelease.ymlwith thenpm-releaseenvironment. npm trusted publisher configuration must therefore bind publishable packages to.github/workflows/release.ymlandnpm-release. npm-releaseis the only job that may request npmid-token: write; it builds the TypeScript SDK packages from the validated source and publishes withrelease-cli npm publish --mode=publish --provenance --report=....- npm publish relies on GitHub Actions trusted publishing and passes
--provenanceon real publish paths. - crates publish uses
release-cli crates publish; package gates must not use--allow-blocked, publish jobs keep--allow-dirtyand--allow-blockedout, andrust-lang/crates-io-auth-action@v1enables crates.io trusted publishing. - real crates.io OIDC publish runs in the
crates-releasejob insiderelease.ymlwith thecrates-io-releaseenvironment. crates.io trusted publisher configuration must bind publishable crates to.github/workflows/release.ymlandcrates-io-release. crates-releaseis the only job that may request crates.ioid-token: write; it runscrates publish --mode=package, exchanges GitHub OIDC throughrust-lang/crates-io-auth-action@v1, then runscrates publish --mode=publish. Package and publish reports are uploaded separately.- Docker release publishing is artifact-first inside the single
docker-releasejob: the job buildssecuritydept-server,securitydept-cli, and the web UI outside Docker, stages them underrelease-runtime/, then buildsDockerfile.runtimein the same job. Dockerfile.runtimeis the release Docker path. It is Debian slim based to match the GNU/glibc binaries built on GitHub Ubuntu runners. The existing cargo-chef/AlpineDockerfileremains a full-build diagnostic fallback, not the release publish path.- Docker tag calculation still comes from
release-cli docker publish --format=github-outputand feeds the resulting tags/labels directly intodocker/build-push-action. When the source isrefs/heads/release, Docker tag calculation usesrefs/tags/<expected-tag>so release-branch publishes also produce the version and channel tags (vX.Y.Z...,vX.Y,vX,rc/nightly/latest) plus the immutablesha-*tag. - standalone npm, crates, Docker, and common-CI workflows are not active release entrypoints. Reintroducing one requires updating this document and moving trusted-publisher bindings deliberately.
Cache and artifact rules:
pnpm and Rust setup/cache behavior is owned by repo-local composite actions under
.github/actions/.pnpm cache modes are explicit:
read-write,read-only, andnone. The stable restore key ispnpm-store-${runner.os}-${hashFiles(lockfile)}; only one job in a workflow topology may be the read-write owner for that key.Rust cache modes are also explicit. A job that uses the shared key as
read-writemust be the only writer in that topology; downstream jobs useread-onlyrestore or artifacts.Debug CI topology lives directly in
.github/workflows/tests.yml;release.ymlis dispatched by the successfulTestsrun instead of repeating the same debug verification graph, and it avoids the crates.io-unsupportedworkflow_runpublish entrypoint.Rust shared keys are stable lane/profile scopes such as
securitydept-rust-${runner.os}-pr-mainline-debug,securitydept-rust-${runner.os}-mainline-debug, andsecuritydept-rust-${runner.os}-release. Do not embedhashFiles(...)manually in workflowshared-keyvalues;Swatinem/rust-cachealready adds its own Rust-environment hash for Cargo manifests, lockfiles, toolchains, and relevant env vars, and it can restore from previous lockfile versions.Rust cache ownership is split by profile and workflow source:
Cache key profile Read-write owner Consumers Notes securitydept-rust-${runner.os}-${cache_scope}-debugthe Tests workflow rust-debug-cache-primejobclippy, Rust tests, E2E prebuild, and release.ymlcrates-releaseread-only restoreowned by the debug CI topology; cache_scopeis intentionally collapsed into shared lanes such aspr-mainlineandmainlineinstead of per-PR/per-branch names so the cache budget stays bounded whilemain,release, and tag-driven flows still reuse the same debug artifactssecuritydept-rust-${runner.os}-${cache_scope}-releaseinrelease.ymldocker-releasewhenpublish_docker=trueruntime binary builds inside the same docker-releasejobonly Docker consumes release-profile artifacts today, so the writer lives in the single consuming job; split out a prime job only if future release-profile consumers need the same cache Each row has exactly one read-write owner for its cache key. Jobs outside that owner restore read-only or do not touch the key. This is the current practice-approved provisional optimization and depends on the unique-writer topology; its wall-clock benefit still needs a reproducible local workflow benchmark before further tuning.
Docker buildx cache is scoped only to Docker layer caching. The runtime release scope no longer attempts to cache cargo or pnpm builds because those happen before Docker.
already-published skip behavior remains owned by
release-cli npm publishandrelease-cli crates publish, so partial release reruns continue instead of failing on duplicate npm package or crate versions.
This keeps one implementation of:
- allowed release-version grammar
- prerelease-to-channel mapping
- stable Docker aliases
- branch / SHA / tag naming behavior for Docker images
Local Workflow
Recommended local sequence before an actual publish:
mise exec --command "just fix-release-metadata"mise exec --command "just release-version-check"mise exec --command "just release-npm-dry-run"mise exec --command "just release-crates-package"mise exec --command "just release-docker-metadata vX.Y.Z[-alpha.N|-beta.N]"
If the version needs to move first:
mise exec --command "just release-version-set X.Y.Z[-alpha.N|-beta.N]"mise exec --command "just release-version-check"
For local workflow simulation, prefer the just wrappers around scripts/actions-cli.ts: just action-release-validate and just action-release-dry-run. just action-release-run is temporarily disabled while the project replaces the removed act-js integration with the planned local workflow runner. Release publish jobs must still use local dry-run/package/build paths for local simulation and must not push to npm, crates.io, or GHCR.
Example validation commands:
just action-release-validate
just action-release-dry-runThe action recipes accept both CLI-style flags such as --publish-npm=false and just-friendly shorthand such as publish_npm=false. Publish toggles default to false, matching release.yml manual dispatch defaults; opt in per channel when local package/build simulation is needed.
act -n does not execute release-plan, so jobs whose if condition depends on needs.release-plan.outputs.* may not expand in dry-run mode. Full local workflow execution is intentionally unavailable until the replacement local runner is integrated.
Maintenance Expectations
When release rules change:
- update
release-clifirst - update workflows,
justfile, andjustfiles/*.justto call into that logic instead of duplicating it - update this document and the summary rule in AGENTS.md
Do not add new release channels, ad hoc workflow-only tag rules, or manual per-command dist-tag flags without updating the shared release policy.