release: prepare content-addressable 0.1.0 - #74
Merged
Merged
Conversation
Replace the fragmented, grep/sed-based version check (which read only two of the
three manifests and whose `${ver/-/}` PEP 440 mapping was wrong — it produced
`0.1.0alpha.1`, not `0.1.0a1`) with one repository-owned tool that both local dev
(`just verify-release`) and CI call.
`scripts/verify_release.py` (real TOML parser, fail-closed) reads all THREE
version declarations — root `Cargo.toml`, `content-addressable-py/Cargo.toml`, and
the root `pyproject.toml` (the actual path; `content-addressable-py/pyproject.toml`
does not exist) — and proves: both Rust manifests agree; `pyproject` is the
canonical PEP 440 of the Rust version (0.1.0->0.1.0, 0.1.0-alpha.1->0.1.0a1,
-beta.2->b2, -rc.3->rc3); and an optional tag is exactly `v<rust-version>`.
Missing files, duplicate-key/unparsable TOML, non-string versions, disagreement,
unsupported/ambiguous prereleases, and build metadata all exit non-zero with a
file-and-value diagnostic.
`tests/test_verify_release.py` (27 tests, run by CI's python job) covers valid
final/alpha/beta/rc, malformed, mismatched, missing, duplicate-key, and
unsupported cases. `just verify-release [tag]` is the single call site; the CI
release gate will call the same recipe (no second mapping in YAML/shell).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fact tests
Rebuild release.yml around ONE deterministic gate that both registries depend
on. Neither `cargo publish` nor PyPI publication can begin until `release-gate`
passes, and `release-gate` requires every releasable condition:
- provenance: `just verify-release <tag>` (the canonical three-manifest tool) +
the tag points at HEAD + HEAD is contained in the protected branch
`origin/main` (a tag on an unmerged/detached commit is refused). Full-history
checkout; does not trust the ref name alone.
- rust-gate: `just check` + release-only supersets (`cargo clippy --workspace
--all-targets --all-features -D warnings`, `cargo test --workspace --locked`,
`--all-features --locked`, `cargo doc --workspace --all-features` with
`-D warnings`) + `cargo publish -p content-addressable --locked --dry-run`.
- msrv: build + test on the pinned Rust 1.85.
- build-wheels / build-sdist: gated behind provenance + rust-gate.
- install-smoke: downloads each platform's BUILT wheel, installs it into a clean
venv on {Linux x86_64, Linux aarch64, macOS universal2, Windows x64} x
{py3.9, 3.12, 3.13}, asserts the import resolves to site-packages (NOT the
source tree), and runs the golden-vector + fail-closed-parsing + README tests
against the INSTALLED package. `twine check` is kept but is no longer treated
as an install test.
- sdist-smoke: installs the sdist (build-from-source) and runs the same tests.
Both publish jobs `needs: [release-gate]` and consume the EXACT validated
artifacts (download-artifact; no rebuild). Previously `publish-crate` depended
only on the old grep-based `verify`, so a broken Python build or a failing test
could not stop a half-completed dual-registry release.
Security: top-level `permissions: {}` with explicit least-privilege per job
(`contents: read`; PyPI `id-token: write` only; no write scope unused). Every
third-party action is pinned to a full commit SHA with the tag in a trailing
comment. Both publish jobs run in protected environments (`pypi`, `crates-io`).
actionlint + shellcheck clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supply-chain + least-privilege hardening for the CI workflow, matching the posture already applied to release.yml. Action pinning Every third-party action in ci.yml was pinned from a moving ref (@v4, @stable, @1.85, @v2, @v5) to a full commit SHA with a human-readable version in a trailing comment. A moving tag can be silently repointed at malicious code; a SHA cannot. The SHAs match release.yml exactly so the two workflows stay in lockstep: actions/checkout @11bd719 # v4.2.2 dtolnay/rust-toolchain @2c7215f (toolchain: stable / "1.85") Swatinem/rust-cache @98c8021 # v2.8.0 actions/setup-python @a26af69 # v5.6.0 dtolnay/rust-toolchain now takes an explicit `toolchain:` input (the ref name is no longer the toolchain selector once pinned to a SHA). Least privilege Top-level `permissions: {}` drops the default GITHUB_TOKEN to nothing; each job re-grants only `contents: read`. No CI job writes to the repo, publishes, or needs an OIDC token, so nothing else is granted. Dependabot New .github/dependabot.yml opens weekly reviewable PRs for the github-actions ecosystem (grouped) plus cargo and pip, with rebase disabled so it never force-pushes over a branch under review. SHA pins are safe but frozen — this is how they pick up upstream security fixes without landing silently. Validation: actionlint clean on ci.yml and release.yml; dependabot.yml parses as a valid v2 config; no moving-ref action pins remain in either workflow. Co-authored-by: Claude <hartsock@users.noreply.github.com>
Rename the two experimental, default-off cargo features so their names carry
the stability signal at the call site:
merkle -> unstable-merkle (src/merkle.rs, MerkleNode<T>)
store -> unstable-store (src/store.rs, NodeStore seam)
A developer copy-pasting `features = ["merkle"]` from an example had no signal
that the serialized node bytes / trait API are NOT frozen for 0.1.x. The
`unstable-` prefix is the Cargo convention (cf. `unstable-*` in tokio, serde)
for "this may change under you"; the frozen 0.1.x contract is exactly the
default feature set, and now nothing outside it can be enabled without typing
`unstable-`.
No compatibility aliases: there is no released 0.1.0 yet, so a clean rename is
correct — leaving `merkle`/`store` as aliases would re-introduce the very
footgun this removes. Pre-1.0 alpha, this is not a breaking change to any
published surface.
Scope of the rename (feature FLAG only):
- Cargo.toml [features] keys + their doc comments
- every #[cfg(feature = ...)] / #![cfg(feature = ...)] in src/ and tests/
- README install snippets, the compatibility table, and prose that names the
feature to enable
- src/lib.md, docs/STABILITY.md feature sections
- justfile + .github/workflows/ci.yml explanatory comments
Deliberately UNCHANGED (not the flag): the Rust module paths
`content_addressable::merkle` / `::store`, the type names (`MerkleNode`,
`NodeStore`, ...), the rustdoc intra-doc links `[`merkle`]` / `[`store`]` (they
resolve to the still-named modules), and the crates.io "merkle" domain keyword.
The feature that ENABLES a module and the module's NAME are intentionally
distinct (the common Cargo pattern), which also makes the enable-line read
clearly: `unstable-store` turns on the `store` module.
Validation: `just check` green (fmt, clippy -D warnings --all-features, test,
rustdoc -D warnings --all-features, leaf guard). `cargo test` (default) proves
both features stay OFF; `cargo test --all-features` compiles + runs both under
the new gate names (34 core + 29 store + 6 merkle/store integration tests pass).
Co-authored-by: Claude <hartsock@users.noreply.github.com>
RELEASING.md described a two-file, single-`verify`-job release that no longer
matches the hardened workflow, and its prerelease example used a spelling the
release gate now REJECTS. Rewrite it to the actual, gated reality.
Corrections
- "two version strings" -> THREE files (root Cargo.toml, the PyO3 crate's
Cargo.toml, and pyproject.toml). The table already listed three; the prose
contradicted it.
- Prerelease spelling: the old example `0.1.0-rc1` / tag `v0.1.0-rc1` is
rejected by scripts/verify_release.py (SemVer prerelease numbers require a
dot). Corrected to `0.1.0-rc.1` -> `0.1.0rc1`, tag `v0.1.0-rc.1`. Every
SemVer->PEP 440 pair in the doc was checked against the real tool.
Added
- The full pre-publish gate DAG and a job-by-job table: nothing publishes
unless provenance + rust-gate + msrv + wheels + sdist + install-smoke +
sdist-smoke all pass through the single `release-gate` aggregator.
- Provenance/branch-containment: the tag must be `v<Cargo.toml version>`,
`HEAD` must be the tag commit, and that commit must be contained in `main`.
- The canonical SemVer->PEP 440 table, pointing at verify_release.py as the
single source of the mapping (no second copy in YAML/shell).
- Local dry-run (`just verify-release [v<version>]`) and the
`workflow_dispatch` no-publish dry-run.
- Partial-outage recovery: re-run only the failed publish; both consume the
already-validated artifacts, so a re-run cannot emit different bytes. Never
re-tag or hand-publish to patch a partial publish.
- The second deployment environment `crates-io` (publish-crate uses it; the
old doc only mentioned `pypi`), branch/tag protection, and a human-only
configuration checklist for the settings CI cannot assert.
- Classifier policy: keep `Development Status :: 3 - Alpha` until there is
evidence for Beta/Stable; do not claim Production/Stable to look finished.
No version strings were changed — the version bump is deliberately a separate
commit/PR (documented here as such), keeping this release-hardening work off the
0.1.0 bump.
Co-authored-by: Claude <hartsock@users.noreply.github.com>
CLAUDE.md hook-parity rule: a pipeline edit triggers a hook audit. The feature rename (merkle -> unstable-merkle) updated the CI `test` job's step names; the pre-push hook that mirrors that job still echoed the old `merkle` feature name in three comment/echo lines. The command (`cargo test --all-features`) is unchanged, so this is text-only parity — but the hook must read as the local equivalent of the pipeline. shellcheck clean. Co-authored-by: Claude <hartsock@users.noreply.github.com>
First final release of the frozen ContentId contract. Bump all three
authoritative version declarations from the alpha to 0.1.0:
Cargo.toml 0.1.0-alpha.1 -> 0.1.0
content-addressable-py/Cargo.toml 0.1.0-alpha.1 -> 0.1.0
pyproject.toml 0.1.0a1 -> 0.1.0
Cargo.lock refreshed via `cargo update -p ... --precise 0.1.0` (Cargo tooling,
not a hand edit). The canonical verifier agrees:
just verify-release -> Rust '0.1.0' == PEP 440 '0.1.0'
just verify-release v0.1.0 -> OK (tag matches)
just verify-release v0.1.0-rc.1 -> FAILS closed (tag mismatch)
Isolated version-only commit; de-alpha documentation, the classifier bump, and
the changelog follow in a separate commit.
Co-authored-by: Claude <hartsock@users.noreply.github.com>
Make the release-facing documentation and metadata describe a shipped, final
0.1.0 instead of an in-progress alpha, and resolve one doc contradiction.
Metadata
- pyproject.toml classifier: Development Status 3 - Alpha -> 4 - Beta. Beta,
not Production/Stable: the core is frozen for 0.1.x but the optional
unstable-* features are still moving, and there is not yet a stable track
record to claim Production/Stable.
De-alpha prose (now "0.1.0, the first release that freezes the core contract")
- README status section + all install snippets (0.1.0-alpha.1 -> 0.1.0).
- docs/STABILITY.md package-status line.
- src/lib.md crate-level stability section.
- src/merkle.rs: the Merkle-byte-freeze target "0.1.0-rc1" -> "a later 0.1.x
release" (merkle bytes stay unstable in 0.1.0, honestly).
- Cargo.toml unstable-merkle comment.
- RELEASING.md classifier/maturity note (now describes 4 - Beta).
- tests/common/mod.rs: corrected an inaccurate "ContentId serde not frozen
during 0.1.0-alpha" aside — the ContentId serde repr IS frozen; the shared
JSON vector format simply carries no native CID type, so it is exercised in
the per-language suites.
Doc contradiction resolved
- docs/MERKLE_ROADMAP.md framed "0.1.0 = catalog complete" on a 0.0.x
micro-release channel that never happened. That framing is now marked
SUPERSEDED: 0.1.0 shipped as the FROZEN CORE contract with the catalog
riding 0.1.x behind the experimental unstable-* features. Historical ladder
(approved 2026-07-21) is preserved with the superseding note.
CHANGELOG.md (new) — 0.1.0 release notes: first stable-contract release; CIDv1 +
dag-cbor + BLAKE3 32-byte profile; canonical dag-cbor; frozen Rust/Python
presentation; golden cross-language vectors; fail-closed profile validation at
every ingress; checked/unchecked byte constructors; verification behavior; MSRV
1.85; Python 3.9+ abi3; unstable-merkle/unstable-store status; the
alpha-to-final feature-rename migration (merkle->unstable-merkle,
store->unstable-store, no aliases); the incompatible SHA3-256/pickle sibling
warning; package-name vs import-name table; known limitations.
`just check` green at 0.1.0 (fmt, clippy -D warnings --all-features, test,
rustdoc -D warnings, leaf). No frozen vectors changed.
Co-authored-by: Claude <hartsock@users.noreply.github.com>
The rust-gate's `cargo doc --workspace --all-features --no-deps` step failed (exit 101): the binding crate's lib is deliberately named `content_addressable` (so the installed Python import name is correct), which collides with the core crate's lib of the same name — rustdoc refuses to write two crates to the same output path. The gate would have gone red on its first real run; only `just check` (non-workspace doc, core crate only) had been exercised before. Fix: `[lib] doc = false` on content-addressable-py. Its API is documented on the Python side, not via rustdoc, and the collision made it undocumentable via `--workspace` regardless. `cargo doc --workspace` now documents only the core crate (exit 0). No effect on maturin, clippy, or `cargo test --workspace` (the binding is still built and tested). Co-authored-by: Claude <hartsock@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
release: prepare content-addressable 0.1.0
First final release — freezes the core content-addressing contract for the
0.1.xline and publishes the Rust core to crates.io + the Python distributionto PyPI via the hardened, gated release workflow.
No package, tag, or GitHub release has been published by this PR. It only
prepares
main.v0.1.0is cut after this merges and a human authorizes it.Provenance
origin/main):7678df4db8d66b93290fe020d3dcaa91169a1ab7release/0.1.0):195eb7d3d0edc410dc0036b67788974f24b3325d0.1.0is absent from crates.io (only0.1.0-alpha.1) and PyPI (only0.1.0a1). Nov0.1.0tag exists.Why this PR is large: it also lands the release hardening
The hardening machinery this release depends on had not yet landed on
main, soit is folded in here (per the release runbook). Two concerns, clearly separated
in the commits:
e45a216…a6e8a6e,195eb7d): canonical versionverifier, common pre-publish gate + provenance, SHA-pinned actions +
least-privilege permissions + Dependabot, experimental-feature rename, aligned
RELEASING, and a fix so
cargo doc --workspace(a gate step) actually passes.17b9cfc,a4846c4): the three versionstrings, classifier, README/STABILITY/lib docs, CHANGELOG.
Stable contract (frozen for
0.1.x)CIDv1 · dag-cbor (
0x71) · BLAKE3 multihash (0x1e) · 32-byte digest ·base32-lower
bafyr4i…string · frozen binary envelope + bare-digest accessors ·frozen Rust↔Python parity (golden
tests/vectors.json, byte-identical to thebaseline — no frozen-vector drift) · fail-closed CID-profile validation at
every ingress (Rust
FromStr/TryFrom<Cid>/from_bytes/serde; Pythonparse/from_bytes).The optional features were renamed to make their instability unmistakable
(no compatibility aliases):
features = ["merkle"]features = ["unstable-merkle"]features = ["store"]features = ["unstable-store"]Module paths (
content_addressable::merkle,::store) and type names areunchanged — only the feature flags moved. Default (frozen-core) users are
unaffected. Their bytes/API remain not frozen in 0.1.0.
Version changes
Cargo.toml,content-addressable-py/Cargo.toml→0.1.0;pyproject.toml→0.1.0; classifier3 - Alpha→4 - Beta.Cargo.lockrefreshed via cargo.just verify-release v0.1.0→ OK; a wrong tag fails closed.Test evidence (local, Linux x86_64 / py3.12)
cargo fmt --check,clippy --workspace --all-targets --all-features -D warnings,cargo test --workspace [--all-features] --locked,cargo doc --workspace --all-features --no-deps -D warnings,cargo publish -p content-addressable --locked --dry-run,actionlint,shellcheck, MSRV 1.85 build+test — all green.Packaging evidence
maturinwheel (cp39-abi3manylinux_2_34_x86_64) + sdist built;twine checkPASSED both. Clean-venv install of wheel and sdist: import resolves to
site-packages, task smoke passes, and the golden + profile-validation + README
suites pass 45/45 against each installed artifact. Contents scanned — no
secrets/paths/junk; LICENSE present; wheel ships a CycloneDX SBOM.
Local artifact SHA-256 (informational — the published checksums come from the
CI-built wheels):
content_addressable-0.1.0-cp39-abi3-manylinux_2_34_x86_64.whl—19dfe5ce2ea2c752614b18203487b20092a3557b5a302814f4523fcfd2900bfecontent_addressable-0.1.0.tar.gz—eda8774cdd45799ee4fd8bfdd95c768618124c7d86bdd8e2b526a6aef4057096The full py3.9/3.13 × macOS/Windows/aarch64 matrix + sdist is exercised by the
release workflow's
install-smoke/sdist-smokejobs (CI-only).Release workflow evidence
.github/workflows/release.yml— actionlint clean.provenanceasserts tag ==v<version>, tag == HEAD, and HEAD contained inmain. Bothpublish-pypi(OIDC, env
pypi) andpublish-crate(envcrates-io)need: [release-gate],which needs the whole DAG. A
workflow_dispatchdry-run against this branch isbeing run (no publish).
Registry prerequisites (human-only — CI cannot assert)
hartsock/content-addressable, workflowrelease.yml, envpypi.CARGO_REGISTRY_TOKENscoped tocontent-addressable.pypiandcrates-iowith required reviewers;mainbranch protection;v*tag protection.Do NOT
Do not merge or cut
v0.1.0without explicit authorization. Draft/hold status isgoverned by the release runbook.
Refs #14