forked from ChelseaKR/habitable
-
Notifications
You must be signed in to change notification settings - Fork 0
123 lines (115 loc) · 5.44 KB
/
Copy pathrelease.yml
File metadata and controls
123 lines (115 loc) · 5.44 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
# SPDX-License-Identifier: AGPL-3.0-or-later
name: release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Existing tag to (re)build and attest"
required: true
permissions:
contents: read
# A tag push and a manual re-run of the same tag must never race each other into
# two concurrent publishes of the same release; queue them instead of cancelling
# (cancelling a half-finished publish is worse than waiting).
concurrency:
group: release
cancel-in-progress: false
jobs:
release:
name: build · SBOM · provenance · publish
runs-on: ubuntu-latest
# P3 (CICD-04): write grants scoped to the job, not the whole workflow —
# rewards a tighter blast radius (OpenSSF Scorecard Token-Permissions check).
permissions:
contents: write # create/edit the GitHub release
id-token: write # sigstore signing for provenance
attestations: write # build-provenance attestations
steps:
# P1-3 (SEC-04): audit-mode egress monitoring; see ci.yml's Harden Runner
# step for the telemetry trade-off note. Flip to `block` + an explicit
# allowlist once a few real release runs show the actual endpoint set
# (PyPI/uv index, GitHub API, Sigstore) — don't guess the allowlist blind.
- name: Harden Runner (audit mode)
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout (full history + tags — needed to verify the tag signature)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve the release tag
id: tag
env:
# Bind the (potentially attacker-influenceable, on a workflow_dispatch
# input) values to an env var rather than expanding `${{ }}` directly
# into the shell command — avoids template-injection (zizmor High).
INPUT_TAG: ${{ github.event.inputs.tag }}
REF_NAME: ${{ github.ref_name }}
run: echo "tag=${INPUT_TAG:-$REF_NAME}" >> "$GITHUB_OUTPUT"
# --- REL-02/03/08: release identity guard ---------------------------------
# An evidence tool's artifact identity is part of its safety case: a tag whose
# version doesn't match pyproject, or that isn't signed, must never reach a
# published release. This fails BEFORE any build step runs.
#
# Uses SSH-format signature verification (git's `gpg.format=ssh`): simpler to
# bootstrap in CI than importing a GPG key, and git has supported it natively
# since 2.34. `.github/allowed_signers` must contain the maintainer's real
# public signing key for this to ever succeed — see that file's header.
- name: Configure tag-signature verification (SSH format)
run: |
git config gpg.format ssh
git config gpg.ssh.allowedSignersFile "${GITHUB_WORKSPACE}/.github/allowed_signers"
- name: "Guard: tag must be signed"
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
if ! git tag -v "$TAG" 2>/tmp/tag-verify.log; then
echo "::error::Tag '$TAG' is not a valid signed tag (git tag -v failed)."
echo "::error::habitable requires signed release tags (REL-08). See docs/releasing.md."
cat /tmp/tag-verify.log
exit 1
fi
- name: "Guard: tag version must match pyproject.toml"
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
TAG_VERSION="${TAG#v}"
PYPROJECT_VERSION="$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/')"
if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then
echo "::error::Tag '$TAG' (version $TAG_VERSION) does not match pyproject.toml version '$PYPROJECT_VERSION'."
exit 1
fi
echo "Tag version $TAG_VERSION matches pyproject.toml."
# --- REL-14/15: release re-runs the full merge gate before it ships --------
- name: Install uv (pinned)
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.11.19"
enable-cache: false # this job runs once per release; a stale cache is not worth the risk
- name: Sync environment (Python 3.14, locked, with dev tools)
run: uv sync --frozen
- name: Run the full gate at the tagged commit (ruff + mypy --strict + pytest + coverage + i18n)
run: make verify
- name: Build wheel + sdist
run: uv build
- name: Generate a runtime SBOM (CycloneDX)
run: |
uv sync --frozen --no-dev
uvx cyclonedx-py environment .venv > dist/sbom.cdx.json
- name: Attest build provenance (signed via Sigstore)
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: "dist/*.whl,dist/*.tar.gz"
- name: Publish artifacts to the GitHub release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
gh release view "$TAG" >/dev/null 2>&1 \
|| gh release create "$TAG" --title "habitable $TAG" --generate-notes
gh release upload "$TAG" dist/* --clobber
# Note: PyPI publishing via Trusted Publishing (OIDC) is the next step once a
# PyPI project + trusted publisher are configured; see docs/releasing.md.