Skip to content

Release

Release #34

Workflow file for this run

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