Skip to content

Merge pull request #108 from constk/release/0.2.17 #1

Merge pull request #108 from constk/release/0.2.17

Merge pull request #108 from constk/release/0.2.17 #1

Workflow file for this run

name: Release
# Tag-triggered release pipeline. Runs on every annotated tag matching
# `v*.*.*`. Builds the production Docker image, pushes it to GHCR, generates
# a CycloneDX SBOM from the locked dependency set, and publishes the GitHub
# Release using release-drafter's pre-drafted body so notes match the merged
# PR titles.
#
# Auth: uses GITHUB_TOKEN — no PAT required.
# Storage: SBOM JSON is attached to the release, NOT uploaded as an Actions
# artifact (account-wide artifact storage quota lives a precarious life).
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write # required to create the GitHub Release
packages: write # push the image to ghcr.io/<owner>/<repo>
jobs:
release:
name: Build, SBOM, Release
runs-on: ubuntu-latest
steps:
# Actions are SHA-pinned because this workflow has elevated permissions
# (contents: write + packages: write). Bump SHAs with the # vX.Y.Z
# annotation when a new release lands and you've reviewed the diff.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.14"
# Production-only sync — matches the Dockerfile's `uv sync --frozen
# --no-dev` so the SBOM walks exactly the wheel set the image loads.
# Including dev deps here would publish a CycloneDX document that
# claims pytest/mypy/ruff are in the image and undermine the
# SBOM-as-attestation property.
- name: Install project (production deps only)
run: uv sync --frozen --no-dev
- name: Resolve image tags
id: tags
run: |
VERSION="${GITHUB_REF_NAME#v}"
IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}"
{
echo "version=${VERSION}"
echo "image=${IMAGE}"
echo "tag_version=${IMAGE}:${VERSION}"
echo "tag_latest=${IMAGE}:latest"
} >> "$GITHUB_OUTPUT"
# Build the same Dockerfile used in `Container image scan (trivy)` —
# tags both `<image>:<version>` and `<image>:latest`.
- name: Build Docker image
run: |
docker build \
-t "${{ steps.tags.outputs.tag_version }}" \
-t "${{ steps.tags.outputs.tag_latest }}" \
.
- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push image
run: |
docker push "${{ steps.tags.outputs.tag_version }}"
docker push "${{ steps.tags.outputs.tag_latest }}"
# CycloneDX SBOM from the locked production wheel set. cyclonedx-bom
# runs in an isolated `uvx --from` venv so it does not bleed into
# `.venv` (that would put the SBOM-generation tooling in the SBOM).
# cyclonedx-py then targets the project venv, walking only the prod
# deps the image loads.
#
# Pinned to an exact version (not >=) so the SBOM bytes are
# reproducible across release runs.
- name: Generate SBOM (CycloneDX)
run: |
uvx --from "cyclonedx-bom==7.3.0" \
cyclonedx-py environment .venv > sbom.json
# Sanity-check: reject zero-byte / malformed JSON before attaching.
python -c "import json; data = json.load(open('sbom.json')); assert data.get('bomFormat') == 'CycloneDX', 'SBOM missing bomFormat field'"
echo "SBOM components: $(python -c "import json; print(len(json.load(open('sbom.json')).get('components', [])))")"
# release-drafter has been keeping a draft under v$VERSION updated on
# every merge to main, so `gh release edit` promotes the existing
# draft and attaches the SBOM. If no draft exists yet (first release
# after the workflow lands), `gh release create` falls back to
# auto-generated notes.
- name: Publish release with SBOM
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${GITHUB_REF_NAME}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Promoting existing draft → published"
gh release edit "$TAG" --draft=false
gh release upload "$TAG" sbom.json --clobber
else
echo "No draft found — creating release with auto-generated notes"
gh release create "$TAG" \
--title "$TAG" \
--generate-notes \
sbom.json
fi