Release process
September 21, 2026 · View on GitHub
Status: bounded tag-release procedure with exact published evidence.
The main sections below record the most recently published tagged milestone. Later releases inherit the same release workflow; this archive keeps the detailed evidence for that milestone.
Audience: maintainers and release reviewers.
SEMAPRAX tag releases are produced only by the repository CI workflow after
the exact tag commit passes every job aggregated by release-gate. A local
archive can establish scoped local packaging and product behavior, but is not
release-promotion evidence.
Tag admission
The release tag must be v followed by the root Cargo.toml package version.
The packaging scripts reject a mismatch before building. They also require the
exact 40-character lowercase hexadecimal Git commit supplied by the workflow;
that commit is embedded into the compiler CLI's version response and recorded
in the manifest. The daemon has no version command or separately attested
embedded commit. The scripts check the supplied label's form and the unpacked
CLI's agreement with it; they do not independently authenticate the checkout
against Git HEAD or the tag. Exact-checkout provenance remains the release
workflow's responsibility, not a consequence of this self-consistency check.
The admitted release hosts and target archives are:
| Hosted runner | Exercised target | Archive |
|---|---|---|
| Ubuntu 24.04 | x86_64-unknown-linux-gnu | semaprax-v0.4.0-x86_64-unknown-linux-gnu.tar.gz |
| macOS 15 | aarch64-apple-darwin | semaprax-v0.4.0-aarch64-apple-darwin.tar.gz |
| Windows 2025 | x86_64-pc-windows-msvc | semaprax-v0.4.0-x86_64-pc-windows-msvc.zip |
Each archive contains semaprax, semapraxd, LICENSE, README.md, a fixed
smoke program, and the deterministic semaprax.release-artifact.v1 manifest.
The archive's semaprax is the unpublished semaprax-toolchain package's
semaprax-full binary, renamed during staging. The standalone crates.io
package excludes private Native Rust package publication, Windows revision-store
host operations, Windows owned npm publication, and the held-parent staged publication behind
the full toolchain's new; its own new is the bounded route owned by
standalone project creation. Both binaries
dispatch doctor through the shared driver; the ordinary unavailable-profile
path grants no production tool authority. This distribution split does not
publish any private library crate or promote its platform support.
The platform script unpacks its completed archive and uses the unpacked
semaprax binary to run --version, version --json, check, and run
before the archive can be uploaded.
Pre-tag release checklist
A package-version release is a repository-wide consistency change, not only a root-manifest edit. Before committing the release, update and verify all of the following surfaces together:
The mechanical portion is automated and intentionally excludes historical evidence, frozen protocol identities, the human-written release record, and release-note curation:
python3 scripts/prepare-release.py --write --version 0.4.0 --date 2026-09-06
python3 scripts/prepare-release.py --check --version 0.4.0
The write mode requires a clean worktree, updates the declared current-version surfaces, and regenerates every repository-owned lockfile offline. Check mode also runs locked Cargo metadata against the root, example, and platform-test manifests. Review its diff and complete the human-owned items below.
- Set the root
Cargo.tomlpackage version and the unpublishedsemaprax-toolchainpackage version to the release version. - Update every exact path-dependency requirement in workspace-private crates
and isolated
platform-tests/*manifests. Regenerate the root, example, and platform-test lockfiles so each localsemapraxorsemaprax-toolchainpackage row agrees with its manifest. - Update executable version contracts: CLI text and JSON fixtures, daemon and agent transport assertions, doctor report fixtures, external-consumer manifest assertions, and the provisioned doctor release tag.
- Cut the accumulated
Unreleasedchangelog entries under a dated release heading. Update the README badge and release links, installation archive names and example output, documentation landing page, changelog summary, citation metadata, and CodeMeta version, date, description, and Rust floor. - Search the repository for both the old bare version and old
vtag. Review each survivor rather than replacing it mechanically: evidence logs, historical release records, dependency test vectors, crate dependency versions, and versioned WIT/schema identifiers are not package-release numbers. In particular,semaprax:private@0.3.0is a frozen WIT identity and does not follow the CLI package version. - Run the version, doctor, documentation, manifest/lockfile, package, and release-packaging contracts plus the repository's full quality profile. The tag must name the exact tested release commit, not a later documentation or lockfile repair.
Before tagging, inspect GitHub Actions at the job level. A Release gate
failure after a newer main push can be a synthetic consequence of cancelled
matrix jobs; it is not evidence that an executed test failed. Conversely, a
job whose overall conclusion is cancelled may have reached real test
failures before its timeout, so retain and inspect its log. The Windows
integration-0 shard includes the large Project harness: on the v0.4.0
candidate that harness ran 337 passing tests locally in about fifteen minutes,
while the Windows shard also spent substantial time compiling and running
other targets. Its 60-minute job budget and --nocapture diagnostics are
therefore release-safety controls, not permission to skip, weaken, or hide a
test.
A late syntax or AST addition must compile every workspace-private all-target consumer before release. Audit exhaustive statement matches and bounded walkers explicitly: adding a catch-all can make compilation succeed while still omitting multi-child traversal or introduced bindings from capacity accounting. Compiler matrix shards are the final check for host-private consumers, not the first place this audit should happen.
When a bounded carrier ceiling changes, audit its hostile and exact-boundary
fixtures in the same commit. An oversized-length fixture must derive
current limit + 1 from the owning constant; a frozen historical byte count
can become admissible and then exercise a later malformed-input diagnostic
instead of the intended fail-before-allocation path.
Release automation must also select UTF-8 explicitly for every repository text read and write. Python's ambient encoding is still a legacy charmap on some Windows runners; relying on it makes valid UTF-8 documentation fail before the version or changelog checks execute. The cross-platform workflow regression disables Python UTF-8 mode while keeping captured process output UTF-8 so this boundary remains exercised.
Exact CLI help ledgers preserve historical pins by removing each intentional additive usage line before comparing the older byte length and digest. A new command or operation must therefore add its exact line to that restoration inventory; changing an old known-answer merely to accept the larger current help page would discard the historical compatibility witness.
Use an annotated tag, matching the established repository convention, only
after the release commit is on main and the remote head still resolves to
that exact commit:
git tag -a v0.4.0 -m v0.4.0 <exact-release-commit>
git push origin v0.4.0
If another contributor advances main before the tag is created, rebase the
release commit, rerun the affected gates, and resolve the new exact commit. If
that newer head contains user-visible or gate behavior, move its changelog
entry into the release bucket before retesting; entries left under
Unreleased are intentionally omitted by scripts/release-notes.py.
Never move or recreate a published release tag to absorb later work.
Build-output selection
Both scripts reserve a fresh build-<target> directory under the requested
output root and pass its absolute path through Cargo's explicit --target-dir.
They copy both binaries only from that same build directory. Ambient
CARGO_TARGET_DIR or Cargo configuration cannot redirect the build while
leaving the packager to select stale binaries from the repository's target/.
The build directory, package stage, archive and smoke extraction paths must
all be absent, including dangling links, before any of those paths is created.
A Windows smoke extraction root is created exactly once by literal-path .NET
ZIP extraction after that absence check; it is not pre-created through the
PowerShell provider.
A new output root is still supported. Windows resolves relative output paths
from PowerShell's filesystem location, not the process working directory.
The Unix host query and version/run smoke checks also retain each command's
exit status: expected stdout cannot turn a failed command into release success.
These scripts assume a trusted, quiescent source checkout and output parent. Fresh-path checks are not retained-handle authentication against concurrent filesystem substitution. Failures leave build/staging residue for inspection; the scripts do not delete or retry over it. A successful archive still needs the exact-head release gate and real unpacked-binary execution described above.
tests/offline_package/release_packaging_unix.rs and
tests/offline_package/release_packaging_windows.rs author packaging-mechanics regressions
using deliberately fake toolchain executables. They distinguish fresh build
output from stale sentinels, exercise paths containing spaces and rejection
before build/staging effects, and run the actual archive/extraction scripts
when selected. Unix failures include commands that emit the expected stdout
but exit unsuccessfully. The Windows fixture also separates PowerShell's location from
the process working directory. The implemented mechanics regressions are HOSTED GREEN; fake tools do not
prove compiler execution,
daemon behavior, release provenance, or a successful product release.
cargo test --locked -p semaprax --test offline_package release_packaging_unix::
cargo test --locked -p semaprax --test offline_package release_packaging_windows::
Explicit unpacked-product acceptance
tests/release_archive_product_v1.rs is an opt-in local product gate over an
already unpacked archive. It does not build or extract that archive, install
anything, consult hosted CI, or fall back to a checkout compiler. Provision
SEMAPRAX_RELEASE_ROOT as an absolute native archive directory outside the
checkout, and SEMAPRAX_RELEASE_COMMIT as its expected 40-byte lowercase Git
label. The gate checks the closed plain-file inventory, canonical manifest,
fixed smoke source, documentation bytes and exact CLI version responses.
The calculator/daemon lane creates a fresh outside-checkout project, checks its literal template and repeated graph output, runs check/test/run/Web publication, and checks no-clobber behavior. Two finite default-v2 daemon sessions obtain the daemon's revision bindings, exercise read-only snapshot/check/graph/test, reject a stale revision, recover with a healthy query, and shut down. The daemon has no version command; this is behavior evidence, not daemon commit attestation.
The owned-frame lane publishes npm and Rust packages using the archived CLI, then runs the unchanged standalone Node and locked/offline Rust consumers for both the baseline and display-renamed Project. The canonical nine-case and supplemental 72-case corpora are shared with the ordinary frame suite. One shared test oracle replays source-bound descriptors, regenerates npm artifacts and reconstructs the native manifest from published bytes and the current test driver's provider. A differing older compiler must fail those comparisons, not silently adopt the driver's artifact bytes. This does not prove arbitrary cross-version compatibility.
Both lanes require a trusted, quiescent archive, source checkout, temporary parent and selected tool installation. Fresh fixtures and captures are retained. The owned-frame lane also retains its separately reserved Cargo build cache, including on failure: direct-child settlement does not justify deleting files that an unproven descendant might still use. Finite file-backed input/output and deadline polling bound capture reads and direct-child settlement attempts; they are not a hard disk quota, descendant containment, hostile same-principal isolation or a sandbox. No library or compiler gains authority from this test helper. Archive hashes and label agreement prove self-consistency only; the caller still owns provenance.
The separate generated-package preview checker can exercise an npm archive
without granting registry authority. After prepare, pass explicit absolute
--npm-bin and --node-bin paths together with --npm-tarball-consumer to
scripts/generated-package-release.py check. The checker packs in a private
snapshot, verifies the tar stream against the admitted payload, creates a
fresh offline consumer and lockfile, runs npm ci --ignore-scripts, verifies
the installed package's exact inventory and bytes, and only then imports it.
This is a local file-tarball consumer gate. It neither contacts npmjs.org nor
turns a synthetic fixture into evidence for a compiler-built package.
Run the ordinary admission/capture controls without provisioning an archive:
cargo test --locked --offline -p semaprax --test release_archive_product_v1
After setting the two archive variables above, select the real onboarding lane:
cargo test --locked --offline -p semaprax --test release_archive_product_v1 provisioned_archive_cli_and_daemon_work_outside_checkout -- --ignored --exact
The owned-frame lane additionally requires absolute provisioned NODE,
CLANG, SEMAPRAX_ARCHIVER and CARGO paths, a compatible Rust/linker/SDK
environment, and already cached consumer dependencies. On Windows retain the
existing SEMAPRAX_LINKER/SEMAPRAX_VCTOOLS policy. No dependency downloads are
performed and missing prerequisites fail the selected gate:
cargo test --locked --offline -p semaprax --test release_archive_product_v1 provisioned_archive_owned_frame_consumers_work_outside_checkout -- --ignored --exact
0.2.0 hosted release evidence
The annotated v0.2.0 tag resolves to exact commit
5f6fb9655fdec92c57ab71615cfd7bfa8cc76051. Its tag-triggered
workflow run 33608662244
completed successfully on 2026-09-02 with all 45 jobs green. That includes the
complete release-blocking Linux, macOS, Windows, Rust 1.88, dependency,
sanitizer, browser, Project, generated-Rust-consumer, desktop, Android, iOS,
and Component lanes. The blocking
release gate
then admitted all three host-built archive jobs and the final
publication job.
The published SEMAPRAX v0.2.0 prerelease contains exactly these release assets:
| Asset | Bytes | SHA-256 |
|---|---|---|
semaprax-v0.2.0-x86_64-unknown-linux-gnu.tar.gz | 12,064,489 | 955a892dd750cf8d783df583b39b65bf456d8832b55320781166c618a3ba325c |
semaprax-v0.2.0-aarch64-apple-darwin.tar.gz | 10,542,259 | aaa453e5b6226afed3d2ba25df2db9e46154968342305a6a793ebf56972efe80 |
semaprax-v0.2.0-x86_64-pc-windows-msvc.zip | 12,355,475 | 879d9b825fab8cff995ec73fc41992348a8ae2db5a120fcdcb85cd5976bf76dc |
SHA256SUMS | 333 | 2f433932cca89307441e42802527789a253e95ff6300084ff78bbd570f6c67b1 |
The SHA256SUMS contents independently agree with the three archive digests
reported by GitHub. Each archive job built on its advertised host, unpacked
its own output, and ran the packaged CLI version, JSON version, check, and
run smoke before upload. This is exact release-build and smoke evidence; it
does not mean every opt-in or ignored archive-consumer test ran, establish
cross-host byte reproducibility, or broaden any feature contract beyond its
owning specification.
Historical local archive evidence
A real local aarch64-apple-darwin archive was built offline from clean source
commit 177fccfd5f5ab08ac2c86da77046b47f5b4c22f1, using Rust 1.98 and the
unchanged optimized release profile. The packaging script's unpacked
--version, version --json, check, and run checks passed. The resulting
semaprax-v0.2.0-aarch64-apple-darwin.tar.gz has SHA-256
2c07c488a726824ff3b4b3a59379e1cd71a32bcbe93b5f7551283a621efa49c6.
Six Unix packaging-mechanics regressions also passed separately at that commit;
their fake tools do not contribute to the real compiler execution claim.
The new archive acceptance driver subsequently passed both explicitly selected lanes on this same retained macOS archive: calculator/Web and read-only daemon onboarding, plus Node 24.3 and Rust 1.98 consumers of the baseline and renamed owned-frame packages over both corpora. Five default harness tests also pass, including admission hostility and finite capture controls; the two actual archive lanes remain ignored unless explicitly selected. The existing ordinary frame suite and calibrated macOS ASan/UBSan gate pass after sharing the artifact oracle, and the two focused test targets pass Clippy with warnings denied.
This older retained local artifact is distinct from the later v0.2.0 tag and published archives recorded above. Its local acceptance results must not be relabeled as evidence from the tag commit; changes to either require separate evidence.
0.4.0 hosted release evidence
The annotated v0.4.0 tag resolves to exact commit
dfc15e2ddc818fa97744b5a9d69fd6108dd6a321. The accepted implementation and
release-regression baseline is HOSTED GREEN, as recorded in the
release baseline. The release-note length issue was
a publication issue, not an outstanding implementation or conformance gate.
The SEMAPRAX v0.4.0 prerelease
was published at 2026-09-10T10:31:03Z with the three archives below.
Historical Actions attempts keep their recorded conclusions; this acceptance
record does not relabel an attempt or invent a successful run identifier.
The published prerelease contains exactly these release assets (digests as
reported by the GitHub release API and matching the SHA256SUMS generated
during publication):
| Asset | Bytes | SHA-256 |
|---|---|---|
semaprax-v0.4.0-x86_64-unknown-linux-gnu.tar.gz | 17,922,460 | 21613bed94c9ed41d8ca67cee0924429fff18b1236198c58b16cb4bfd40786f9 |
semaprax-v0.4.0-aarch64-apple-darwin.tar.gz | 15,789,284 | 9b4ebf2bc0e9ca8bdb12db4dea795f9bf7b7b8cf73731f077c3bb80221acda60 |
semaprax-v0.4.0-x86_64-pc-windows-msvc.zip | 18,547,218 | e175bfc830f189229f0b9881df3afcf0bfb10939cd4a89c5b18bc138def54d07 |
Each archive job built on its advertised host, unpacked its own output, and ran
the packaged CLI version, JSON version, check, and run smoke before upload.
This is exact release-build and smoke evidence; it does not mean every opt-in
or ignored archive-consumer test ran, establish cross-host byte reproducibility,
or broaden any feature contract beyond its owning specification.
0.4.1 hosted release evidence
The annotated v0.4.1 tag resolves to exact commit
2ef043ba1b989f49b256e456f71fb6e89068bf33. Its tag-triggered
workflow run 34598481769
completed successfully on 2026-09-11 with all 56 jobs green, including the
blocking
release gate,
the three
release-artifact
builds, and the final
publication job.
The SEMAPRAX v0.4.1 prerelease
was published at 2026-09-11T14:46:53Z with the three archives below.
The published prerelease contains exactly these release assets (digests as
reported by the GitHub release API and matching the SHA256SUMS generated
during publication):
| Asset | Bytes | SHA-256 |
|---|---|---|
semaprax-v0.4.1-x86_64-unknown-linux-gnu.tar.gz | 17,959,385 | 02add301067c553d582a783a10a11193e83b504f90238777bf1b4de53d47d09d |
semaprax-v0.4.1-aarch64-apple-darwin.tar.gz | 15,832,200 | 35a818b826c0094541e1deb8a96953e561563ac454270ef5c3d3489a6f9fcbf5 |
semaprax-v0.4.1-x86_64-pc-windows-msvc.zip | 18,590,222 | d170a4948958e821d139334a7e29f5dd3a5626be9081cccb8427b92850757126 |
Each archive job built on its advertised host, unpacked its own output, and ran
the packaged CLI version, JSON version, check, and run smoke before upload.
This is exact release-build and smoke evidence; it does not mean every opt-in
or ignored archive-consumer test ran, establish cross-host byte reproducibility,
or broaden any feature contract beyond its owning specification.
scripts/release-reconcile.py --version 0.4.1 reports state=published-documented
with no problems against this section, README's claim, and CHANGELOG.md's
## 0.4.1 — 2026-09-11 heading -- see Release state and
reconciliation. Before this section and the
corresponding README citation were corrected, that same command reported the
exact class of drift #167 records: a "published" claim citing v0.4.0's date and
commit and no evidence section for v0.4.1 at all.
Publication boundary
Artifact matrix jobs retain read-only repository authority. Each receives only
the job-scoped OIDC and attestation permissions needed to generate GitHub's
SLSA provenance for its own already smoke-tested archive. The final
publish-release job alone receives contents: write and, after both
release-gate and every artifact-matrix child succeed, the separate OIDC
capability needed to sign the final aggregate provenance. It authenticates the
exact three-archive inventory, writes one SHA256SUMS, creates the final
manifest and provenance, and keylessly signs that provenance before publishing
a GitHub prerelease because SEMAPRAX remains alpha. No repository signing key
exists. The publisher derives the body with scripts/release-notes.py: it
selects only the tagged version's dated CHANGELOG.md section, stopping at the
next release heading, and surrounds it with the release nonclaims. A missing,
duplicate, or empty section fails the publication instead of silently creating
incomplete notes.
Canonical release manifest
scripts/release-manifest.py builds the aggregate, cross-archive release
manifest issue #167 asks for:
version, tag, commit, the required-check inventory (the exact
release-gate needs: list, read from .github/workflows/ci.yml rather than
restated), the artifact inventory (each admitted target's archive name,
platform, byte size, and sha256:-prefixed digest, computed from the archive's
real bytes), the prerelease flag, and a changelog-section digest (a
digest of the exact dated CHANGELOG.md section, so a later silent edit to
that section is detectable). Its schema is semaprax.release-manifest.v1,
distinct from the narrower per-archive semaprax.release-artifact.v1 manifest
scripts/package-release.sh/.ps1 embed inside each single archive: that
document is written before any sibling archive or its digest exists, so it can
only assert what one packaging run knows about itself. This one is built once,
after every target archive exists, from the sibling archives' actual bytes:
python3 scripts/release-manifest.py \
--version 0.4.1 --tag v0.4.1 --commit <exact-40-hex-commit> \
--archives-dir dist --output dist/release-manifest.json
It fails closed: a missing admitted-target archive, a tag/version/commit that
disagree, an archive digest that disagrees with a sibling SHA256SUMS, or a
missing/duplicate/empty changelog section all reject before anything is
written. --check PATH instead recomputes the manifest and diffs it
field-by-field against an existing one at PATH, naming every disagreement
(including exactly which artifact or field) rather than only reporting a
single pass/fail bit -- useful both as a local sanity check before tagging and
as the mechanism a recovery path can use to confirm a manifest written by an
earlier, possibly-interrupted attempt still agrees with what is actually on
disk.
Building or checking a manifest here is generated evidence, not a publication
decision: it creates no GitHub Release, calls no network API, and grants no
authority. tests/offline_package/release_manifest.rs covers the parser
(including a synthetic-workflow case pinned independently of ci.yml's
current contents), the digest/diff semantics, and an end-to-end CLI run over
real synthetic archives; it also cross-checks that the required-check
inventory this script derives from the live workflow agrees, as a set, with
the exact inventory tests/offline_package/ci_release_gate.rs pins.
Each producer runs the pinned GitHub attest-build-provenance action after its
smoke test and before the archive is retained, copying that action's bundle
into its deterministic release-attestation-<target>.json release asset. The
aggregate publisher requires exactly one non-empty archive and bundle for every
admitted target, freezes trusted_root.jsonl, then has gh attestation verify
check each exact archive against its corresponding held bundle and the pinned
repository, caller workflow, tag ref, source commit, and hosted-runner policy.
That check computes the archive digest from the downloaded bytes; a non-empty
but swapped, stale, or foreign bundle cannot reach SHA256SUMS, the final
manifest, provenance, or publication. Only after this archive-attestation gate
does the workflow write dist/SHA256SUMS, run scripts/release-manifest.py,
run scripts/release-provenance.py, and keylessly cosign sign-blob the final
provenance. It then derives and byte-replays release-signature-claim.json
from the exact provenance/bundle pair. The workflow uploads
release-manifest.json, release-provenance.json, the Sigstore
release-provenance.bundle, its deterministic
release-signature-claim.json, and the explicit trusted_root.jsonl snapshot
with the three archives and checksum file. The per-archive attestations do not
replace the signature over the final closed inventory, and the bundle does not
replace the archive attestations. This strict order prevents signing a mutable
or incomplete archive inventory. The trusted-root snapshot is release input
for both the aggregate archive-attestation gate and a later offline verifier,
not a claim that merely parsing it authenticates anything. Its streamed capture
is capped at the verifier's 4 MiB trusted-root limit. Workflow configuration is
not hosted evidence:
only a completed tag run and its immutable assets can establish that a release
was signed.
Release provenance and keyless signing (workflow wired; no hosted signed release yet)
Every SEMAPRAX release remains unsigned today -- see "Nonclaims" below.
Issue #168 defines the
policy and tooling the configured pipeline will use on its first qualifying
tag, and
docs/RELEASE-SIGNING-POLICY-V1.md is the owning versioned specification:
the threat model, the pinned trusted-identity policy (OIDC issuer,
repository, workflow path, per-tag subject), the semaprax.release-provenance.v1
and semaprax.release-signature-claim.v1 schemas, what independent
verification does and does not prove, and the remaining hosted-evidence and
identity-rotation checklist. The workflow has not been used as evidence for a
signed release.
scripts/release-provenance.py builds the provenance document from an
already-built scripts/release-manifest.py manifest -- copying its version,
tag, commit, prerelease flag, required-check inventory, and artifact
inventory verbatim rather than re-deriving them, plus a byte-exact digest of
the manifest and explicit (never ambient-environment) builder/toolchain/
host-class fields:
build_host_class names the runner that creates the aggregate provenance
document. It does not overwrite or summarize the Linux, macOS, and Windows
archive build hosts; each archive's separately published GitHub attestation
carries that producer identity.
python3 scripts/release-provenance.py \
--manifest dist/release-manifest.json \
--workflow-identity wavect/semaprax/.github/workflows/ci.yml@refs/tags/v0.4.2 \
--run-id <run-id> --run-attempt <run-attempt> \
--rustc-version "$(rustc --version)" --host-class github-hosted-ubuntu-24.04 \
--output dist/release-provenance.json
src/release_provenance.rs independently verifies the binding between a
manifest, a provenance document, and a claimed signature -- byte-exact
digest agreement across all three, plus the trusted-identity policy. Its
SigstoreOfflineVerifier then cryptographically verifies each exact held
Sigstore v0.3 bundle and subject against only the explicitly supplied
trusted_root.jsonl bytes. It performs no network access and does not update
or discover roots; acceptance is validity under that historical snapshot, not
proof of current revocation state. See docs/RELEASE-SIGNING-POLICY-V1.md for
the complete verification and nonclaim boundary.
tests/offline_package/release_provenance.rs covers the CLI end to end
against real synthetic archives, cross-checks the trusted-identity constants
against both the script and the policy document, and exercises every hostile
case issue #168 names (single-byte mutation to the manifest, an artifact, or
the provenance document; a provenance statement for a different commit/tag;
a missing or extra artifact; an unapproved repository/issuer identity; and a
signature claim replayed from another version).
Building or verifying either document here creates no GitHub Release, signs nothing, and grants no network, signing, or publication authority -- exactly like the manifest and dry-run tooling above. In particular, a successful local cryptographic replay does not establish that a SEMAPRAX release containing the held assets was ever hosted or published, does not refresh a stale root, and does not claim reproducible builds or production support.
Disposable dry-run harness and simulated recovery
scripts/release-publish-simulate.py is an explicit, offline simulation of
gh release create/gh release view used only by
tests/offline_package/release_dry_run.rs. It is not a GitHub client: it
makes no network call, ever, and a record it writes to its JSON "store" file
is a fixture, never release-promotion evidence. It exists so the create ->
(possible crash before create) -> retry -> published state machine the real
publish-release job implements can be exercised mechanically, in a
disposable scratch directory, without hosted CI or GitHub credentials.
release_dry_run.rs builds synthetic archives and a real
release-manifest.py manifest in a disposable directory, renders real release
notes with release-notes.py, and then exercises:
- The complete flow: manifest -> notes -> simulated publish -> exactly one
simulated release with exactly three assets, and a retried
create(an operator re-running an already-succeeded job) is refused as a duplicate rather than appending a second copy. - A simulated mid-publish failure and its recovery: archives and the
manifest exist, but the process is interrupted before any simulated Release
is ever created -- the exact gap between
release-artifactssucceeding andpublish-releasecompleting. Recovery (a second attempt, as if the underlying failure were fixed and the job re-run) produces exactly one clean release with no duplicate assets and no residue from the interrupted attempt; a second recovery attempt is refused the same way an already-successful publish is. This is the mechanismbd777928's commit message noted was missing: "The failure and recovery path" below remains a documented manual procedure for a human operator, and this harness is what now exercises its shape mechanically rather than only in prose. - A tampered asset: an archive whose bytes disagree with the manifest's own recorded digest/size at publish time is refused, and no store record is written for the refused attempt.
This harness does not re-execute the 22 hosted release-blocker CI jobs
themselves (release-gate's needs: list, above); that remains hosted
evidence recorded in this file's dated ## X.Y.Z hosted release evidence
sections, not something a disposable local harness can honestly stand in for.
Release state and reconciliation
scripts/release-reconcile.py is a read-only check, not a release step. It
never creates a tag, calls the GitHub API with write authority, or edits a
file; it only reads the checkout (and, with --live, the public GitHub API)
and reports. Run it after any edit that touches a version, a tag, or a
"published" claim, and always before tagging:
python3 scripts/release-reconcile.py --version 0.4.1
Locally observable candidate states
The tool reports exactly one of four states, derived only from files already in the checkout and the local Git tag list:
| State | Meaning |
|---|---|
no-candidate | No local vX.Y.Z tag exists for the version. |
tagged-unpublished | The tag exists, but docs/RELEASE-PROCESS.md has no ## X.Y.Z hosted release evidence section for it yet. This is exactly the shape a release that failed anywhere between tagging and the documentation step leaves behind -- CI still ran and may have published a GitHub Release, but nothing in this repository has recorded that as accepted evidence. |
published-documented | The tag exists, an evidence section exists, and the section's recorded commit agrees with the local tag's commit. |
inconsistent | A problem was found: an evidence commit disagreeing with the local tag, or README claiming a version is published while its state is not published-documented. Exit code is nonzero. |
These four states intentionally stop short of the finer distinction the
Implementation-sequence text in issue
#167 describes
(candidate-created, gate-running, gate-accepted, artifacts-built).
Telling those apart requires the live Actions run's per-job status, which
static repository state cannot show; reporting them from local files alone
would be an unearned claim. Read the tag's workflow run directly
(gh run list --branch vX.Y.Z) for that distinction, or extend --live if a
durable local record of it becomes necessary.
This deferral was revisited alongside the canonical manifest and dry-run work
above and reaffirmed: the manifest and the simulated publish/recovery harness
both give a disposable stand-in for gate-running/artifacts-built, never
a durable record of the live run's state, which still needs --live or
gh run list against the exact tag. Building the finer state machine over
local files alone would still be an unearned claim; it remains correctly out
of scope for this tool.
The failure and recovery path
A release that fails between tagging and the documentation step is safe by
construction, not by this tool's vigilance. README and docs/RELEASE-PROCESS.md
are edited by hand as the last step of the pre-tag release
checklist and its post-tag counterpart below;
until that edit lands, nothing in the repository claims the new version is
published, so a stalled or failed tag run leaves the checkout in
tagged-unpublished (or no-candidate, if the tag push itself never
succeeded) with zero problems reported. That is the "explicit candidate
state, no misleading claim" property: it is the absence of a doc edit, kept
that way by never writing the evidence section until the Release is real.
Recovery:
- Confirm what actually happened:
gh run list --branch vX.Y.Zfor the workflow conclusion, thengh api repos/wavect/semaprax/releases/tags/vX.Y.Z(404 means no Release was created; a non-404 body means one was, and itsassetsarray is the ground truth for the next step). - If no Release exists, fix the underlying failure, and either push a new
tag once the fix is verified (never move or recreate the failed tag; see
Tag admission) or, if the tag itself is unrecoverable,
record that abandonment in
CHANGELOG.mdunder the version's own heading rather than silently reusing the number. - If a Release exists but publication otherwise looked incomplete, run
scripts/release-reconcile.py --version X.Y.Z --liveto fetch its realtag_name/published_at/asset list read-only, andscripts/release-reconcile.py --version X.Y.Z --archives-dir <dir>with the real assets downloaded into<dir>(gh release download vX.Y.Z) to cross-check every archive's ownrelease-manifest.jsonagainst the tag's commit and version before trusting it. Only once both report no problems, add the## X.Y.Z hosted release evidencesection and correct README's citation together, in the same commit. - Never write the evidence section, and never change README's "published tag" citation, from anything other than a real, already-published Release confirmed by step 1 or step 3. A candidate state is not itself publication-worthy evidence, and this repeats the rule already stated for evidence capsules generally: they carry no authority of their own.
Nonclaims
The archives are unsigned and are not notarized. No cross-host reproducible build is claimed. The deterministic manifest does not make the enclosing archive byte-reproducible. SHA-256 checksums are integrity facts, not signatures, provenance, or publisher authentication. The historical v0.2.0 publication completed only the tagged-artifact milestone recorded as WP-04. A later publication is likewise only its bounded release record: it does not promote any completion-matrix row or establish production readiness, a stable language ABI, a stable public protocol, or safety-critical suitability.