Repository navigation
169 lines (151 loc) · 6.29 KB
/
Copy pathrelease.yml
File metadata and controls
169 lines (151 loc) · 6.29 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
name: Release
# A release is a tag push. Nothing here is built by hand on a maintainer's
# laptop, because the v0.1.0 wheel was, and it went out named
# plumbline-0.1.0.dev0 against a tag that said v0.1.0.
on:
push:
tags: ["v*"]
# So a release can be re-run against an existing tag without moving it.
workflow_dispatch:
inputs:
tag:
description: "Existing tag to build and attach, e.g. v0.1.0"
required: true
type: string
# No job gets a token it does not need, so each names its own. The build runs
# the project's code and every dependency's, so it can only read; the job that
# can write runs none of it.
permissions: {}
# Keyed on the tag itself, not the ref, so a tag push and a manual re-run of
# the same tag queue behind each other instead of racing to replace the same
# assets.
concurrency:
group: release-${{ inputs.tag || github.ref_name }}
cancel-in-progress: false
jobs:
resolve:
name: the tag is a release tag
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
# The tag arrives through the environment and is checked before any
# other job sees it. A dispatch input is free text, and one expanded
# straight into a script is a script of the caller's choosing.
- id: tag
env:
TAG: ${{ inputs.tag || github.ref_name }}
run: |
if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::the tag is not a release tag of the form v1.2.3"
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
build:
name: build and check the distributions
needs: resolve
runs-on: ubuntu-latest
permissions:
contents: read
env:
TAG: ${{ needs.resolve.outputs.tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: refs/tags/${{ needs.resolve.outputs.tag }}
fetch-depth: 0
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
python-version: "3.13"
# A release builds from a clean download, never from a cache that
# another workflow could have written.
enable-cache: false
- name: Sync dependencies
run: uv sync --locked
# The whole reason this workflow exists. A tag and a version that
# disagree is exactly what shipped in v0.1.0, and it was invisible
# because the test asserting the version used a substring match.
- name: The tag and the package version must agree
run: |
expected="${TAG#v}"
actual="$(uv run python -c 'import plumbline; print(plumbline.__version__)')"
echo "tag $TAG implies version $expected; package says $actual"
if [ "$expected" != "$actual" ]; then
echo "::error::tag $TAG does not match __version__ $actual. Bump the version or move the tag; do not release a mismatch."
exit 1
fi
- name: The gate still passes at this tag
run: |
uv run ruff check .
uv run ruff format --check .
uv run mypy --strict
uv run pytest -q
- name: Build the wheel and the sdist
run: uv build
- name: The artifact names carry the released version
run: |
version="${TAG#v}"
ls -l dist/
test -f "dist/plumbline-${version}-py3-none-any.whl" \
|| { echo "::error::expected dist/plumbline-${version}-py3-none-any.whl"; exit 1; }
test -f "dist/plumbline-${version}.tar.gz" \
|| { echo "::error::expected dist/plumbline-${version}.tar.gz"; exit 1; }
- name: The wheel installs clean, with neither torch nor transformers
run: |
uv venv --python 3.13 /tmp/relvenv
uv pip install --python /tmp/relvenv/bin/python dist/*.whl
/tmp/relvenv/bin/plumbline version
/tmp/relvenv/bin/python - <<'PY'
import importlib.util as u, pathlib, plumbline
for name in ("torch", "transformers"):
assert u.find_spec(name) is None, f"{name} leaked into the core install"
assert (pathlib.Path(plumbline.__file__).parent / "py.typed").is_file()
print("installed", plumbline.__version__, "clean")
PY
- name: Hand the distributions to the publishing job
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist
path: dist/
if-no-files-found: error
retention-days: 7
publish:
name: attest and attach the distributions
needs: [resolve, build]
runs-on: ubuntu-latest
# This job holds the only write token, so it checks out nothing and runs
# no project code: it signs what the build made and attaches it.
permissions:
contents: write # attaching assets to the release
id-token: write # signing the provenance
attestations: write # storing the provenance
env:
TAG: ${{ needs.resolve.outputs.tag }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
- name: Collect the distributions
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist
# Anyone can then check that a wheel came from this workflow at this
# tag: gh attestation verify plumbline-*.whl -R TMHSDigital/plumbline
- name: Record where the distributions came from
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: dist/*
- name: Attach the distributions to the release
run: |
# The release notes are written by hand, so this only ever adds
# assets to a release that already exists. It does not create one and
# it does not generate notes from commit messages.
if ! gh release view "$TAG" > /dev/null 2>&1; then
echo "::error::no release exists for $TAG. Create it with written notes first, then re-run this workflow."
exit 1
fi
gh release upload "$TAG" dist/* --clobber
echo "attached:"
gh release view "$TAG" --json assets --jq '.assets[].name'