Release #34
Workflow file for this run
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: Release | |
| on: | |
| push: | |
| tags: ['v*.*.*'] # runs only on release tags like v0.1.0 | |
| permissions: | |
| contents: read | |
| jobs: | |
| build-publish: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # attach assets to GH Release | |
| id-token: write # OIDC for PyPI Trusted Publisher + attestations | |
| attestations: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 | |
| - name: Set up Python | |
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 | |
| with: | |
| python-version: "3.12" | |
| # Tools via pipx (no raw pip install into runner) | |
| - name: Install pipx + tools | |
| run: | | |
| set -euxo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y pipx | |
| pipx ensurepath | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| pipx install build==1.2.2 | |
| # pipx install twine==5.1.1 | |
| # pipx install sigstore==3.6.3 # CLI used to produce .sig + .crt | |
| # Tag must match version in pyproject.toml (vX.Y.Z vs X.Y.Z) | |
| - name: Check version matches tag | |
| run: | | |
| TAG="${GITHUB_REF_NAME#v}" | |
| PYVER=$(python -c "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text(encoding='utf-8'))['project']['version'])") | |
| echo "tag=${TAG}" | |
| echo "pyproject=${PYVER}" | |
| test "$PYVER" = "$TAG" || { echo 'ERROR: pyproject.toml version != tag'; exit 1; } | |
| - name: Clean dist | |
| run: rm -rf dist/* || true | |
| - name: Build distributions | |
| run: pyproject-build -o dist | |
| - name: Inspect wheel METADATA (must have Name & Version) | |
| run: | | |
| python - <<'PY' | |
| import glob, zipfile | |
| whl = glob.glob('dist/*.whl')[0] | |
| with zipfile.ZipFile(whl) as z: | |
| meta = [n for n in z.namelist() if n.endswith('.dist-info/METADATA')] | |
| assert meta, 'Wheel missing .dist-info/METADATA' | |
| head = z.read(meta[0]).decode().splitlines()[:15] | |
| print('---- METADATA head ----'); print('\n'.join(head)); print('---- /METADATA ----') | |
| text = '\n'.join(head) | |
| assert 'Name: decision-security' in text or 'Name: decision_security' in text | |
| PY | |
| # Create provenance in GitHub Attestations for each dist/* | |
| - name: Generate SLSA provenance (GitHub Attestations) | |
| id: prov | |
| uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a | |
| with: | |
| subject-path: 'dist/*' | |
| - name: Download per-artifact provenance (*.intoto.jsonl) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euxo pipefail | |
| shopt -s nullglob | |
| # The attestation can take a few seconds to become queriable; retry a bit. | |
| for f in dist/*.whl dist/*.tar.gz; do | |
| out="${f}.intoto.jsonl" | |
| ok=0 | |
| for i in 1 2 3 4 5; do | |
| if gh attestation download --repo "$REPO" "$f" --format=jsonl > "$out"; then | |
| ok=1; break | |
| fi | |
| echo "Attestation not ready for $f (attempt $i/5). Sleeping 5s…" | |
| sleep 5 | |
| done | |
| if [ "$ok" -ne 1 ]; then | |
| echo "WARNING: no attestation found for $f; continuing without ${out}" | |
| rm -f "$out" || true | |
| fi | |
| done | |
| ls -l dist || true | |
| - name: Download per-artifact provenance (*.intoto.jsonl) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euxo pipefail | |
| shopt -s nullglob | |
| for f in dist/*.whl dist/*.tar.gz; do | |
| out="${f}.intoto.jsonl" | |
| ok=0 | |
| # Give GitHub a few seconds to make the attestation queriable | |
| for i in 1 2 3 4 5; do | |
| # Note: no --format flag; write stdout to file | |
| if gh attestation download --repo "$REPO" "$f" -d sha256 > "$out"; then | |
| ok=1; break | |
| fi | |
| echo "Attestation not ready for $f (attempt $i/5). Sleeping 5s…" | |
| sleep 5 | |
| done | |
| if [ "$ok" -ne 1 ]; then | |
| echo "WARNING: no attestation found for $f; continuing without ${out}" | |
| rm -f "$out" || true | |
| fi | |
| done | |
| ls -l dist || true | |
| - name: Sign wheels and sdists (Sigstore) | |
| uses: sigstore/gh-action-sigstore-python@f7ad0af51a5648d09a20d00370f0a91c3bdf8f84 | |
| with: | |
| inputs: dist/* | |
| release-signing-artifacts: false | |
| - name: Verify signature artifacts exist | |
| run: | | |
| ls -al dist | |
| if ls dist/*.sigstore.json >/dev/null 2>&1; then | |
| echo "Found Sigstore bundle(s) (*.sigstore.json)." | |
| elif ls dist/*.sig >/dev/null 2>&1 && ls dist/*.crt >/dev/null 2>&1; then | |
| echo "Found legacy .sig/.crt pair(s)." | |
| else | |
| echo "No signature artifacts found in dist/"; exit 1 | |
| fi | |
| # Create / update GitHub Release and attach all assets (dists + sigs + provenance) | |
| - name: Create GitHub Release (attach assets) | |
| uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 | |
| with: | |
| files: | | |
| dist/*.whl | |
| dist/*.tar.gz | |
| dist/*.sig | |
| dist/*.crt | |
| dist/*.intoto.jsonl | |
| dist/*.sigstore.json | |
| # Skip publish if this version already exists on PyPI | |
| - name: Skip if version exists on PyPI | |
| id: pypi-exists | |
| run: | | |
| PKG="decision-security" | |
| VER="${GITHUB_REF_NAME#v}" | |
| python - <<'PY' "$PKG" "$VER" || exit 0 | |
| import json, sys, urllib.request | |
| pkg, ver = sys.argv[1], sys.argv[2] | |
| with urllib.request.urlopen(f"https://pypi.org/pypi/{pkg}/json", timeout=10) as r: | |
| data = json.load(r) | |
| if ver in data.get("releases", {}): | |
| print(f"Version {ver} already on PyPI; skipping publish.") | |
| sys.exit(78) # neutral exit | |
| PY | |
| continue-on-error: true | |
| - name: Publish to PyPI (Trusted Publisher) | |
| if: startsWith(github.ref, 'refs/tags/') && steps.pypi-exists.outcome != 'success' | |
| uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 | |
| with: | |
| print-hash: true |