-
Notifications
You must be signed in to change notification settings - Fork 0
115 lines (102 loc) · 4.63 KB
/
release.yml
File metadata and controls
115 lines (102 loc) · 4.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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