Merge pull request #108 from constk/release/0.2.17 #1
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 | |
| # 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 |