Repository navigation
163 lines (149 loc) · 6.61 KB
/
Copy pathrelease.yml
File metadata and controls
163 lines (149 loc) · 6.61 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
# Build, sign and publish the self-contained binaries.
#
# Two things here are load-bearing and easy to get subtly wrong:
#
# * Asset names must match exactly what install.sh asks for. The installer
# builds the name from `uname -s` and `uname -m`, so the matrix uses those
# values verbatim (Linux-x86_64, Darwin-arm64) rather than prettier ones.
#
# * A frozen bundle links against the glibc it was built on, so the Linux
# build pins the oldest runner available rather than using ubuntu-latest.
# That sets the floor at glibc 2.35 (Ubuntu 22.04, Debian 12, RHEL 9), which
# is stated in the release notes rather than left to be discovered.
#
# manylinux would give a lower floor and was tried first: its CPython is
# built without a shared library, which PyInstaller requires, so the build
# fails outright. Lowering the floor means a container with a shared-library
# Python, not a manylinux image. Worth doing only if something actually
# needs it.
name: release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Tag to build and publish"
required: true
permissions:
contents: write # create the release
id-token: write # keyless signing
attestations: write # build provenance
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
asset: Linux-x86_64
- os: macos-14
asset: Darwin-arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
# Not the system interpreter. macOS runners ship a PEP 668
# externally-managed Python that refuses `pip install` outright, and
# these builds are compiled with --enable-shared, which PyInstaller
# needs and several distribution Pythons lack.
with:
python-version: "3.11"
- name: Build the bundle
run: |
set -euo pipefail
python -m pip install --quiet --upgrade pip
python -m pip install --quiet . pyinstaller
# `bootstrap/regen.py` is shipped as DATA, not as a module: it is
# piped to the consuming repository's own interpreter, and nothing
# in stackward imports it — which is what keeps pydantic out of the
# bundle. PyInstaller's import analysis therefore never sees it, so
# it has to be added explicitly.
python -m PyInstaller --noconfirm --clean --onedir \
--name stackward --paths src --distpath dist --workpath build \
--add-data "src/stackward/bootstrap/regen.py:stackward/bootstrap" \
entry.py
- name: It must actually run
# A bundle that builds is not a bundle that works. The checks, and
# the reasoning behind each, live in `scripts/check-bundle.sh` --
# one definition, run here against what this job just built and
# again in `publish` against the artifact actually being released.
# Two copies of this is how `publish` came to test only `--version`
# and `doctor`, the two commands a missing lazily-dispatched module
# cannot break.
run: |
set -euo pipefail
scripts/check-bundle.sh ./dist/stackward/stackward
# Not in the shared script: this one inspects the PyInstaller
# output tree, which only this job has. `regen.py` ships as data
# precisely so nothing imports it and pydantic stays test-only. If
# it ever became an import, the bundle would still build, doctor
# would still pass, and the only visible symptom would be a fatter
# download -- so the absence has to be asserted, not assumed.
if find dist -iname '*pydantic*' -print -quit | grep -q .; then
echo "::error::pydantic was bundled; it is a test-only dependency" >&2
find dist -iname '*pydantic*' | head -20 >&2
exit 1
fi
- name: Sign for macOS
if: runner.os == 'macOS'
# Ad-hoc signature. Not notarisation, but enough that arm64 will load a
# locally-extracted binary.
run: codesign --force --sign - --deep ./dist/stackward/stackward
- name: Package
env:
ASSET: ${{ matrix.asset }}
run: |
set -euo pipefail
tar -czf "stackward-${ASSET}.tar.gz" -C dist stackward
shasum -a 256 "stackward-${ASSET}.tar.gz" > "stackward-${ASSET}.tar.gz.sha256" \
2>/dev/null || sha256sum "stackward-${ASSET}.tar.gz" > "stackward-${ASSET}.tar.gz.sha256"
cat "stackward-${ASSET}.tar.gz.sha256"
- uses: actions/attest-build-provenance@v2
with:
subject-path: stackward-${{ matrix.asset }}.tar.gz
- uses: actions/upload-artifact@v4
with:
name: stackward-${{ matrix.asset }}
path: |
stackward-${{ matrix.asset }}.tar.gz
stackward-${{ matrix.asset }}.tar.gz.sha256
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Verify the install path before publishing it
# Install exactly as a user would, from the artifacts about to be
# released, and run the same check the build job ran -- against the
# bytes actually being published, which is the only artifact whose
# correctness this job can still act on. A release that cannot be
# installed is deleted, not patched: the tag is already immutable in
# people's lockfiles and instructions.
#
# `scripts/check-bundle.sh`, not a second smoke test written here:
# this step used to run `--version` and `doctor` alone, and those are
# exactly the two commands that stayed green through a bundle that
# was missing every lazily dispatched one.
run: |
set -euo pipefail
cd artifacts
sha256sum --check stackward-Linux-x86_64.tar.gz.sha256
mkdir -p /tmp/probe && tar -xzf stackward-Linux-x86_64.tar.gz -C /tmp/probe
# Absolute: the working directory is `artifacts`, not the checkout.
"$GITHUB_WORKSPACE/scripts/check-bundle.sh" /tmp/probe/stackward/stackward
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event.inputs.tag || github.ref_name }}
run: |
set -euo pipefail
gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "$TAG" \
--generate-notes \
artifacts/* install.sh