content-addressable 0.1.1

IPLD-native content addressing: data that carries its own proof of integrity, intrinsically (BLAKE3 + CIDv1 + canonical dag-cbor).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# ============================================================================
# release.yml — tag-driven, multi-platform release for `content-addressable`.
#
# DETERMINISTIC RELEASE GATE. Publication to crates.io OR PyPI cannot begin
# until ONE common gate (`release-gate`) has proven every releasable condition:
#
#   provenance   version agreement (canonical tool) + tag == v<version> +
#                the tagged commit is contained in the protected branch `main`
#   rust-gate    just check + fmt/clippy/test/doc (workspace, --locked,
#                --all-features) + `cargo publish --dry-run`
#   msrv         build + test on the pinned Rust 1.85
#   build-wheels abi3 wheels for every target (twine check + uploaded artifact)
#   build-sdist  the source distribution (twine check + uploaded artifact)
#   install-smoke installs the BUILT wheel into a clean venv on each
#                OS x Python and runs the golden-vector + fail-closed tests
#                against the INSTALLED package (not the checkout)
#   sdist-smoke  installs the sdist (from source) and runs the same tests
#
# Both `publish-*` jobs `needs: [release-gate]` and consume the EXACT artifacts
# those jobs built and validated — nothing is rebuilt inside a publish job.
#
# SECURITY POSTURE
#   - Top-level `permissions: {}`; each job opts into the least it needs.
#   - Every third-party action is pinned to a full commit SHA (tag in a trailing
#     comment). Dependabot (.github/dependabot.yml) refreshes the pins via PRs.
#   - PyPI uses Trusted Publishing (OIDC, no token). crates.io uses a scoped
#     token. Both publish jobs run in protected GitHub environments so a human
#     gate sits in front of each irreversible publish.
#
# HOOK PARITY
#   Tag-triggered and publish-only; never runs on push/PR, so it is intentionally
#   NOT mirrored in .githooks/pre-push (same documented exception as ci.yml's
#   `msrv`/`python` jobs). There is no per-push gate here to mirror.
#
# See RELEASING.md for the full runbook and the one-time maintainer setup
# (PyPI trusted publisher, crates.io token, the `pypi` and `crates-io`
# environments, and branch/tag protection).
# ============================================================================
name: release

on:
  push:
    tags:
      - "v*" # e.g. v0.1.0, v0.1.0-rc.1
  # Manual dry run: runs the ENTIRE gate (the version/provenance checks relax to
  # a no-tag drift check) and builds + install-tests wheels/sdist, but the
  # publish jobs are gated on `github.event_name == 'push'`, so nothing is
  # published.
  workflow_dispatch:

permissions: {} # least privilege at the top; jobs opt into what they need.

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

env:
  CARGO_TERM_COLOR: always

jobs:
  # --------------------------------------------------------------------------
  # provenance — the release-authorization seam. Proves (a) the three version
  # declarations agree and the tag is v<version> (the canonical
  # scripts/verify_release.py, via `just verify-release`), (b) the tag points at
  # HEAD, and (c) HEAD is contained in the protected branch `origin/main`. A tag
  # on an unmerged or detached commit fails here, before anything is built.
  # --------------------------------------------------------------------------
  provenance:
    name: provenance (version + tag + branch containment)
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          fetch-depth: 0 # full history for the ancestor check
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: "3.12"
      - uses: taiki-e/install-action@7f4eb899022d8fe70b20c4f3de697aa85c309026 # v2.85.11
        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 — the canonical `just check` PLUS release-only supersets
  # (workspace scope, --locked, --all-features) and the crate-publish dry run.
  # A superset of the per-push CI gate: nothing publishable is exempt from it.
  # --------------------------------------------------------------------------
  rust-gate:
    name: rust gate (just check + workspace/locked + publish dry-run)
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # rev of master (2026-01)
        with:
          toolchain: stable
          components: rustfmt, clippy
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: "3.12"
      - uses: taiki-e/install-action@7f4eb899022d8fe70b20c4f3de697aa85c309026 # v2.85.11
        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 — build + test on the pinned MSRV (Rust 1.85). The declared floor must
  # actually build and test, not merely be asserted in Cargo.toml.
  # --------------------------------------------------------------------------
  msrv:
    name: MSRV (Rust 1.85)
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # rev of master (2026-01)
        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 — abi3 wheels across the target matrix. abi3-py39 means ONE
  # cp39-abi3 wheel per target covers CPython >= 3.9. Gated behind provenance +
  # rust-gate so a version/provenance/test failure stops the build.
  # --------------------------------------------------------------------------
  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 # v7.0.1
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: "3.12"
      - name: build abi3 wheel
        uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
        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 # v7.0.1
        with:
          name: wheels-${{ matrix.platform.runner }}-${{ matrix.platform.target }}
          path: dist/*.whl
          if-no-files-found: error

  # --------------------------------------------------------------------------
  # build-sdist — the source distribution (built once).
  # --------------------------------------------------------------------------
  build-sdist:
    name: sdist
    needs: [provenance, rust-gate]
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - name: build sdist
        uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
        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 # v7.0.1
        with:
          name: sdist
          path: dist/*.tar.gz
          if-no-files-found: error

  # --------------------------------------------------------------------------
  # install-smoke — the "prove the wheel, not the checkout" gate. For each
  # OS x Python, download the platform's BUILT wheel, install it into a clean
  # venv, prove the import resolves to site-packages (not the source tree), and
  # run the golden-vector + fail-closed-parsing + README tests against the
  # INSTALLED package. abi3 means one wheel serves 3.9..3.13 on that platform.
  # --------------------------------------------------------------------------
  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 # v7.0.1
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: ${{ matrix.python }}
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        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 — install the sdist (which builds from source) and run the same
  # golden + fail-closed tests, proving the source distribution is installable.
  # --------------------------------------------------------------------------
  sdist-smoke:
    name: sdist install-smoke (build from source)
    needs: [build-sdist]
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # rev of master (2026-01)
        with:
          toolchain: stable
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: "3.12"
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        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 — the SINGLE aggregator both publish jobs depend on. It passes
  # only if every validation job above succeeded, so no publish can start until
  # every releasable artifact has passed validation.
  # --------------------------------------------------------------------------
  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 — Trusted Publishing (OIDC, no token). Consumes the EXACT
  # wheel + sdist artifacts built and install-tested above; does not rebuild.
  # --------------------------------------------------------------------------
  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 # OIDC Trusted Publishing. No secrets used.
    steps:
      - name: download the exact validated wheels + sdist
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          path: dist
          merge-multiple: true # every wheels-* + sdist, flattened into dist/
      - name: list the artifacts to publish
        run: ls -la dist/
      - name: publish to PyPI
        uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
        with:
          packages-dir: dist/

  # --------------------------------------------------------------------------
  # publish-crate — the CORE crate to crates.io (the py crate is
  # `publish = false`). `--locked` resolves to the committed lockfile that the
  # rust-gate's --dry-run already validated. In the protected `crates-io`
  # environment so a human gate precedes the irreversible publish.
  # --------------------------------------------------------------------------
  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 # v7.0.1
      - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # rev of master (2026-01)
        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 — workflow_dispatch only. Confirms the whole gate ran
  # without publishing.
  # --------------------------------------------------------------------------
  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."