chore: bump version to v1.0.0 with SEC-02 hardening #52
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
| name: Build | |
| # Restored from the workflow that actually published v0.6.5 -> v0.6.9 | |
| # (preserved at the v0.6.9 checkout). The v0.7.0 prep commit 3d332eb replaced | |
| # that proven file with an untested rewrite; every release failure found since | |
| # was latent in the rewrite: | |
| # | |
| # * it built AND published an sdist, which docs/RELEASING.md forbids because | |
| # src/* is gitignored, so the tarball has no .rs sources and any platform | |
| # without a matching wheel fails on a source build; | |
| # * its smoke test called `get_decoder_info`, which does not exist; | |
| # * it added musllinux and Linux-aarch64 targets that README says are not | |
| # published, and neither built or smoke-tested cleanly; | |
| # * its publish gate read `|| inputs.publish`; a workflow_dispatch input is a | |
| # string and every non-empty string is truthy, so `publish=false` evaluated | |
| # TRUE and publish was live on runs explicitly asked not to publish; | |
| # * it dropped abi3 to 6 wheels while README documents 15. | |
| # | |
| # What is deliberately KEPT from the rewrite: | |
| # * the 12-chunk secret restore. The proven file inlined 3 chunks and gzip; | |
| # the core has since outgrown that (12 chunks, xz), which is the legitimate | |
| # reason it was rewritten. This is the one change that could not be avoided. | |
| # * the strict dependency gate, which caught RUSTSEC-2026-0204. | |
| # * a smoke test that decodes and asserts H@c == s rather than only importing. | |
| # | |
| # What is deliberately DROPPED: the sdist job, musllinux, Linux aarch64, and | |
| # macos-intel (it hangs). The result is 3 platforms x 5 CPython versions = the | |
| # 15 wheels README documents and PyPI already carries for 0.6.9. | |
| on: | |
| push: | |
| branches: [main, master] | |
| tags: ["v*", "ci-*", "test-*"] | |
| pull_request: {} | |
| workflow_dispatch: {} | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| MATURIN_ARGS: --release --no-default-features --features cuda --out dist | |
| jobs: | |
| linux-x86_64: | |
| name: linux-x86_64-py${{ matrix.python-version }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Inject Rust source | |
| env: | |
| RUST_SRC_B64_1: ${{ secrets.RUST_SRC_B64_1 }} | |
| RUST_SRC_B64_2: ${{ secrets.RUST_SRC_B64_2 }} | |
| RUST_SRC_B64_3: ${{ secrets.RUST_SRC_B64_3 }} | |
| RUST_SRC_B64_4: ${{ secrets.RUST_SRC_B64_4 }} | |
| RUST_SRC_B64_5: ${{ secrets.RUST_SRC_B64_5 }} | |
| RUST_SRC_B64_6: ${{ secrets.RUST_SRC_B64_6 }} | |
| RUST_SRC_B64_7: ${{ secrets.RUST_SRC_B64_7 }} | |
| RUST_SRC_B64_8: ${{ secrets.RUST_SRC_B64_8 }} | |
| RUST_SRC_B64_9: ${{ secrets.RUST_SRC_B64_9 }} | |
| RUST_SRC_B64_10: ${{ secrets.RUST_SRC_B64_10 }} | |
| RUST_SRC_B64_11: ${{ secrets.RUST_SRC_B64_11 }} | |
| RUST_SRC_B64_12: ${{ secrets.RUST_SRC_B64_12 }} | |
| run: | | |
| python scripts/pack_rust_core.py unpack --from-env | |
| python scripts/pack_rust_core.py check-manifest | |
| - name: Build manylinux wheel | |
| uses: PyO3/maturin-action@v1 | |
| with: | |
| target: x86_64 | |
| manylinux: auto | |
| args: ${{ env.MATURIN_ARGS }} -i python${{ matrix.python-version }} | |
| sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: wheels-linux-x86_64-py${{ matrix.python-version }} | |
| path: dist/*.whl | |
| windows-x64: | |
| name: windows-x64-py${{ matrix.python-version }} | |
| runs-on: windows-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Inject Rust source | |
| shell: bash | |
| env: | |
| RUST_SRC_B64_1: ${{ secrets.RUST_SRC_B64_1 }} | |
| RUST_SRC_B64_2: ${{ secrets.RUST_SRC_B64_2 }} | |
| RUST_SRC_B64_3: ${{ secrets.RUST_SRC_B64_3 }} | |
| RUST_SRC_B64_4: ${{ secrets.RUST_SRC_B64_4 }} | |
| RUST_SRC_B64_5: ${{ secrets.RUST_SRC_B64_5 }} | |
| RUST_SRC_B64_6: ${{ secrets.RUST_SRC_B64_6 }} | |
| RUST_SRC_B64_7: ${{ secrets.RUST_SRC_B64_7 }} | |
| RUST_SRC_B64_8: ${{ secrets.RUST_SRC_B64_8 }} | |
| RUST_SRC_B64_9: ${{ secrets.RUST_SRC_B64_9 }} | |
| RUST_SRC_B64_10: ${{ secrets.RUST_SRC_B64_10 }} | |
| RUST_SRC_B64_11: ${{ secrets.RUST_SRC_B64_11 }} | |
| RUST_SRC_B64_12: ${{ secrets.RUST_SRC_B64_12 }} | |
| run: | | |
| python scripts/pack_rust_core.py unpack --from-env | |
| python scripts/pack_rust_core.py check-manifest | |
| - name: Build Windows wheel | |
| uses: PyO3/maturin-action@v1 | |
| with: | |
| target: x64 | |
| args: ${{ env.MATURIN_ARGS }} -i python | |
| sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: wheels-windows-x64-py${{ matrix.python-version }} | |
| path: dist/*.whl | |
| macos-arm: | |
| name: macos-arm64-py${{ matrix.python-version }} | |
| runs-on: macos-14 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Inject Rust source | |
| env: | |
| RUST_SRC_B64_1: ${{ secrets.RUST_SRC_B64_1 }} | |
| RUST_SRC_B64_2: ${{ secrets.RUST_SRC_B64_2 }} | |
| RUST_SRC_B64_3: ${{ secrets.RUST_SRC_B64_3 }} | |
| RUST_SRC_B64_4: ${{ secrets.RUST_SRC_B64_4 }} | |
| RUST_SRC_B64_5: ${{ secrets.RUST_SRC_B64_5 }} | |
| RUST_SRC_B64_6: ${{ secrets.RUST_SRC_B64_6 }} | |
| RUST_SRC_B64_7: ${{ secrets.RUST_SRC_B64_7 }} | |
| RUST_SRC_B64_8: ${{ secrets.RUST_SRC_B64_8 }} | |
| RUST_SRC_B64_9: ${{ secrets.RUST_SRC_B64_9 }} | |
| RUST_SRC_B64_10: ${{ secrets.RUST_SRC_B64_10 }} | |
| RUST_SRC_B64_11: ${{ secrets.RUST_SRC_B64_11 }} | |
| RUST_SRC_B64_12: ${{ secrets.RUST_SRC_B64_12 }} | |
| run: | | |
| python scripts/pack_rust_core.py unpack --from-env | |
| python scripts/pack_rust_core.py check-manifest | |
| - name: Build macOS ARM wheel | |
| uses: PyO3/maturin-action@v1 | |
| with: | |
| target: aarch64 | |
| args: ${{ env.MATURIN_ARGS }} -i python | |
| sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: wheels-macos-aarch64-py${{ matrix.python-version }} | |
| path: dist/*.whl | |
| # Blocking half of the dependency policy. This is what caught | |
| # RUSTSEC-2026-0204 before it could ship. | |
| dependency-gate: | |
| name: Dependency + licence gate | |
| # Tag pushes only. dependency-audit.yml already runs non-strict on every | |
| # push as its own check; running it again here would duplicate that on every | |
| # commit for no benefit. It matters at release time, where strict turns an | |
| # advisory into a hard block - which is what stopped RUSTSEC-2026-0204. | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| uses: ./.github/workflows/dependency-audit.yml | |
| with: | |
| strict: true | |
| permissions: | |
| contents: read | |
| secrets: | |
| SAFETY_API_KEY: ${{ secrets.SAFETY_API_KEY }} | |
| publish: | |
| name: publish-to-pypi | |
| runs-on: ubuntu-latest | |
| # Tag-only, exactly as in the workflow that shipped 0.6.5 -> 0.6.9. There is | |
| # deliberately no `inputs.publish` term: that was the rewrite's accidental | |
| # -publish hole, since a dispatch input arrives as a string and "false" is | |
| # truthy. A workflow_dispatch run here builds and verifies but cannot | |
| # publish, because the ref is a branch. | |
| if: > | |
| startsWith(github.ref, 'refs/tags/v') && | |
| github.repository == 'GuillaumeLessard/qector-decoder' && | |
| github.event_name != 'pull_request' | |
| needs: [linux-x86_64, windows-x64, macos-arm, dependency-gate] | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/qector-decoder-v3 | |
| permissions: | |
| id-token: write # PyPI Trusted Publishing (OIDC) — no stored token | |
| contents: read | |
| attestations: write # build provenance, non-gating (see 49ba957) | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: dist | |
| merge-multiple: true | |
| - name: Wheels only — no sdist may reach PyPI | |
| run: | | |
| ls -la dist/ | |
| if ls dist/*.tar.gz >/dev/null 2>&1; then | |
| echo "::error::an sdist reached dist/ — refusing to publish (docs/RELEASING.md)" | |
| exit 1 | |
| fi | |
| n=$(ls dist/*.whl 2>/dev/null | wc -l) | |
| echo "wheels collected: $n" | |
| if [ "$n" -lt 15 ]; then | |
| echo "::error::expected 15 wheels (3 platforms x 5 CPython versions), found $n" | |
| exit 1 | |
| fi | |
| - name: Smoke-test a wheel before publishing | |
| run: | | |
| set -euo pipefail | |
| WHEEL=$(ls dist/qector_decoder_v3-*cp312*linux*.whl | head -1) | |
| python -m venv /tmp/smoke && /tmp/smoke/bin/pip install --upgrade pip | |
| /tmp/smoke/bin/pip install "$WHEEL" | |
| /tmp/smoke/bin/python - <<'PY' | |
| import numpy as np | |
| import qector_decoder_v3 as qd | |
| print("version:", qd.__version__) | |
| c2q = [[0, 1], [1, 2], [2, 3], [3, 4]] | |
| n = 5 | |
| syn = np.array([0, 1, 0, 0], dtype=np.uint8) | |
| H = np.zeros((len(c2q), n), dtype=np.uint8) | |
| for i, ch in enumerate(c2q): | |
| for q in ch: | |
| H[i, q] ^= 1 | |
| for name in ("UnionFindDecoder", "BlossomDecoder"): | |
| corr = np.asarray(getattr(qd, name)(c2q, n).decode(syn), dtype=np.uint8).reshape(-1) | |
| assert corr.shape == (n,), f"{name}: {corr.shape}" | |
| assert (((H @ corr) % 2) == syn).all(), f"{name} is not syndrome-faithful" | |
| print(f"{name}: H@c == s OK") | |
| PY | |
| - name: Check metadata | |
| run: | | |
| pip install twine | |
| twine check dist/*.whl | |
| # Non-gating by design: commit 49ba957 ("artifact attestation must not gate | |
| # publishing"). Provenance requires a public repo or GHAS, and on a private | |
| # repo the API returns "Feature not available", which must not fail a | |
| # release that is otherwise good. | |
| - name: Generate artifact attestations | |
| if: ${{ github.event.repository.private == false }} | |
| continue-on-error: true | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: "dist/*" | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: dist/ | |
| # Idempotent upload. PyPI rejects re-uploading a file that already | |
| # exists, so without this a run that uploaded 9 of 15 wheels and then | |
| # failed could never be retried - the retry would die on the first | |
| # duplicate and the release would be stuck half-published. With it, | |
| # a re-run completes the remaining wheels. | |
| skip-existing: true |