-
Notifications
You must be signed in to change notification settings - Fork 4
252 lines (226 loc) · 10.2 KB
/
Copy pathbuild-python.yml
File metadata and controls
252 lines (226 loc) · 10.2 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
name: Build Python Packages
on:
push:
pull_request:
workflow_dispatch:
inputs:
release_date:
description: >
Release tag (YYYYMMDD). When set, a single GitHub release with that
tag is created and all per-platform tarballs from every matrix entry
are published as assets. Leave empty for a build-only run that uploads
per-job artifacts but does not publish a release.
required: false
type: string
default: ""
# Cancel in-flight runs when a newer event arrives for the same logical branch.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
jobs:
setup:
name: Read build matrix from manifest
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.read.outputs.versions }}
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Extract Python versions from manifest.json
id: read
# manifest.json is the single source of truth: it both selects which
# CPython versions are built here and is published as a release asset
# (see publish-release) for serious_python / flet to consume.
run: echo "versions=$(jq -c '[.pythons[].full_version]' manifest.json)" >> "$GITHUB_OUTPUT"
build-matrix:
name: Build Python ${{ matrix.python_version }}
needs: setup
strategy:
fail-fast: false
matrix:
python_version: ${{ fromJSON(needs.setup.outputs.versions) }}
uses: ./.github/workflows/build-python-version.yml
with:
python_version: ${{ matrix.python_version }}
# Release-only, isolated signing job. The provider certificate is never
# available to build-matrix (which runs on every push and PR); it lives in the
# protected `release-signing` environment and is only reachable from an
# explicit release dispatch on main.
#
# Signing operates on the finished archives rather than inside the build, which
# guarantees the required ordering: every mutation (install names, plists,
# privacy manifests, headers, pruning, stripping) is already done by the time
# an archive exists. The script re-packs and re-verifies after a round trip.
sign-darwin-artifacts:
name: Provider-sign Darwin XCFrameworks
runs-on: macos-26
if: >-
github.event_name == 'workflow_dispatch'
&& inputs.release_date != ''
&& github.ref == 'refs/heads/main'
needs:
- build-matrix
environment: release-signing
env:
XCFRAMEWORK_EXPECTED_TEAM_ID: ${{ vars.XCFRAMEWORK_EXPECTED_TEAM_ID }}
# Missing credentials, a missing secure timestamp, a wrong team, or an
# archive containing zero XCFrameworks all fail the release here.
REQUIRE_XCFRAMEWORK_SIGNATURE: '1'
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Download unsigned Darwin archives
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: darwin-unsigned-*
path: unsigned
merge-multiple: true
- name: Import Apple Distribution certificate into a temporary keychain
env:
CERT_P12_BASE64: ${{ secrets.APPLE_DISTRIBUTION_CERT_P12_BASE64 }}
CERT_P12_PASSWORD: ${{ secrets.APPLE_DISTRIBUTION_CERT_P12_PASSWORD }}
run: |
set -euo pipefail
: "${CERT_P12_BASE64:?APPLE_DISTRIBUTION_CERT_P12_BASE64 is not set}"
: "${CERT_P12_PASSWORD:?APPLE_DISTRIBUTION_CERT_P12_PASSWORD is not set}"
KEYCHAIN_PATH="$RUNNER_TEMP/xcframework-signing.keychain-db"
CERT_PATH="$RUNNER_TEMP/xcframework-signing.p12"
# Ephemeral: the keychain lives for this job only and is deleted in the
# always-run cleanup step, so the password never leaves this step.
KEYCHAIN_PASSWORD=$(openssl rand -base64 24)
printf '%s' "$CERT_P12_BASE64" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# No -A: the private key is reachable only by the two Apple tools named
# below, not by any process that happens to run in this job.
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$CERT_P12_PASSWORD" \
-f pkcs12 -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -s \
-k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" >/dev/null
# codesign resolves an identity through the search list even when
# --keychain is passed, so prepend ours to the user list.
security list-keychains -d user -s "$KEYCHAIN_PATH" \
$(security list-keychains -d user | tr -d '"')
# Derive EXACTLY ONE fingerprint. Selecting by display name is
# ambiguous when a keychain holds more than one matching certificate,
# and codesign then picks arbitrarily; a hard count check turns a
# multi-certificate .p12 into a build failure instead of a coin flip.
IDENTITIES=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH")
echo "$IDENTITIES"
FPRS=$(printf '%s\n' "$IDENTITIES" \
| sed -n 's/^ *[0-9]*) \([0-9A-F]\{40\}\) .*/\1/p' | sort -u)
COUNT=$(printf '%s' "$FPRS" | grep -c . || true)
if [ "$COUNT" -ne 1 ]; then
echo "::error::expected exactly 1 codesigning identity in the imported keychain, found $COUNT"
exit 1
fi
# Fingerprint and keychain path are not secrets.
echo "XCFRAMEWORK_CODESIGN_IDENTITY=$FPRS" >> "$GITHUB_ENV"
echo "XCFRAMEWORK_SIGNING_KEYCHAIN=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Sign and re-pack Darwin archives
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
archives=(unsigned/python-*.tar.gz)
if [ "${#archives[@]}" -eq 0 ]; then
echo "::error::no Darwin archives to sign"
exit 1
fi
bash darwin/sign_darwin_archives.sh signed "${archives[@]}"
- name: Upload signed Darwin archives
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: python-darwin-signed
path: signed/python-*.tar.gz
if-no-files-found: error
- name: Remove temporary keychain and certificate
if: always()
run: |
security delete-keychain "$RUNNER_TEMP/xcframework-signing.keychain-db" 2>/dev/null || true
rm -f "$RUNNER_TEMP/xcframework-signing.p12"
publish-release:
name: Publish Release Assets
runs-on: ubuntu-latest
# Date-keyed releases (PBS-style): only publish when an operator explicitly
# triggers via workflow_dispatch with a `release_date` input, and only from
# the protected main branch — the same condition that gates real signing, so
# a release can never be assembled from artifacts that were never signed.
if: >-
github.event_name == 'workflow_dispatch'
&& inputs.release_date != ''
&& github.ref == 'refs/heads/main'
needs:
- setup
- build-matrix
- sign-darwin-artifacts
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
# Downloaded by explicit pattern, never `python-*` wholesale: the unsigned
# Darwin build artifacts are named `darwin-unsigned-*` precisely so no
# pattern here can reach them. The only Darwin tarballs that enter the
# release directory are the ones sign-darwin-artifacts produced.
- name: Download Android build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: python-android-*
path: release-artifacts
merge-multiple: true
- name: Download Linux build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: python-linux-*
path: release-artifacts
merge-multiple: true
- name: Download Windows build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: python-windows-*
path: release-artifacts
merge-multiple: true
- name: Download signed Darwin artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: python-darwin-signed
path: release-artifacts
- name: Assert the release payload carries the signed Darwin tarballs
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
ls -lh release-artifacts
for kind in python-ios-dart python-macos-dart python-ios-mobile-forge; do
found=(release-artifacts/$kind-*.tar.gz)
if [ "${#found[@]}" -eq 0 ]; then
echo "::error::no $kind-*.tar.gz in the release payload"
exit 1
fi
done
- name: Add runtime manifest (with release date) to the release
# Publish the same manifest.json that drove this build, with the release
# date injected, so consumers can fetch a consistent version set by date.
env:
INPUTS_RELEASE_DATE: ${{ inputs.release_date }}
run: jq --arg date "$INPUTS_RELEASE_DATE" '.release = $date' manifest.json > release-artifacts/manifest.json
- name: Publish all artifacts to release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
tag_name: ${{ inputs.release_date }}
name: ${{ inputs.release_date }}
files: release-artifacts/*
fail_on_unmatched_files: true
generate_release_notes: false
draft: false
prerelease: false