Maintainer And Release Guide
Copy MarkdownLockspire release work should stay boring, reviewable, tied to repo truth, and inside the 1.0 GA support contract defined in docs/supported-surface.md.
Repository evidence policy
tmp/admin-ui-polish/*.png is retained, repo-only milestone evidence. Images
must use demo data or be redaction-safe, must never be imported by runtime code
or package inputs, and may be replaced only when a newer inventory supersedes
them. Before deleting an image, update every retained evidence matrix that
references it. Generated build outputs, logs, dependency directories, and
one-off debug scripts are not durable evidence and must remain untracked.
Lockspire now operates on a sustaining release train by default. That means the normal posture is not "open the next milestone"; it is "keep main green, keep release truth coherent, and let patch-eligible merged changes flow toward the next patch release."
This guide is maintainer-only release operations guidance. It does not define a second public support contract.
For DCR/logout wording in release notes or release review, defer to the canonical support contract in docs/supported-surface.md rather than restating a separate support matrix here. The same rule applies to client_secret_jwt: this guide can acknowledge the shipped narrow direct-client slice, but the canonical support contract and the dedicated host guide own the exact support wording.
The same rule applies to advanced setup claims such as mTLS and protected-route support: release notes and maintainer review can point to docs/supported-surface.md, docs/mtls-host-guide.md, and docs/protect-phoenix-api-routes.md, but should not invent broader trust equivalence, automatic proxy trust, or generic deployment automation language here. Canonical wording enforcement belongs in the proof-focused contract tests and should stay there rather than being redefined in this guide.
Normal flow
Run the repo hygiene gate before opening release-prep cleanup or trying to cut a release:
./scripts/maintainer/repo_hygiene_check.sh
Treat PASS as ready, WARN as triage required, and BLOCK as stop-and-fix. If you already have fresh contributor-gate evidence for the exact branch, you can skip the local rerun with ./scripts/maintainer/repo_hygiene_check.sh --skip-mix-ci.
The standing release-train ledger lives in .planning/RELEASE-TRAIN.md. Update it after every real publish so the next maintainer inherits concrete proof instead of oral history.
- Merge reviewed changes to
main. - Let Release Please open or update the release PR.
- Treat the Release Please PR as review-only evidence, not authenticated release proof.
- Review the release PR diff,
mix.exs,CHANGELOG.md, and the workflow/config artifacts that define the release lane. - Let
.github/workflows/release-please-automerge.ymlsquash-merge an eligible bot Release Please PR only when the repository variableLOCKSPIRE_RELEASE_AUTOMERGE_ENABLEDis explicitlytrue; otherwise keep the automation closed and use the separately reviewed PR path. - Wait for the merge commit's own successful
CIpush run on the currentmainhead. That exact run dispatches the trusted release lane with its SHA and run ID; the pre-merge CI run is never publish evidence. - Let the validator confirm the exact immutable main SHA, matching successful CI run, and repository identity.
- Let the unprivileged prepublish job build one tar, bind it to a redacted manifest, and prove that local tar through the clean-room SaaS HTTP journey.
- Let the protected publisher validate and consume that same SHA-bound artifact; it checks any existing GitHub tag before publication, applies a no-bypass update/deletion freeze to the exact version tag, and creates or verifies that tag at the exact source SHA before uploading to Hex. It then publishes the manifest-verified package to Hex before creating or validating the matching GitHub release, and verifies the tag again. The temporary tag freeze remains through release creation and is removed by the job's always-run cleanup. A failed Hex upload can leave the verified tag without a GitHub release; recovery must reuse that same tag and SHA. It must not rebuild release intent from a moving branch.
- Treat the resulting protected Hex package, matching GitHub release, exact-version public HTTP journey, and bounded evidence artifact as the authoritative release record.
Checked-in proof stops at the merged release commit plus the repo-owned workflow and docs. Protected-environment proof starts only when the publish job in .github/workflows/release.yml enters the hex-publish environment.
Normal releases maintain the Release Please PR on main pushes. After an authorized repository-token merge, automation explicitly dispatches canonical CI for that exact current main SHA because token-authored merges do not recursively emit push workflows. A successful canonical CI run—either a normal push or that bounded workflow_dispatch—then dispatches publish. Recovery needs the same full SHA, successful CI run ID, auditable reason, and the one-SHA LOCKSPIRE_PHASE143_AUTHORIZED_SHA authorization. After the protected environment approval, the publisher mints a short-lived GitHub App token, creates an active restrict updates ruleset scoped only to main with no bypass actors, then rechecks the current main SHA and authorization in the same shell invocation that uploads the exact package. The freeze remains active through GitHub release creation and release PR labeling; an always-run cleanup removes it when the publish job ends.
Evidence boundaries
Keep release evidence in three separate buckets:
- Repo-owned proof:
.github/workflows/release.yml,.github/workflows/release-please-automerge.yml,.github/actions/release-please/action.yml,docs/maintainer-release.md, andtest/lockspire/release_readiness_contract_test.exsdefine the canonical lane and should stay reviewable in git. - GitHub settings proof: the live
hex-publishenvironment settings prove branch restriction tomain, admin-bypass posture, and environment-secret placement. - Main-freeze proof: a temporary repository ruleset prevents updates to
mainduring protected publication. The publish job reads back its exact active scope, no-bypass policy, and effectiveupdaterule before using the final exact-SHA check. A separate temporary tag ruleset targets onlylockspire-v<version>and blocks updates and deletions while the package and GitHub release are created. The tag is created through GitHub's create-reference endpoint, then its exact commit is read back before Hex upload. The GitHub App installation is limited toszTheory/lockspireand its token receives onlyAdministration: writeandActions: readpermissions. - Workflow-run proof: the single-artifact chain retains a schema-versioned manifest plus bounded prepublish/postpublish receipts. The manifest binds source SHA, package version, tar checksum and byte size, and pinned Elixir/OTP/Mix/Hex/Phoenix/LiveView/PostgreSQL versions. Raw OAuth journey logs, process configuration, and secrets are never uploaded.
Plan 04 review finding CR-01 added a temporary no-bypass ruleset for the exact version tag and a create-only tag reservation before Hex upload. The tag guard rechecks the ruleset and the remote peeled target immediately before uploading; the ruleset is removed only after release creation or the always-run cleanup. Manifest validation also rejects JSON booleans for integer schema and byte-size fields.
The exact-tar uploader calls the undocumented, version-bound internal API
Hex.API.Release.publish/5. The prepublish receipt records that the manifest's
runtime.publisher_hex equals the builder's runtime.hex, that the selected
archive exports the API, and that the exact-byte fixture passed. Review those
fields before the protected publication decision. The protected job installs
that same manifest-recorded archive and repeats the version and API checks
before it reaches any credential-bearing upload step. This is evidence for the
selected archive only; it does not claim compatibility with a version range.
Terminal outcome and partial publication
Every workflow_dispatch release attempt produces the bounded
lockspire-release-terminal-receipt-<workflow_run_id> artifact, including when
the publisher or postpublish job fails or is skipped. It contains only
release-terminal-receipt.json and is retained for 90 days. The separate
prepublish package artifact, including the exact tar, is retained for 30 days.
The terminal receipt records .workflow_run_id, .source_sha, .ci_run_id,
.package, .version, .public_latest_version, .manifest.sha256,
.manifest.state, .artifact.sha256, and .artifact.state. Each entry under
.stages (candidate, artifact, prepublish, hex_publish,
github_release, tag, docs, and install) is passed, failed,
not_run, or unknown. .observations records hex_presence,
hex_checksum, github_release_presence, tag_target_sha, docs_presence,
and install_result; .release_verified, .blocker, and .next_safe_action
summarize the outcome. It contains no raw logs or credentials.
Treat observations.hex_presence: present as “public on Hex,” even when
release_verified is false. Complete verification requires the manifest and
exact tar to match, the exact Hex checksum, a GitHub release and tag target
matching the source SHA, exact-version docs, and a passing public install
journey. A failed or skipped workflow job alone does not prove that Hex lacks
the package. Read the terminal receipt and independently query the public Hex
release before updating release truth.
If Hex exposes 1.5.1 while later proof is incomplete, record 1.5.1 as public in
.planning/RELEASE-TRAIN.md, state that release verification is incomplete,
preserve the blocker and terminal receipt, and leave the milestone open. The
maintainer-facing result should show the current public version, verification
state, source SHA, matching CI run, package checksum, GitHub release/tag
target, install result, blocker, and next safe action. Link the bounded receipt
instead of copying logs.
Recovery may reuse only the same authorized source SHA and the same manifest-verified tar bytes; first confirm any existing Hex version has that same whole-tar checksum. Never rebuild under prior proof, overwrite a public version, or retarget a mismatching tag. The tar is available for same-artifact recovery for 30 days, while bounded evidence remains for 90 days. Once the tar expires, same-artifact recovery under the old proof is no longer possible; do not describe retained metadata or receipts as the missing package bytes.
Public release claims stay anchored to docs/supported-surface.md plus the checked-in artifact chain (mix.exs, .release-please-manifest.json, CHANGELOG.md). GitHub settings and workflow-run evidence support that story, but they do not replace the canonical support contract.
Contributor gate
Contributors should have one canonical answer before merge: run mix ci.
mix ci is the maintained contributor lane and it covers:
mix qafor format, warnings-as-errors compile, Credo, and Sobelowmix docs.verifymix deps.auditmix package.buildmix test.fastmix test.integration(including the canonical onboarding and OIDC e2e proofs)
CI may keep those checks split into separate jobs for cacheability and diagnostics, but that workflow still needs to remain mechanically equivalent to mix ci.
Dialyzer is a required, cached PR CI job with a zero-warning baseline and no warning suppression. It remains available locally as mix qa.dialyzer and stays separate from the faster mix ci contributor loop so the expensive type proof is explicit and independently diagnosable.
Release Please generated PR checks are informative review context. They are not authoritative release proof, because trusted proof starts only after merge in the protected hex-publish lane.
Keep the Release Please invocation repo-controlled. .github/workflows/release.yml should call ./.github/actions/release-please, and that checked-in action should remain the only implementation detail between the workflow contract and the upstream release-please runtime.
Maintainer-only release gate
mix release.preflight stays additive to mix ci. It is not a second contributor command; release automation runs it at the exact verified SHA before the secret-bearing environment and binds its tar output to the retained manifest.
mix package.publish-dry-run remains a required release gate through mix release.preflight. It does not require the publish secret and is not a manual local verification requirement for contributor closure.
If workflow_dispatch is used, treat it as exact-ref only. It is not a new release-intent trigger and it does not replace the Release Please driven path. workflow_dispatch must run from main and accepts only one lowercase full 40-hex commit equal to current origin/main, together with the matching successful canonical CI run ID and a nonempty matching LOCKSPIRE_PHASE143_AUTHORIZED_SHA. After the environment approval, the protected job freezes main, then the publish script reads back the active no-bypass freeze and requires a fresh origin fetch, hosted main ref, hosted authorization, recovery input, and verified source SHA to agree immediately before invoking the Hex publisher in that same script. It never accepts a tag or arbitrary candidate.
Sustaining release train
The default train rules are:
- patch-eligible merged changes on shipped surfaces should ride the next patch release
- the train moves only from green
main ./scripts/maintainer/repo_hygiene_check.shis the maintainer readiness gateworkflow_dispatchis exact-ref only for release automation or recovery- anything that widens Lockspire's support boundary or product scope should become a new milestone rather than an opportunistic patch release
Release candidate checklist
Before merging a Release Please PR for the root package, confirm this checked-in release-candidate contract end to end. The auto-merge workflow uses successful canonical CI for the exact candidate commit and revalidates the post-merge commit as current main; the Release Please PR is generated by GITHUB_TOKEN and contains only release metadata files.
- Confirm canonical CI is green for the exact candidate commit; if merging manually because the guard does not apply, run
mix cior review equivalent SHA-matched CI evidence, then require the merge commit's own successful CI before publication. - Review
mix.exs,.release-please-manifest.json, andCHANGELOG.mdtogether so version, package metadata, and release notes describe one embedded-library release story. - Review
release-please-config.jsonand confirm the root package still usescomponent: "lockspire",include-v-in-tag: true, andinclude-component-in-tag: true. - Confirm the expected root release tag target is still
lockspire-v<version>for the root package and matches the currentmix.exs/ manifest version. - Review
.github/workflows/release.ymland confirm the only checked-in Release Please entry point isuses: ./.github/actions/release-please. - Confirm
.github/actions/release-please/action.ymlstill preserves root outputs such astag_nameandrelease_created, because those outputs define which merged release commit is allowed to approach the protected publish lane. - Confirm
workflow_dispatchremains exact-ref only, requiresrecovery_reason,recovery_ref, andsource_ci_run_id, and accepts only a lowercase full 40-hex commit equal to currentorigin/mainrather than a tag or new release intent. - Confirm the publish job still targets exactly one protected environment,
hex-publish, and that checked-in proof stops there. - Confirm
docs/supported-surface.mdremains the canonical support contract and that this maintainer guide,README, andSECURITY.mdonly defer to it rather than creating a second support matrix. - Confirm
LOCKSPIRE_RELEASE_AUTOMERGE_ENABLEDis intentionally enabled before allowing the guard to merge. During the Phase 142 hold, it remains absent/false and the hosted workflow is disabled. Only Phase 143 may authorize one exact current-main recovery SHA; publish only after the environment approval and last-moment SHA recheck.
Repo-owned commands stop at mix ci and the checked-in artifact review above. mix release.preflight and the exact-tar Hex upload are release-workflow commands only; publication belongs to the protected hex-publish boundary, not to local maintainer folklore.
Secrets and environment
- Use a protected
hex-publishenvironment for publish jobs. - Store
HEX_API_KEYas an environment secret, not an inline workflow secret. - Create a GitHub App installed only on
szTheory/lockspire, withAdministration: writeandActions: readrepository permissions. Put its numeric ID in thehex-publishenvironment variableLOCKSPIRE_RELEASE_FREEZE_APP_IDand its private key in thehex-publishenvironment secretLOCKSPIRE_RELEASE_FREEZE_APP_PRIVATE_KEY. The workflow mints a single-repository installation token only after environment approval; the token is revoked at job completion. Do not reuse a broad organization-wide App installation. - Restrict the environment to deployments from
main. - Require explicit environment approval for
hex-publish. The current repository has only the maintainer as an eligible reviewer, soszTheorymay approve their own deployment (prevent_self_review=false); this is a manual hold, not independent review. - Keep workflow permissions minimal and publish jobs pinned to immutable action SHAs.
- Keep
HEX_API_KEYavailable only to the protected publish step. The prepublish clean-room proof and postpublish public verification stay unprivileged. - Configure the
hex-publishenvironment to serialize and restrict publication frommain; the workflow's release concurrency remains non-canceling. - If a merged release needs to be replayed after a workflow failure, use
workflow_dispatchwith both a recovery reason and the exact recovery ref so the protected publish lane replays the intended revision rather than whatevermainpoints to later. - The publisher creates the temporary ruleset
Lockspire protected release main freezewith exactly one rule (update, with fetch-and-merge disabled),refs/heads/mainas its only target, and an empty bypass actor list. Thepublishjob removes the ruleset in analways()cleanup step after publishing or failure. If the runner is lost or cleanup fails,mainstays frozen; check for active protected release runs, then inspect and remove only this reserved ruleset as an administrator before resuming merges. - The publisher also creates
Lockspire protected release tag freeze <tag>for the exactlockspire-v<version>tag, with update and deletion blocked and no bypass actors. If its cleanup fails, check for active protected release runs, inspect the exact tag target and ruleset scope, then remove only the stranded ruleset for that tag as an administrator. Preserve the tag and any unrelated rulesets; stop for maintainer review if the tag does not point to the verified release SHA. - Record protected-environment evidence separately from repo-owned proof: deployment restrictions, bypass posture, and environment-secret placement all live in GitHub settings rather than in the repo.
Phase 142/143 release hold
Phase 142 keeps the hosted Release Please Auto Merge workflow disabled. LOCKSPIRE_RELEASE_AUTOMERGE_ENABLED stays absent or anything other than the literal true, and LOCKSPIRE_PHASE143_AUTHORIZED_SHA stays absent. Do not merge the correction PR, dispatch protected publication, create a tag/release, or publish while this hold is in effect.
The hex-publish environment retains its custom main-only deployment policy and requires a named reviewer. The sole eligible reviewer is the authenticated maintainer, so the configured approval permits self-review and is not independent. main requires a PR, strict status checks for all seven canonical CI jobs, stale-review dismissal, conversation resolution, admin enforcement, and no force pushes or branch deletion. Its approval count is zero because no independent reviewer is available; the separate exact-head merge authorization remains required.
Phase 143 may authorize one publication only after its separate maintainer decision and fresh current-main proof. Set LOCKSPIRE_PHASE143_AUTHORIZED_SHA to that full lowercase 40-hex main SHA, then use the exact-ref recovery dispatch with the matching successful canonical CI run and reason. The protected job waits for hex-publish approval, creates and reads back the no-bypass main freeze, then reads the hosted main ref and authorization variable through GitHub's API in the same invocation that publishes. It requires every value plus the recovery input and verified source SHA to match. The freeze remains active through matching GitHub release creation and is removed by the publisher's final cleanup step. Clear the authorized SHA after the attempt. Re-enable Release Please Auto Merge and set LOCKSPIRE_RELEASE_AUTOMERGE_ENABLED=true only after publication and public artifact verification pass.
For rollback, clear LOCKSPIRE_PHASE143_AUTHORIZED_SHA, remove or set LOCKSPIRE_RELEASE_AUTOMERGE_ENABLED to a value other than true, disable Release Please Auto Merge, and query both release workflows for queued, in-progress, waiting, or pending runs. Stop if a protected publish run is active; do not silently cancel it. Verify the hex-publish main policy and required reviewer gate remain in place.
Release posture
Releases should only claim the supported surface the repo can currently prove.
The repo should not claim full release readiness or broader protocol support until the docs, CI, support policy, and maintainer runbooks all agree with implemented behavior.
That means release posture must stay inside the embedded Phoenix library wedge already proven in-repo: authorization code + PKCE, PAR, JAR request objects by value on the shipped /authorize and /par paths, discovery, JWKS, repo-proven private_key_jwt on Lockspire-owned direct-client endpoints, userinfo, revocation, introspection, refresh rotation, generator-backed install, and operator workflows. The same release posture can now also acknowledge the narrow client_secret_jwt direct-client slice documented in docs/supported-surface.md and docs/client-secret-jwt-host-guide.md.
Do not broaden release claims to external JAR-by-reference, generic external request_uri handling, unsupported client-auth methods, hosted auth service language, certification language, demo-app proof, or full CIAM positioning. Generic JWT client-auth beyond the documented client_secret_jwt direct-client slice remains outside that release posture as well.
Preflight checklist
Before merging a release PR, confirm:
./scripts/maintainer/repo_hygiene_check.shmix ci- the Release Please PR is still review-only and points at the same release workflow/config artifacts
.github/workflows/release-please-automerge.ymlstill limits auto-merge to the GitHub Actions bot Release Please branch, exact release title, and themix.exs/.release-please-manifest.json/CHANGELOG.mdfile allowlist.github/workflows/release.ymlstill calls./.github/actions/release-pleaserather than a direct third-party Release Please action referencerelease-please-config.jsonand.release-please-manifest.jsonstill match the intended release policy- publish job still targets the protected
hex-publishenvironment - trusted release workflow still runs
mix release.preflight - trusted publish lane passes the manifest-verified tar bytes directly to
Hex.API.Release.publish/5and usesmix hex.publish docs --yesonly for documentation - prepublish, protected publish, and postpublish jobs all carry the same verified SHA, manifest version, and tar checksum
- retained release evidence contains only the manifest and bounded JSON receipts, never raw journey logs
- public docs and
SECURITY.mdstill defer todocs/supported-surface.md
Hold points
Stop the release if:
- docs require claims the repo no longer proves
- package metadata or docs build drift from the release artifact
- a workflow change bypasses CODEOWNERS or dependency review
- CI stops being equivalent to the maintained
mix cicontract - the protected
hex-publishenvironment stops being restricted tomain, allows bypass you do not intend to allow, or storesHEX_API_KEYoutside the environment boundary
This file does not broaden the Lockspire product contract. For public support truth, defer to docs/supported-surface.md.
Post-Publish Verification
The release workflow automatically verifies the exact published artifact to guarantee "Install Truth". Its invocation is manifest-bound:
./scripts/publish/verify_install_truth.sh release-manifest.json <same-verified-sha> postpublish-receipt.json
The verifier polls only the release-specific Hex endpoint with bounded retries, requires the public checksum to equal the reviewed tar, checks versioned HexDocs, and fetches that exact public version into the clean-room SaaS HTTP journey. A checksum mismatch is an immediate hard failure: stop, preserve the bounded evidence, and investigate registry/source identity. Do not republish or replace a public version opportunistically.
Recovery must reuse the same verified SHA, successful source CI run ID, version, and manifest-bound artifact. Re-dispatch with an auditable recovery reason; never rebuild from the current branch or substitute a new tar. Runtime and tool versions in the manifest describe the prepublish builder used for the reviewed bytes. They are reproducibility evidence, not a claim that every host must run those exact versions.
The final release-evidence-<sha> artifact has explicit retention and contains
only release-manifest.json, prepublish-receipt.json, and, after public proof
succeeds, postpublish-receipt.json. Workflow console output remains ephemeral
and must already be redacted by the clean-room runner.
After that verification passes, record the shipped version, publish proof, and install-truth proof in .planning/RELEASE-TRAIN.md.
Hygiene Automation
The repo-owned hygiene gate lives at ./scripts/maintainer/repo_hygiene_check.sh.
- Local mode checks git cleanliness, main divergence, worktree and branch clutter, open PR triage, recent GitHub workflow health, release metadata coherence, and optionally reruns
mix ci. - CI runs
./scripts/maintainer/repo_hygiene_check.sh --cito fence the repo-owned release truth that GitHub can prove without local state, including the standing release-train ledger. - Keep this command diagnostic-first. It should tell maintainers what to fix, not silently mutate branches or PR state.