---
name: Build, Sign & Publish
"on":
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g., 3.0.1)"
required: true
type: string
permissions:
contents: write
id-token: write # Required for PyPI trusted publishing
pull-requests: write # Reusable lint workflow posts summaries
statuses: write # Allow MegaLinter to update commit statuses
security-events: write # Allow SARIF uploads from reusable workflow
jobs:
quality-lint:
name: Lint, Type, Test & SBOM Gate (Lint)
uses: ./.github/workflows/lint-and-test.yml
with:
enable-dependency-review: false
enable-tests: false
secrets:
REUSABLE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
validate:
name: Pre-release Validation
runs-on: ubuntu-latest
needs: quality-lint
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Prepare Python environment
uses: ./.github/actions/uv-env
with:
cache-key-prefix: release-validate
- name: Audit dependencies
run: |
uv tool run pip-audit --strict
- name: Lint with ruff
run: |
uv run ruff check src tests
- name: Type check with mypy
run: |
uv run mypy src
- name: Run tests with coverage (full if API key present, else offline)
env:
BITSIGHT_API_KEY: ${{ secrets.BITSIGHT_API_KEY }}
run: |
if [ -n "${BITSIGHT_API_KEY}" ]; then
uv run pytest \
--cov=src/birre \
--cov-report=term \
--cov-fail-under=80
else
uv run pytest --offline \
--cov=src/birre \
--cov-report=term \
--cov-fail-under=80
fi
build:
name: Build Distribution
needs: validate
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Prepare Python environment
uses: ./.github/actions/uv-env
with:
cache-key-prefix: release-build
- name: Install SBOM generator
run: |
uv tool install cyclonedx-bom
- name: Build package
run: |
uv build
- name: Generate SBOM
run: |
uv tool run cyclonedx-py environment \
--pyproject pyproject.toml \
--output-reproducible \
--of JSON \
-o dist/birre-sbom.json \
"$(uv python find)"
- name: Verify package contents
run: |
tar -tzf dist/*.tar.gz | head -20
unzip -l dist/*.whl | head -30
- name: Upload build artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: dist
path: dist/
retention-days: 7
github-release:
name: Create GitHub Release
needs: build
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0 # Fetch all history for changelog
- name: Download build artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: dist
path: dist/
- name: Sign release artifacts with Sigstore
uses: sigstore/gh-action-sigstore-python@f832326173235dcb00dd5d92cd3f353de3188e6c # v3.1.0
with:
inputs: ./dist/*.tar.gz ./dist/*.whl
upload-signing-artifacts: true
release-signing-artifacts: true
- name: Extract version from tag
id: version
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_VERSION: ${{ github.event.inputs.version }}
GITHUB_REF: ${{ github.ref }}
run: |
SEMVER_REGEX='^[0-9]+(\.[0-9]+){2}(-[0-9A-Za-z][0-9A-Za-z\.-]*)?(\+[0-9A-Za-z\.-]+)?$'
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
RAW_VERSION="$INPUT_VERSION"
if [[ "$RAW_VERSION" =~ $SEMVER_REGEX ]]; then
VERSION="$RAW_VERSION"
else
echo "Invalid version input: must match SemVer (e.g., 3.0.1 or 4.0.0-alpha.2)." >&2
exit 1
fi
else
VERSION="${GITHUB_REF#refs/tags/v}"
if ! [[ "$VERSION" =~ $SEMVER_REGEX ]]; then
echo "Invalid tag version '$VERSION'; expected SemVer." >&2
exit 1
fi
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
if [[ "$VERSION" == *-* ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi
- name: Generate changelog
id: changelog
run: |
python - <<'PY'
import sys
version = "${{ steps.version.outputs.version }}"
capture = False
lines = []
with open("CHANGELOG.md", encoding="utf-8") as fh:
for line in fh:
if line.startswith("## "):
if capture:
break
capture = line.startswith(f"## [{version}]")
if capture:
lines.append(line.rstrip("\n"))
if not lines:
sys.stderr.write(f"Missing changelog entry for version {version}.\n")
sys.stderr.write(f"Add '## [{version}]' before tagging.\n")
sys.exit(1)
with open("changelog.txt", "w", encoding="utf-8") as out:
out.write("\n".join(lines) + "\n")
PY
- name: Create GitHub Release
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with:
tag_name: ${{ steps.version.outputs.tag }}
name: Release ${{ steps.version.outputs.version }}
body_path: changelog.txt
files: |
dist/*.tar.gz
dist/*.whl
dist/*.sigstore.json
dist/birre-sbom.json
draft: false
prerelease: ${{ steps.version.outputs.prerelease }}
pypi-publish:
name: Publish to PyPI
needs: github-release
runs-on: ubuntu-latest
environment:
name: release
url: https://pypi.org/project/birre/
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: dist
path: dist/
- name: Remove non-distribution artifacts
run: |
find dist -type f ! -name '*.whl' ! -name '*.tar.gz' -delete
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
with:
print-hash: true
verbose: true