name: release
on:
push:
tags:
- "v*" workflow_dispatch:
permissions: {}
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
jobs:
provenance:
name: provenance (version + tag + branch containment)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
fetch-depth: 0 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 with:
python-version: "3.12"
- uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc with:
tool: just
- name: fetch the protected branch + tags
run: |
set -euo pipefail
git fetch --no-tags --prune origin +refs/heads/main:refs/remotes/origin/main
git fetch --tags --force origin
- name: verify version agreement (and tag on a real release)
shell: bash
run: |
set -euo pipefail
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
just verify-release "${GITHUB_REF_NAME}"
else
just verify-release
fi
- name: verify tag provenance (tag == HEAD, HEAD contained in main)
if: github.event_name == 'push'
shell: bash
run: |
set -euo pipefail
head="$(git rev-parse HEAD)"
tagged="$(git rev-parse "refs/tags/${GITHUB_REF_NAME}^{commit}")"
if [ "${tagged}" != "${head}" ]; then
echo "::error::tag ${GITHUB_REF_NAME} points at ${tagged}, not the checked-out HEAD ${head}"
exit 1
fi
if ! git merge-base --is-ancestor "${head}" origin/main; then
echo "::error::tagged commit ${head} is NOT contained in the protected branch origin/main (unmerged or detached). Refusing to release."
exit 1
fi
echo "provenance OK: ${GITHUB_REF_NAME} -> ${head} is contained in origin/main"
rust-gate:
name: rust gate (just check + workspace/locked + publish dry-run)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 with:
toolchain: stable
components: rustfmt, clippy
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 with:
python-version: "3.12"
- uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc with:
tool: just
- name: canonical gate (just check) + version drift guard
run: |
set -euo pipefail
just check
just verify-release
- name: release-only supersets (workspace + locked + all-features)
run: |
set -euo pipefail
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --locked
cargo test --workspace --all-features --locked
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
- name: cargo publish --dry-run (core crate, from the committed lockfile)
run: cargo publish -p content-addressable --locked --dry-run
msrv:
name: MSRV (Rust 1.85)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 with:
toolchain: "1.85"
- name: build + test on 1.85 (locked)
run: |
set -euo pipefail
cargo build --all-targets --all-features --locked
cargo test --all-features --locked
build-wheels:
name: wheels (${{ matrix.platform.target }} on ${{ matrix.platform.runner }})
needs: [provenance, rust-gate]
runs-on: ${{ matrix.platform.runner }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
platform:
- runner: ubuntu-latest
target: x86_64
args: --release --out dist --manifest-path content-addressable-py/Cargo.toml
manylinux: auto
- runner: ubuntu-latest
target: aarch64
args: --release --out dist --manifest-path content-addressable-py/Cargo.toml
manylinux: auto
- runner: macos-14
target: universal2-apple-darwin
args: --release --out dist --manifest-path content-addressable-py/Cargo.toml --target universal2-apple-darwin
manylinux: ""
- runner: windows-latest
target: x64
args: --release --out dist --manifest-path content-addressable-py/Cargo.toml
manylinux: ""
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 with:
python-version: "3.12"
- name: build abi3 wheel
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b with:
target: ${{ matrix.platform.target }}
args: ${{ matrix.platform.args }}
manylinux: ${{ matrix.platform.manylinux }}
sccache: "true"
- name: twine check the wheels
shell: bash
run: |
set -euo pipefail
python -m pip install --upgrade pip twine
twine check dist/*
- name: upload wheel artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: wheels-${{ matrix.platform.runner }}-${{ matrix.platform.target }}
path: dist/*.whl
if-no-files-found: error
build-sdist:
name: sdist
needs: [provenance, rust-gate]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: build sdist
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b with:
command: sdist
args: --out dist --manifest-path content-addressable-py/Cargo.toml
- name: twine check the sdist
shell: bash
run: |
set -euo pipefail
python -m pip install --upgrade pip twine
twine check dist/*
- name: upload sdist artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: sdist
path: dist/*.tar.gz
if-no-files-found: error
install-smoke:
name: install-smoke (${{ matrix.artifact }} / py${{ matrix.python }})
needs: [build-wheels]
runs-on: ${{ matrix.runner }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- { runner: ubuntu-latest, artifact: wheels-ubuntu-latest-x86_64, python: "3.9" }
- { runner: ubuntu-latest, artifact: wheels-ubuntu-latest-x86_64, python: "3.12" }
- { runner: ubuntu-latest, artifact: wheels-ubuntu-latest-x86_64, python: "3.13" }
- { runner: ubuntu-24.04-arm, artifact: wheels-ubuntu-latest-aarch64, python: "3.12" }
- { runner: macos-14, artifact: wheels-macos-14-universal2-apple-darwin, python: "3.9" }
- { runner: macos-14, artifact: wheels-macos-14-universal2-apple-darwin, python: "3.12" }
- { runner: macos-14, artifact: wheels-macos-14-universal2-apple-darwin, python: "3.13" }
- { runner: windows-latest, artifact: wheels-windows-latest-x64, python: "3.9" }
- { runner: windows-latest, artifact: wheels-windows-latest-x64, python: "3.12" }
- { runner: windows-latest, artifact: wheels-windows-latest-x64, python: "3.13" }
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 with:
python-version: ${{ matrix.python }}
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: ${{ matrix.artifact }}
path: dist
- name: install the built wheel into a clean venv and smoke-test it
shell: bash
run: |
set -euo pipefail
python -m venv .venv
if [ -f .venv/bin/python ]; then VPY=.venv/bin/python; else VPY=.venv/Scripts/python; fi
"$VPY" -m pip install --upgrade pip
shopt -s nullglob
wheels=(dist/*.whl)
wheel="${wheels[0]:-}"
[ -n "${wheel}" ] || { echo "::error::no wheel found in dist/"; exit 1; }
echo "installing built wheel: ${wheel}"
"$VPY" -m pip install "${wheel}" pytest
# The import MUST resolve to the installed wheel, not the source tree.
"$VPY" -c "import content_addressable as m; assert 'site-packages' in m.__file__, m.__file__; print('installed from', m.__file__)"
"$VPY" -c "import content_addressable as m; cid = m.content_id({'name': 'alpha'}); assert str(cid).startswith('bafyr4i'); assert len(cid.digest_bytes()) == 32; assert m.ContentId.parse(str(cid)) == cid; print('smoke OK', cid)"
"$VPY" -m pytest tests/test_content_addressable.py tests/test_readme.py tests/test_profile_validation.py -q
sdist-smoke:
name: sdist install-smoke (build from source)
needs: [build-sdist]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 with:
toolchain: stable
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 with:
python-version: "3.12"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: sdist
path: dist
- name: install the sdist (from source) and test it
shell: bash
run: |
set -euo pipefail
python -m venv .venv
.venv/bin/pip install --upgrade pip
shopt -s nullglob
sdists=(dist/*.tar.gz)
sdist="${sdists[0]:-}"
[ -n "${sdist}" ] || { echo "::error::no sdist found in dist/"; exit 1; }
echo "installing sdist (from source): ${sdist}"
.venv/bin/pip install "${sdist}" pytest
.venv/bin/python -c "import content_addressable as m; assert 'site-packages' in m.__file__, m.__file__; print('sdist installed from', m.__file__)"
.venv/bin/python -m pytest tests/test_content_addressable.py tests/test_profile_validation.py -q
release-gate:
name: release gate (all validation passed)
needs:
- provenance
- rust-gate
- msrv
- build-wheels
- build-sdist
- install-smoke
- sdist-smoke
runs-on: ubuntu-latest
permissions: {}
steps:
- name: gate summary
run: echo "release-gate PASSED — provenance, rust-gate, msrv, wheels, sdist, and install-smoke (wheel + sdist) all green. Publication may proceed."
publish-pypi:
name: publish to PyPI (Trusted Publishing)
if: github.event_name == 'push'
needs: [release-gate]
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write steps:
- name: download the exact validated wheels + sdist
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
path: dist
merge-multiple: true - name: list the artifacts to publish
run: ls -la dist/
- name: publish to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 with:
packages-dir: dist/
publish-crate:
name: publish core crate to crates.io
if: github.event_name == 'push'
needs: [release-gate]
runs-on: ubuntu-latest
environment: crates-io
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 with:
toolchain: stable
- name: cargo publish (core crate only)
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish -p content-addressable --locked
dry-run-summary:
name: dry-run summary (no publish)
if: github.event_name == 'workflow_dispatch'
needs: [release-gate]
runs-on: ubuntu-latest
permissions: {}
steps:
- run: echo "Dry run complete — provenance, gate, msrv, wheels + sdist built, install-smoke (wheel + sdist) all passed. Nothing published."