-
-
Notifications
You must be signed in to change notification settings - Fork 7
497 lines (484 loc) · 18.7 KB
/
release.yaml
File metadata and controls
497 lines (484 loc) · 18.7 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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
---
name: 🚀 Build & release
# Read https://github.com/actions/runner/issues/491 for insights on complex workflow execution logic.
"on":
workflow_call:
secrets:
PYPI_TOKEN:
required: false
WORKFLOW_UPDATE_GITHUB_PAT:
required: false
outputs:
nuitka_matrix:
description: Nuitka build matrix
value: ${{ toJSON(fromJSON(jobs.metadata.outputs.metadata).nuitka_matrix) }}
# Targets are chosen so that all commits get a chance to have their package tested.
push:
branches:
- main
pull_request:
branches-ignore:
- prepare-release
- renovate/**
paths:
- repomatic/**
- tests/**
- pyproject.toml
- uv.lock
- .github/workflows/release.yaml
concurrency:
# Release commits get a unique SHA-based group so they can never be cancelled.
# See repomatic/github/actions.py for rationale.
group: >-
${{ github.workflow }}-${{
github.event.pull_request.number
|| (
(startsWith(github.event.head_commit.message, '[changelog] Release')
|| startsWith(github.event.head_commit.message, '[changelog] Post-release'))
&& github.sha
)
|| github.ref
}}
cancel-in-progress: true
jobs:
detect-squash-merge:
name: 🧯 No squash on release
# Detect squash merges on release PRs and open an issue instead of releasing.
# See RELEASE_COMMIT_PATTERN in repomatic/metadata.py for the detection mechanism.
if: >-
github.event_name == 'push'
&& startsWith(github.event.head_commit.message, 'Release `v')
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v6.0.2
- uses: astral-sh/setup-uv@v7.3.1
- name: Extract PR reference
id: extract
env:
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: |
pr_ref=$(echo "${COMMIT_MSG}" | grep -oE '#[0-9]+' | tail -1)
echo "pr_ref=${pr_ref}" >> "$GITHUB_OUTPUT"
- name: Generate issue body
id: issue-metadata
env:
PR_REF: ${{ steps.extract.outputs.pr_ref }}
run: >
uvx --no-progress --from . repomatic pr-body
--template detect-squash-merge
--pr-ref "${PR_REF}"
--output "$GITHUB_OUTPUT"
- name: Open issue to notify maintainer
id: issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TITLE: ${{ steps.issue-metadata.outputs.title }}
BODY: ${{ steps.issue-metadata.outputs.body }}
run: |
issue_url=$(gh issue create \
--title "${TITLE}" \
--assignee "${GITHUB_ACTOR}" \
--body "${BODY}")
echo "issue_url=${issue_url}" >> "$GITHUB_OUTPUT"
- name: Fail workflow
env:
ISSUE_URL: ${{ steps.issue.outputs.issue_url }}
run: |
echo "::error::Squash merge detected. Release was skipped. See ${ISSUE_URL}"
exit 1
metadata:
name: 🧬 Project metadata
runs-on: ubuntu-slim
outputs:
metadata: ${{ steps.metadata.outputs.metadata }}
steps:
- uses: actions/checkout@v6.0.2
with:
# Checkout pull request HEAD commit to ignore actions/checkout's merge commit. Fallback to push SHA.
# See: https://github.com/actions/checkout/issues/504
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# We're going to browse all new commits.
fetch-depth: 0
- name: List all branches
run: git branch --all
- name: List all commits
run: git log --decorate=full --oneline
- uses: astral-sh/setup-uv@v7.3.1
- name: Run repomatic metadata
id: metadata
run: >
uvx --no-progress --from . repomatic metadata
--format github-json --output "$GITHUB_OUTPUT"
new_commits_matrix release_commits_matrix build_targets nuitka_matrix
is_python_project package_name current_version released_version
release_notes release_notes_with_admonition skip_binary_build
build-package:
name: 📦 Build package (${{ matrix.short_sha }})
needs:
- metadata
if: fromJSON(needs.metadata.outputs.metadata).is_python_project
strategy:
matrix: >-
${{ fromJSON(needs.metadata.outputs.metadata).release_commits_matrix
|| fromJSON(needs.metadata.outputs.metadata).new_commits_matrix }}
runs-on: ubuntu-slim
permissions:
id-token: write
attestations: write
steps:
- uses: actions/checkout@v6.0.2
with:
ref: ${{ matrix.commit }}
- uses: astral-sh/setup-uv@v7.3.1
- name: Build package
run: uv --no-progress build
- name: Generate build attestations
uses: actions/attest-build-provenance@v3.2.0
with:
subject-path: ./dist/*
- name: Upload artifacts
uses: actions/upload-artifact@v6.0.0
with:
name: ${{ github.event.repository.name }}-${{ matrix.short_sha }}
path: ./dist/*
compile-binaries:
name: "${{ matrix.state == 'stable' && '✅' || '⁉️' }} ${{ matrix.os }}, ${{ matrix.short_sha }} build"
needs:
- metadata
- create-release
# Skip binary compilation for branches that don't affect code (e.g., update-mailmap, format-markdown).
# Use always() because create-release is skipped on non-release pushes and PRs.
if: >-
always()
&& needs.metadata.result == 'success'
&& fromJSON(needs.metadata.outputs.metadata).nuitka_matrix
&& !(fromJSON(needs.metadata.outputs.metadata).skip_binary_build || false)
strategy:
matrix: ${{ fromJSON(needs.metadata.outputs.metadata).nuitka_matrix }}
runs-on: ${{ matrix.os }}
# We keep going when a job flagged as not stable fails.
continue-on-error: ${{ matrix.state == 'unstable' }}
permissions:
id-token: write
attestations: write
contents: write
steps:
- uses: actions/checkout@v6.0.2
with:
ref: ${{ matrix.commit }}
- uses: astral-sh/setup-uv@v7.3.1
- name: Setup venv
run: uv --no-progress venv --python 3.13
- name: Install Nuitka
run: uv --no-progress pip install 'nuitka[onefile]==2.8.10'
- name: Pre-bake version with commit hash
if: contains(matrix.current_version, '.dev')
env:
SHORT_SHA: ${{ matrix.short_sha }}
shell: bash
run: >
uvx --no-progress --from . repomatic
prebake-version --hash "${SHORT_SHA}"
- name: Nuitka + compilers versions
run: uv --no-progress run --frozen -- nuitka --version
- name: Build binary
id: build-binary
continue-on-error: true
# Project-specific Nuitka flags come from [tool.repomatic]
# nuitka-extra-args in pyproject.toml, passed via the build matrix.
env:
NUITKA_EXTRA_ARGS: ${{ matrix.nuitka_extra_args }}
BIN_NAME: ${{ matrix.bin_name }}
MODULE_PATH: ${{ matrix.module_path }}
shell: bash
run: |
# shellcheck disable=SC2086
uv --no-progress run --frozen -- nuitka \
--onefile --assume-yes-for-downloads \
${NUITKA_EXTRA_ARGS} \
--output-filename="${BIN_NAME}" "${MODULE_PATH}"
- name: Upload Nuitka crash report
uses: actions/upload-artifact@v6.0.0
with:
name: nuitka-crash-report-${{ matrix.os }}-${{ matrix.short_sha }}.xml
if-no-files-found: ignore
path: nuitka-crash-report.xml
- if: steps.build-binary.outcome == 'failure'
run: |
echo "Nuitka build failed, skipping the rest of the steps."
exit 1
- name: Install exiftool - Linux
if: runner.os == 'Linux'
run: sudo apt --quiet --yes install exiftool
- name: Install exiftool - macOS
if: runner.os == 'macOS'
run: brew install exiftool
- name: Install exiftool - Windows
if: runner.os == 'Windows'
run: choco install exiftool --no-progress --yes --retry-count=3
- name: Verify binary architecture
env:
TARGET: ${{ matrix.target }}
BIN_NAME: ${{ matrix.bin_name }}
shell: bash
run: >
uvx --no-progress --from . repomatic
verify-binary --target "${TARGET}" --binary "${BIN_NAME}"
- name: Upload binaries
uses: actions/upload-artifact@v6.0.0
with:
# Artifact name includes SHA for uniqueness across commits; the file
# inside uses the clean release name (no SHA suffix).
name: ${{ matrix.bin_name }}-${{ matrix.short_sha }}
if-no-files-found: warn
path: ${{ matrix.bin_name }}
- name: Generate binary attestation
if: >-
github.ref == 'refs/heads/main'
&& fromJSON(needs.metadata.outputs.metadata).release_commits_matrix
uses: actions/attest-build-provenance@v3.2.0
with:
subject-path: ${{ matrix.bin_name }}
- name: Upload binary to GitHub release
if: >-
github.ref == 'refs/heads/main'
&& fromJSON(needs.metadata.outputs.metadata).release_commits_matrix
env:
GH_TOKEN: ${{ secrets.WORKFLOW_UPDATE_GITHUB_PAT || secrets.GITHUB_TOKEN }}
CURRENT_VERSION: ${{ matrix.current_version }}
BIN_NAME: ${{ matrix.bin_name }}
shell: bash
run: >
gh release upload "v${CURRENT_VERSION}"
"${BIN_NAME}"
--repo "${{ github.repository }}"
test-binaries:
name: "${{ matrix.state == 'stable' && '✅' || '⁉️' }} ${{ matrix.os }}, ${{ matrix.short_sha }} test"
needs:
- metadata
- compile-binaries
# Skip binary tests for branches that don't affect code (e.g., update-mailmap, format-markdown).
if: >
fromJSON(needs.metadata.outputs.metadata).nuitka_matrix
&& !(fromJSON(needs.metadata.outputs.metadata).skip_binary_build || false)
strategy:
matrix: ${{ fromJSON(needs.metadata.outputs.metadata).nuitka_matrix }}
runs-on: ${{ matrix.os }}
# We keep going when a job flagged as not stable fails.
continue-on-error: ${{ matrix.state == 'unstable' }}
steps:
- uses: actions/checkout@v6.0.2
with:
ref: ${{ matrix.commit }}
- name: Download artifact
uses: actions/download-artifact@v7.0.0
id: artifacts
with:
name: ${{ matrix.bin_name }}-${{ matrix.short_sha }}
- name: Set binary permissions
if: runner.os != 'Windows'
env:
DOWNLOAD_PATH: ${{ steps.artifacts.outputs.download-path }}
BIN_NAME: ${{ matrix.bin_name }}
run: chmod +x "${DOWNLOAD_PATH}/${BIN_NAME}"
- uses: astral-sh/setup-uv@v7.3.1
- name: Run test plan for binary
env:
DOWNLOAD_PATH: ${{ steps.artifacts.outputs.download-path }}
BIN_NAME: ${{ matrix.bin_name }}
shell: bash
run: >
uvx --no-progress --from . repomatic test-plan
--binary "${DOWNLOAD_PATH}/${BIN_NAME}"
create-tag:
name: 📌 Tag release (${{ matrix.short_sha }})
needs:
- metadata
# Only consider pushes to main branch as triggers for releases.
if: github.ref == 'refs/heads/main' && fromJSON(needs.metadata.outputs.metadata).release_commits_matrix
strategy:
matrix: ${{ fromJSON(needs.metadata.outputs.metadata).release_commits_matrix }}
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v6.0.2
with:
ref: ${{ matrix.commit }}
# PAT required so tag pushes trigger downstream on.push.tags workflows.
# See repomatic/git_ops.py module docstring.
token: ${{ secrets.WORKFLOW_UPDATE_GITHUB_PAT || secrets.GITHUB_TOKEN }}
- uses: astral-sh/setup-uv@v7.3.1
- name: Create and push tag
# Idempotent: skips if tag already exists instead of failing. This allows re-running
# workflows interrupted after tag creation.
env:
CURRENT_VERSION: ${{ matrix.current_version }}
COMMIT: ${{ matrix.commit }}
run: >
uvx --no-progress --from . repomatic git-tag
--tag "v${CURRENT_VERSION}"
--commit "${COMMIT}"
--skip-existing
publish-pypi:
name: 🐍 Publish to PyPI (${{ matrix.short_sha }})
needs:
- metadata
- build-package
- create-tag
if: fromJSON(needs.metadata.outputs.metadata).package_name
strategy:
matrix: ${{ fromJSON(needs.metadata.outputs.metadata).release_commits_matrix }}
runs-on: ubuntu-slim
permissions:
# Allow GitHub's OIDC provider to create a JSON Web Token:
# https://github.blog/changelog/2023-06-15-github-actions-securing-openid-connect-oidc-token-permissions-in-reusable-workflows/
# https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
id-token: write
attestations: write
steps:
- uses: astral-sh/setup-uv@v7.3.1
- name: Download build artifacts
uses: actions/download-artifact@v7.0.0
id: download
with:
name: ${{ github.event.repository.name }}-${{ matrix.short_sha }}
- name: Generate attestations
uses: actions/attest-build-provenance@v3.2.0
with:
subject-path: "${{ steps.download.outputs.download-path }}/*"
- name: Push to PyPI
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
DOWNLOAD_PATH: ${{ steps.download.outputs.download-path }}
run: >
uv --no-progress publish --token "${PYPI_TOKEN}"
"${DOWNLOAD_PATH}/*"
create-release:
name: 🐙 Create GitHub release draft (${{ matrix.short_sha }})
needs:
- metadata
- build-package
- create-tag
- publish-pypi
# Make sure this job always starts if create-tag ran and succeeded.
if: always() && needs.create-tag.result == 'success'
strategy:
matrix: ${{ fromJSON(needs.metadata.outputs.metadata).release_commits_matrix }}
runs-on: ubuntu-slim
permissions:
# Allow GitHub's OIDC provider to create a JSON Web Token:
# https://github.blog/changelog/2023-06-15-github-actions-securing-openid-connect-oidc-token-permissions-in-reusable-workflows/
# https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
id-token: write
attestations: write
# Allow project without WORKFLOW_UPDATE_GITHUB_PAT to create a GitHub release.
contents: write
steps:
- uses: actions/checkout@v6.0.2
with:
ref: ${{ matrix.commit }}
- uses: astral-sh/setup-uv@v7.3.1
with:
enable-cache: false
- name: Download Python package
# Do not fetch build artifacts if build-package was skipped (non-Python projects).
if: needs.build-package.result != 'skipped'
uses: actions/download-artifact@v7.0.0
id: artifacts
with:
path: release_artifact
name: ${{ github.event.repository.name }}-${{ matrix.short_sha }}
- name: Generate attestations
# Do not try to attest artifacts if none have been downloaded.
if: steps.artifacts.outputs.download-path
uses: actions/attest-build-provenance@v3.2.0
with:
subject-path: release_artifact/*
- name: Delete dev pre-release
# Clean up all rolling dev releases before creating the real release.
# See repomatic/github/dev_release.py for rationale.
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.WORKFLOW_UPDATE_GITHUB_PAT || secrets.GITHUB_TOKEN }}
run: uvx --no-progress --from . repomatic sync-dev-release --live --delete
- name: Create GitHub release draft
# Idempotent: skips if release already exists (e.g. workflow re-run).
env:
GH_TOKEN: ${{ secrets.WORKFLOW_UPDATE_GITHUB_PAT || secrets.GITHUB_TOKEN }}
CURRENT_VERSION: ${{ matrix.current_version }}
COMMIT: ${{ matrix.commit }}
REPO: ${{ github.repository }}
RELEASE_NOTES: >-
${{ needs.publish-pypi.result == 'success'
&& fromJSON(needs.metadata.outputs.metadata).release_notes_with_admonition
|| fromJSON(needs.metadata.outputs.metadata).release_notes }}
run: |
tag="v${CURRENT_VERSION}"
if gh release view "${tag}" --repo "${REPO}" > /dev/null 2>&1; then
echo "Release ${tag} already exists, skipping creation."
else
shopt -s nullglob
files=(release_artifact/*)
gh release create "${tag}" \
--draft \
--target "${COMMIT}" \
--title "${tag}" \
--repo "${REPO}" \
--notes-file - \
"${files[@]}" <<< "${RELEASE_NOTES}"
fi
publish-release:
name: 🎉 Publish GitHub release (${{ matrix.short_sha }})
needs:
- metadata
- create-release
- compile-binaries
# Wait for all upstream jobs regardless of result, but only publish if the draft was created.
if: >-
always()
&& needs.create-release.result == 'success'
strategy:
matrix: ${{ fromJSON(needs.metadata.outputs.metadata).release_commits_matrix }}
runs-on: ubuntu-slim
permissions:
contents: write
steps:
- name: Publish release
env:
GH_TOKEN: ${{ secrets.WORKFLOW_UPDATE_GITHUB_PAT || secrets.GITHUB_TOKEN }}
CURRENT_VERSION: ${{ matrix.current_version }}
REPO: ${{ github.repository }}
run: >
gh release edit "v${CURRENT_VERSION}"
--draft=false
--repo "${REPO}"
sync-dev-release:
name: 🔄 Sync dev pre-release
needs:
- metadata
- build-package
- compile-binaries
# Run on non-release pushes to main after builds complete.
# Uses always() because compile-binaries depends on create-release (skipped for non-release pushes).
if: >-
always()
&& github.ref == 'refs/heads/main'
&& !fromJSON(needs.metadata.outputs.metadata).release_commits_matrix
&& fromJSON(needs.metadata.outputs.metadata).current_version
runs-on: ubuntu-slim
permissions:
contents: write
steps:
- uses: actions/checkout@v6.0.2
- uses: astral-sh/setup-uv@v7.3.1
- name: Download all build artifacts
uses: actions/download-artifact@v7.0.0
with:
path: release_assets
merge-multiple: true
- name: Sync dev release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: >
uvx --no-progress --from . repomatic sync-dev-release --live
--upload-assets release_assets