---
name: Build, Sign & Publish
"on":
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g., 3.0.1, 4.0.0-alpha.1, no leading 'v')"
required: true
type: string
permissions:
contents: read
jobs:
lint:
name: MegaLinter
uses: ./.github/workflows/megalinter.yml
permissions:
statuses: write # for GITHUB_STATUS_REPORTER in MegaLinter to post/update commit statuses
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
pull-requests: write # Allow posting summaries as PR comments
id-token: write # for step-security/harden-runner to fetch OIDC token
secrets:
REUSABLE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tests:
name: Tests & Coverage
uses: ./.github/workflows/tests.yml
permissions:
id-token: write # for step-security/harden-runner to fetch OIDC token
pull-requests: write # Allow posting summaries as PR comments
secrets:
BITSIGHT_API_KEY: ${{ secrets.BITSIGHT_API_KEY }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
build:
name: Build Distribution
needs: [lint, tests]
runs-on: ubuntu-latest
permissions:
id-token: write # for step-security/harden-runner to fetch OIDC token
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- 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: 3
github-release:
name: Create GitHub Release
needs: build
runs-on: ubuntu-latest
permissions:
id-token: write # for step-security/harden-runner to fetch OIDC token
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
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@a5caf349bc536fbef3668a10ed7f5cd309a4b53d # v3.2.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: step-security/action-gh-release@674f8f866246c6b91bd3f1688ba95e27b2ae8d2e # 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
permissions:
id-token: write # for step-security/harden-runner to fetch OIDC token
environment:
name: release
url: https://pypi.org/project/birre/
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
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 # v1.13.0
with:
print-hash: true
verbose: true