forked from ChelseaKR/constituent-reconciler
-
Notifications
You must be signed in to change notification settings - Fork 0
195 lines (188 loc) · 8.33 KB
/
Copy pathrelease.yml
File metadata and controls
195 lines (188 loc) · 8.33 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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
name: release
# Trusted-main release (portfolio release policy §4). A
# maintainer supplies an existing signed `vX.Y.Z` tag to the reviewed workflow
# on main, which re-verifies the tagged commit from scratch (this
# repo's PR green check on `ci.yml` is never trusted for a release — main can
# drift after a PR merges), builds the sdist/wheel, generates a CycloneDX SBOM
# (closing the P1-7 gap declared in docs/RESPONSIBLE-TECH-AUDITS.md), attests
# build provenance via keyless OIDC (Sigstore), and publishes a GitHub Release
# with the matching CHANGELOG section as notes.
#
# Not yet published to PyPI (see README) — no `pypa/gh-action-pypi-publish`
# stage. When this repo does publish, add a `publish` job gated on the
# `pypi` environment using Trusted Publishing (OIDC), per
# the portfolio release policy §5.1; do not add a stored PyPI token.
#
# No `v*` tag exists yet — this workflow is prepared ahead of the first
# release so cutting `v0.1.0` doesn't also require writing release CI from
# scratch. Cutting the first tag is a maintainer decision, not something this
# remediation pass performs.
on:
workflow_dispatch:
inputs:
tag:
description: "Existing signed stable SemVer tag (vX.Y.Z)"
required: true
type: string
permissions:
contents: read
# A release publishes a GitHub Release; queue concurrent approvals instead of
# racing two publication runs.
concurrency:
group: release
cancel-in-progress: false
jobs:
# Never publish untested code: re-run the merge-blocking gates at the
# tagged commit, not the PR's stale green check.
release-tests:
runs-on: ubuntu-latest
outputs:
release_commit: ${{ steps.tag.outputs.commit }}
release_tag: ${{ steps.tag.outputs.tag }}
tag_object_sha: ${{ steps.tag.outputs.tag_object_sha }}
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: main
fetch-depth: 0
persist-credentials: false
- name: Resolve and verify the reviewed signed tag
id: tag
env:
INPUT_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
TAG="${INPUT_TAG}"
test "${GITHUB_REF}" = refs/heads/main
test "$(git rev-parse HEAD)" = "${GITHUB_SHA}"
test "$(git rev-parse origin/main)" = "${GITHUB_SHA}"
[[ "${TAG}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
git fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}"
test "$(git cat-file -t "refs/tags/${TAG}")" = tag
RELEASE_COMMIT="$(git rev-parse --verify "${TAG}^{commit}")"
git merge-base --is-ancestor "${RELEASE_COMMIT}" origin/main
git config gpg.format ssh
git config gpg.ssh.allowedSignersFile "${GITHUB_WORKSPACE}/.github/allowed_signers"
git verify-tag -- "${TAG}"
{
echo "tag=${TAG}"
echo "commit=${RELEASE_COMMIT}"
echo "tag_object_sha=$(git rev-parse "refs/tags/${TAG}")"
} >> "${GITHUB_OUTPUT}"
git checkout --detach "${RELEASE_COMMIT}"
- name: Check package version and prepare release notes
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
VERSION="${TAG#v}"
PACKAGE_VERSION="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')"
test "${VERSION}" = "${PACKAGE_VERSION}"
awk -v ver="${VERSION}" '
$0 ~ ("^## \\[" ver "\\]") { found=1; print; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > release-notes.md
test -s release-notes.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-notes
path: release-notes.md
if-no-files-found: error
- uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: false
- name: Install (locked, mirrors make install)
run: make install
- name: Verify (ruff format --check, ruff check, mypy --strict, pytest+coverage)
run: make verify
- name: Dependency-vulnerability gate (pip-audit + osv-scanner)
run: |
set -eu
curl -sSfL -o osv-scanner \
https://github.com/google/osv-scanner/releases/download/v2.4.0/osv-scanner_linux_amd64
echo "15314940c10d26af9c6649f150b8a47c1262e8fc7e17b1d1029b0e479e8ed8a0 osv-scanner" | sha256sum -c -
chmod +x osv-scanner
sudo mv osv-scanner /usr/local/bin/osv-scanner
make security
# Build the distribution, generate its SBOM, and attest build provenance —
# no caching (CI-CD-STANDARD: a cache hit is an unverified input into an
# artifact we're about to publish and attest).
build:
needs: release-tests
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # keyless OIDC identity for build provenance attestation
attestations: write # publish the attestation to the repo's attestation store
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release-tests.outputs.release_commit }}
persist-credentials: false
- uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
enable-cache: false
# --locked, not --frozen: the SBOM below describes this environment as
# the released wheel's dependency set, so the lock it is built from has
# to be current with pyproject.toml. --frozen exits 0 on a drifted lock
# and would ship an SBOM that quietly disagrees with the package.
- name: Sync locked runtime + extract extra (matches the shipped wheel's deps)
run: uv sync --locked --python 3.12 --extra extract
- name: Build sdist + wheel
run: uv build
# SBOM (SECURITY-AND-SUPPLY-CHAIN-STANDARD §6.2): CycloneDX 1.7 of the
# exact locked environment the released wheel was built against. Closes
# the "no SBOM generation on release yet" gap declared in
# docs/RESPONSIBLE-TECH-AUDITS.md (P1-7).
- name: Generate CycloneDX SBOM
run: |
uvx --from cyclonedx-bom==7.3.0 cyclonedx-py environment .venv \
--sv 1.7 --of JSON --pyproject pyproject.toml \
--output-reproducible --validate -o dist/sbom.cdx.json
# Keyless build provenance: an OIDC-backed GitHub attestation binding
# this workflow run to the built artifacts, verifiable with
# `gh attestation verify`. No signing key is stored anywhere.
- name: Attest build provenance (SLSA, keyless OIDC)
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: "dist/*"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dist
path: dist/
# Create the GitHub Release with the built artifacts, SBOM, and the
# matching CHANGELOG section as release notes.
github-release:
needs: [release-tests, build]
runs-on: ubuntu-latest
permissions:
contents: write # create the GitHub Release and upload assets
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: dist
path: dist/
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-notes
# `gh` ships preinstalled on GitHub-hosted runners — prefer it over a
# third-party action (zizmor superfluous-actions) for the one
# built-in-covered step.
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.release-tests.outputs.release_tag }}
TAG_OBJECT_SHA: ${{ needs.release-tests.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}" \
--title "${TAG}" \
--notes-file release-notes.md \
\
dist/*