Skip to content

chore: bump version to v1.0.0 with SEC-02 hardening #52

chore: bump version to v1.0.0 with SEC-02 hardening

chore: bump version to v1.0.0 with SEC-02 hardening #52

Workflow file for this run

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