forked from ChelseaKR/fare-policy-assistant
-
Notifications
You must be signed in to change notification settings - Fork 0
154 lines (146 loc) · 6.17 KB
/
Copy pathrelease.yml
File metadata and controls
154 lines (146 loc) · 6.17 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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
name: release
# Trusted-main release (RELEASE-AND-VERSIONING-STANDARD §4). Nothing here
# publishes to a package index (fare-assistant is a deployed demo/reference
# app, not a library on PyPI — see the README "Standards conformance"
# table), so the pipeline's publish target is the GitHub Release itself:
# build sdist+wheel, generate a CycloneDX SBOM, attest SLSA build
# provenance, and attach all three to a GitHub Release whose notes are the
# matching CHANGELOG section. Every stage re-runs the merge-blocking gates
# at the tagged commit rather than trusting the PR's prior green run.
#
# Tag like `v0.1.0`; `git tag -s v0.1.0 -m v0.1.0 && git push origin v0.1.0`.
on:
workflow_dispatch:
inputs:
tag:
description: "Existing signed stable SemVer tag (vX.Y.Z)"
required: true
type: string
permissions:
contents: read
# A release publishes artifacts to the GitHub Release — never race two tags,
# but never silently drop a re-run either.
concurrency:
group: release
cancel-in-progress: false
jobs:
authorize:
uses: ChelseaKR/portfolio-standards/.github/workflows/release-authorize.yml@3692aa5270d673f91f8571b4fc0ce1c622bf67e5
permissions:
contents: read
with:
tag: ${{ inputs.tag }}
# Never publish untested code: re-run the full offline merge-blocking gate
# set (lint, format, typecheck, coverage-gated tests, a11y, i18n,
# committed-report regression) at the tagged commit — the exact `make
# verify` set CI runs on every push, not the PR's prior checkmark.
release-tests:
needs: authorize
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.authorize.outputs.release-commit }}
persist-credentials: false
# No cache here either: this job gates the artifact build below, so a
# poisoned cache hit would taint the release path just as directly
# (zizmor cache-poisoning audit; same reasoning as the build job).
- uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
with:
enable-cache: false
- name: Install gettext (msgfmt, required by `make i18n`)
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Verify uv.lock is current
run: uv sync --frozen --all-groups
- name: make verify (lint + typecheck + coverage-gated tests + a11y + i18n + report-regression)
run: make verify
build:
needs: [authorize, release-tests]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # SLSA build-provenance attestation
attestations: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.authorize.outputs.release-commit }}
persist-credentials: false
# No cache in the build/publish path: a cache hit is an unverified
# input into the artifact we're about to attest and ship
# (CI-CD-STANDARD; Feb 2026 cache-poisoning campaign).
- uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
with:
enable-cache: false
- name: Build sdist + wheel
run: uv build
- name: Require tag, package, and changelog versions to match
env:
TAG: ${{ needs.authorize.outputs.release-tag }}
run: |
set -euo pipefail
VERSION="${TAG#v}"
PACKAGE_VERSION="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
test "${VERSION}" = "${PACKAGE_VERSION}"
awk -v ver="${VERSION}" '
index($0, "## [" ver "]") == 1 { found=1; print; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > release-notes.md
test -s release-notes.md
- name: Generate a runtime CycloneDX SBOM
run: |
uv sync --frozen --no-dev
uvx --from cyclonedx-bom cyclonedx-py environment .venv \
--sv 1.7 --of JSON --pyproject pyproject.toml \
--output-reproducible -o sbom.cdx.json
- name: Attest build provenance (SLSA, keyless/OIDC via Sigstore)
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: "dist/*.whl,dist/*.tar.gz"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dist
path: dist/
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: sbom-cyclonedx
path: sbom.cdx.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-notes
path: release-notes.md
if-no-files-found: error
# Create the GitHub Release: attach the build + SBOM, and use the matching
# CHANGELOG section as release notes (RELEASE-STANDARD §3.2, §6).
github-release:
needs: [authorize, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download build artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dist
path: dist/
- name: Download CycloneDX SBOM
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: sbom-cyclonedx
path: sbom/
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-notes
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.authorize.outputs.release-tag }}
TAG_OBJECT_SHA: ${{ needs.authorize.outputs.tag-object-sha }}
run: |
set -euo pipefail
LIVE_TAG_OBJECT="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG}" --jq .object.sha)"
test "${LIVE_TAG_OBJECT}" = "${TAG_OBJECT_SHA}"
gh release create "${TAG}" dist/* sbom/sbom.cdx.json \
--title "${TAG}" --notes-file release-notes.md