Releasing CelestialCalendar
The release workflow freezes one candidate for GitHub Release, PyPI, and npm. It accepts three explicit producer
run IDs and rejects a tag, commit, run, artifact, or registry version that does not match exactly. npm-primary
(@0xf3cd/celestial) and npm-alias (celestial-calendar) are separate from the PyPI homonym.
Release
The one-time settings must already be in place. Pending source work is not a release: final API freeze, candidate selection and authorization are separate acceptance steps.
-
Prepare the version in a pull request merged before the tag:
- set
BUILD_VERSIONinproject.py; the Python and npm package versions derive from it, so do not editpyproject.tomlor thepackage.jsonfiles; - replace
docs/RELEASE_NOTES.mdwith the notes of this release only; - finalize the first version heading and entries in
docs/CHANGELOG.md, removing its unreleased disclaimer only when the release is authorized. Keep pending entries undated; historical dates come from tagged commits; - check historical "new in" notes and source-versus-published documentation warnings. The READMEs describe the current source without repeating a global version number.
Then confirm
project.py,docs/RELEASE_NOTES.md, and the firstdocs/CHANGELOG.mdentry all name the same version. - set
-
Create and push the protected
vMAJOR.MINOR.PATCHtag from a commit already contained inmain. -
Dispatch
build_and_test.yml,wasm.yml, andpython-wheel.ymlon that tag. Wait for all three to succeed and record their run IDs. Each must be aworkflow_dispatchrun at the tagged commit. -
Finish the remaining steps within the producer artifacts' 30-day retention window.
Dispatch each producer once:
tag=vMAJOR.MINOR.PATCH
commit=$(git rev-parse "$tag^{commit}")
gh workflow run build_and_test.yml --ref "$tag"
gh workflow run wasm.yml --ref "$tag"
gh workflow run python-wheel.yml --ref "$tag"
After they finish, list the dispatched runs at that exact commit. Record the databaseId for one successful run of
each workflow, then inspect each selected ID before continuing:
tag=vMAJOR.MINOR.PATCH
commit=$(git rev-parse "$tag^{commit}")
for workflow in build_and_test.yml wasm.yml python-wheel.yml; do
gh run list --workflow "$workflow" --event workflow_dispatch --commit "$commit" \
--json databaseId,workflowName,event,headSha,status,conclusion,url
done
gh run view RUN_ID --json databaseId,workflowName,event,headSha,status,conclusion,url
The selected runs must report workflow_dispatch, the value of $commit, completed, and success. A displayed
run number is not a run ID; use databaseId in the release inputs.
Frozen Candidate
Dispatch the release workflow on the tag with the three recorded IDs:
gh workflow run release.yml --ref vMAJOR.MINOR.PATCH \
-f native_run_id=<native-run-id> \
-f wasm_run_id=<wasm-run-id> \
-f python_run_id=<python-run-id>
Preparation validates the protected tag, main ancestry, producer runs, artifact API digests, archive contents, and documentation. It then stages one candidate and classifies both npm identities:
- an absent version in an existing, correctly identified package requires OIDC publication;
- an absent version whose package also returns 404 records
bootstrap_required; - a byte-identical version is a verified no-op;
- any metadata, integrity, or byte mismatch stops before GitHub Release creation.
GitHub Release publishes first. Approve the pypi and npm jobs only after checking the candidate summary. The
final unprivileged job requires exactly four PyPI wheels and two npm tarballs, compares registry hashes and bytes,
and clean-installs PyPI plus npm-primary, npm-alias, and the npm pair from their respective registries.
The candidate has 30 files: 14 GitHub assets, four PyPI wheels, two npm tarballs and ten evidence files
(29 files recorded in the manifest, excluding the manifest itself). The WASM archive has ten members:
the raw module/glue, LICENSE/notices, and both tarball/metadata/SHA-256 triples.
The npm job checks out only the protected release commit and installs the hash-locked Requests closure to run the versioned classifier. Immediately before each publication it rechecks that package against the same frozen candidate, primary first and alias second. Exact bytes are a verified no-op; conflicts, invalid JSON, HTTP 429, and transport failures stop rather than authorizing publication. Verification alone polls indexing and transient transport states to a fixed ceiling and reports which of npm-primary, npm-alias, or PyPI is still pending.
After success, inspect the immutable GitHub Release, asset inventory and verify_registries results.
Do not repeat those installations manually on the same bytes merely to obtain another green result:
| Check | Automatic execution | Manual responsibility |
|---|---|---|
| Native archive/runtime floor | Native producer validates installed archive members and measured requirements | Confirm selected run/commit and candidate hashes |
| Four repaired Python wheels | Producer installs each same wheel on minimum/current interpreters; release verifies registry bytes and installed Python acceptance | Read all four wheel identities and successful acceptance evidence |
| Primary and alias npm tarballs | WASM producer tests exact named tarballs; release verifies registry bytes and installs each name alone and the same-version pair | Read both frozen package identities and all three registry-consumer results |
| Authorization | Protected environments pause publication | Approve only the manifest's tag, commit, producer IDs, filenames, sizes and hashes |
An exceptional recovery that cannot reach verify_registries must perform its missing checks against the
original frozen candidate, as described below. A no-op publication proves byte identity, not a live OIDC publish.
Recovery
- If publication reports
bootstrap_required, use the historical bootstrap appendix as context and obtain separate account/publication authorization; do not substitute newly built bytes. - If immutable GitHub Release creation fails, inspect the release first. Delete it only if it is still a draft, then rerun failed jobs against the same workflow artifact.
- An unambiguous PyPI failure before registry acceptance may use
gh run rerun RUN_ID --failedafter reviewing the evidence. - For npm publication failure, inspect the evidence and retry only the npm job using
gh run rerun --job NPM_JOB_ID. Fresh classification retains successful exact package bytes and continues with the remaining package. This also handles a command that failed after npm accepted its exact bytes. - The unprivileged
verify_registriesjob is idempotent. A transient verification failure after publication may usegh run rerun RUN_ID --failed; the publication jobs have already succeeded and are not rerun. - If a PyPI publish command fails ambiguously but registry queries prove the exact candidate is present, leave that
publication job red. Do not rerun it or use
skip-existing; record the recovery and complete consumer validation manually. - After any irreversible job succeeds, never use "Re-run all jobs". The original run is the identity of the frozen candidate.
Ambiguous PyPI success: original-candidate verification
For the terminal PyPI ambiguous-success case, verify and consume the same candidate manually from a clean checkout of
the release tag. RUN_ID is the release workflow run, not a producer run:
(
set -euo pipefail
tag=vMAJOR.MINOR.PATCH
version=${tag#v}
commit=$(git rev-parse "$tag^{commit}")
test ! -e candidate
gh run download RUN_ID --name celestial-release-candidate --dir candidate
python3 -m venv registry-verify
registry-verify/bin/python -m pip install --require-hashes --only-binary=:all: -r Requirements-producer.txt
registry-verify/bin/python toolbox/registry_verifier.py verify \
--candidate candidate --version "$version" --commit "$commit"
python3 -m venv registry-python
registry-python/bin/python -m pip --isolated install \
--index-url https://pypi.org/simple --only-binary=:all: --no-cache-dir --no-deps \
"celestial-calendar==$version"
root=$(pwd)
work=$(mktemp -d)
(cd "$work" && "$root/registry-python/bin/python" "$root/bindings/python/test/run_all.py")
node bindings/javascript/test/registry/registry_consumer_test.mjs "$version"
)
Rehearse environment self-approval and same-run failed-job artifact recovery after workflow changes and before the
final release tag. The instrument is .github/workflows/release-rehearsal.yml: push a disposable vX.Y.Z-rehearsal
tag — never a plain vX.Y.Z, which is release identity — (the tag rulesets cover it: create via the Admin bypass,
remove afterwards via the ruleset recovery path recorded in #215), dispatch the workflow on that tag, approve both
environment gates against the staged manifest, then rerun the deliberately failed job with
gh run rerun RUN_ID --failed and confirm it recovers the same-run artifact. The v0.6.0 npm no-op cannot rehearse a
live OIDC publish.
One-Time Setup
Before creating a release tag:
- Add an active tag ruleset for
v*. Testgithub.ref_protectedwith a disposable matching tag, then remove it. - Create
pypiandnpmenvironments. The maintainer is the required reviewer and may self-approve after reading the candidate manifest; disable self-review prevention and admin bypass. Restrict deployment tags tov*.*.*. - Register the PyPI pending publisher with owner
0xf3cd, repositorycelestial-calendar, workflowrelease.yml, and environmentpypi. - Register separate Trusted Publishers for both npm names with the same owner, repository and workflow,
using environment
npm. Package existence alone does not prove publisher configuration or live OIDC readiness.
Environment approval is a deliberate manifest check, not independent review.
Appendix: completed npm bootstraps
Historical npm v0.6.0 Bootstrap
npm cannot register a Trusted Publisher before a package exists. The primary bootstrap selected the sole
@0xf3cd/celestial@0.6.0 tarball named by npm-pack.json from the selected celestial-wasm run,
verified npm-pack.sha256, and published those exact bytes with --access public --ignore-scripts.
The same WASM run ID supplied the release workflow. No repack or glob selection was permitted.
The short-lived credential was revoked before registering the Trusted Publisher; credentials must never
appear in commands, shell history, output or the repository.
v0.6.0 has no OIDC-generated provenance. Its later byte-verified no-op proves package identity; the first live primary OIDC publication is v0.6.1.
Alias v0.7.0 Bootstrap
The alias bootstrap followed npm-primary 0.7.0 and required separate account/publication authorization.
It used the original release run's celestial-release-candidate, selecting exactly one plain
celestial-calendar@0.7.0 filename from candidate/evidence/npm-alias-pack.json and checking the
tarball in candidate/npm against candidate/evidence/npm-alias-pack.sha256. Both frozen tarballs
stayed unchanged. Publication used --access public --ignore-scripts; the short-lived credential
was revoked immediately without printing or storing it in the repository.
After registering the alias Trusted Publisher for this repository's release.yml and npm environment,
only the original run's npm job was retried. Fresh classification verified both packages and skipped
exact versions. That no-op proves bytes, not live alias OIDC publication; account readiness and the
first live alias OIDC publication require separate evidence.