Repository navigation
429 lines (386 loc) · 16.8 KB
/
Copy pathrelease.yml
File metadata and controls
429 lines (386 loc) · 16.8 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
name: Release NixOS image
# Builds the PiFinder NixOS SD images (lean, and full when flake.nix has
# catalog images) and the migration tarball and publishes them as
# a GitHub (pre-)release, and records the closure in the nixos-manifest branch.
#
# Runners are native aarch64 (no QEMU): the free GitHub-hosted
# `ubuntu-24.04-arm` runner builds first (faster than the Pi5; Attic substitutes
# everything already built, so only a kernel/source change compiles), with the
# self-hosted Pi5 as a last-resort fallback if the hosted build fails —
# mirroring nixos-pr-build.yml. The arch-sensitive `nix build` runs there; the
# image is handed off as an artifact and the arch-independent packaging +
# publishing happens on ubuntu-latest.
on:
workflow_dispatch:
inputs:
version:
description: 'Version (e.g., 2.5.0)'
required: true
notes:
description: 'Release notes'
required: true
type:
description: 'Release type'
type: choice
options:
- stable
- beta
default: stable
source_branch:
description: 'Source branch (default: main, use release/X.Y for hotfixes)'
required: false
default: 'main'
source_repository:
description: 'Source repo to build (blank = this repo; e.g. mrosseel/PiFinder to build a fork)'
required: false
default: ''
new_migration_tarball:
description: 'Build a new migration tarball (only when the migration itself changes; off = use the newest tarball in the manifest)'
type: boolean
default: false
# Read-only by default. The release job asks for contents: write itself.
permissions:
contents: read
jobs:
# Last-resort fallback: self-hosted Pi5. Only runs if the hosted build failed
# (Pi offline/at capacity). Slow for a from-scratch kernel, hence the timeout.
build-native:
needs: build-hosted
if: always() && needs.build-hosted.result == 'failure'
runs-on: [self-hosted, aarch64]
timeout-minutes: 240
outputs:
store_path: ${{ steps.push.outputs.store_path }}
source_sha: ${{ steps.sha.outputs.source_sha }}
steps:
- uses: actions/checkout@v4
with:
repository: ${{ inputs.source_repository || github.repository }}
ref: ${{ inputs.source_branch }}
persist-credentials: false
- name: Record source SHA
id: sha
run: echo "source_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
# The shared CI action and scripts come from the ref that runs this
# workflow, because the source to build can be another repo or branch.
- uses: actions/checkout@v4
with:
path: .ci
sparse-checkout: .github
persist-credentials: false
- uses: ./.ci/.github/actions/setup-nix
with:
attic-token: ${{ secrets.ATTIC_TOKEN }}
self-hosted: "true"
- name: Build and push release closure to Attic
id: push
run: |
STORE_PATH=$(nix build .#nixosConfigurations.pifinder.config.system.build.toplevel \
-L --json | jq -r '.[].outputs.out')
# Stable → retained pifinder-release (GC off; devices resolve it months
# later). Beta → dev pifinder cache (finite retention), so prereleases
# stay transient.
if [[ "${{ inputs.type }}" == "beta" ]]; then
attic push pifinder:pifinder "$STORE_PATH"
else
attic push pifinder:pifinder-release "$STORE_PATH"
fi
echo "store_path=$STORE_PATH" >> "$GITHUB_OUTPUT"
- name: Push dev shell to the cache
run: .ci/.github/scripts/push_dev_shell.sh aarch64-linux
# Lean image: the system and an empty PiFinder_data.
- name: Build SD image
run: |
nix build .#images.pifinder -L -o result-sd
- name: Upload SD image
uses: actions/upload-artifact@v4
with:
name: sdimage-native
path: result-sd/sd-image/*.img.zst
retention-days: 1
# Full image: the lean image plus the catalog images. It exists only when
# flake.nix sets catalogImagesSrc (plan A4 and B8).
- name: Build full SD image
id: full
run: |
if [ "$(nix eval --json .#images --apply 'i: i ? pifinder-full')" = true ]; then
nix build .#images.pifinder-full -L -o result-full
echo "built=true" >> "$GITHUB_OUTPUT"
else
echo "No images.pifinder-full: publishing the lean image only."
fi
- name: Upload full SD image
if: steps.full.outputs.built == 'true'
uses: actions/upload-artifact@v4
with:
name: sdfull-native
path: result-full/sd-image/*.img.zst
retention-days: 1
# The migration tarball holds the MINIMAL migration system, built
# straight from its closure. It resolves the full system from the update
# manifest at first boot (ADR 0039), so one tarball serves many releases.
# It is built only when new_migration_tarball is set.
- name: Build migration tarball
if: inputs.new_migration_tarball
run: |
nix build .#packages.aarch64-linux.migration-tarball -L -o result-mig
- name: Upload migration image
if: inputs.new_migration_tarball
uses: actions/upload-artifact@v4
with:
name: migimage-native
path: result-mig/pifinder-migration.tar.zst
retention-days: 1
# Primary: free GitHub-hosted arm64 runner (native aarch64, no QEMU). Attic
# substitutes anything already built, so only a kernel/source change compiles
# from scratch (~1 h here); the result is pushed back so the next build is fast.
build-hosted:
runs-on: ubuntu-24.04-arm
timeout-minutes: 150
outputs:
store_path: ${{ steps.push.outputs.store_path }}
source_sha: ${{ steps.sha.outputs.source_sha }}
steps:
- uses: actions/checkout@v4
with:
repository: ${{ inputs.source_repository || github.repository }}
ref: ${{ inputs.source_branch }}
persist-credentials: false
- name: Record source SHA
id: sha
run: echo "source_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
# The shared CI action and scripts come from the ref that runs this
# workflow, because the source to build can be another repo or branch.
- uses: actions/checkout@v4
with:
path: .ci
sparse-checkout: .github
persist-credentials: false
- uses: ./.ci/.github/actions/setup-nix
with:
attic-token: ${{ secrets.ATTIC_TOKEN }}
- name: Build and push release closure to Attic
id: push
run: |
STORE_PATH=$(nix build .#nixosConfigurations.pifinder.config.system.build.toplevel \
-L --json | jq -r '.[].outputs.out')
if [[ "${{ inputs.type }}" == "beta" ]]; then
attic push pifinder:pifinder "$STORE_PATH"
else
attic push pifinder:pifinder-release "$STORE_PATH"
fi
echo "store_path=$STORE_PATH" >> "$GITHUB_OUTPUT"
- name: Push dev shell to the cache
run: .ci/.github/scripts/push_dev_shell.sh aarch64-linux
# Lean image: the system and an empty PiFinder_data.
- name: Build SD image
run: |
nix build .#images.pifinder -L -o result-sd
- name: Upload SD image
uses: actions/upload-artifact@v4
with:
name: sdimage-hosted
path: result-sd/sd-image/*.img.zst
retention-days: 1
# Full image: the lean image plus the catalog images. It exists only when
# flake.nix sets catalogImagesSrc (plan A4 and B8).
- name: Build full SD image
id: full
run: |
if [ "$(nix eval --json .#images --apply 'i: i ? pifinder-full')" = true ]; then
nix build .#images.pifinder-full -L -o result-full
echo "built=true" >> "$GITHUB_OUTPUT"
else
echo "No images.pifinder-full: publishing the lean image only."
fi
- name: Upload full SD image
if: steps.full.outputs.built == 'true'
uses: actions/upload-artifact@v4
with:
name: sdfull-hosted
path: result-full/sd-image/*.img.zst
retention-days: 1
# Minimal migration system — see the native job's comment (ADR 0039).
- name: Build migration tarball
if: inputs.new_migration_tarball
run: |
nix build .#packages.aarch64-linux.migration-tarball -L -o result-mig
- name: Upload migration image
if: inputs.new_migration_tarball
uses: actions/upload-artifact@v4
with:
name: migimage-hosted
path: result-mig/pifinder-migration.tar.zst
retention-days: 1
# Arch-independent: package the migration tarball + checksums and publish. Runs
# on whichever build succeeded (hosted preferred).
release:
needs: [build-hosted, build-native]
if: |
always() &&
(needs.build-hosted.result == 'success' || needs.build-native.result == 'success')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
# Base repo (this repo, not the fork source): supplies the TRUSTED manifest
# scripts and the `origin` the manifest branch is pushed to. Keeps default
# credentials for that push.
- name: Checkout base repo
uses: actions/checkout@v4
# Download hosted and native images to separate dirs. If both builds ran
# (hosted failed → Pi5 fallback fired) their images share a filename, so
# merging them would corrupt the archive — never merge; the assemble step
# prefers the hosted (primary) artifact.
- name: Download native SD image
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: sdimage-native
path: img-native
- name: Download hosted SD image
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: sdimage-hosted
path: img-hosted
- name: Download native full SD image
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: sdfull-native
path: full-native
- name: Download hosted full SD image
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: sdfull-hosted
path: full-hosted
- name: Download native migration image
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: migimage-native
path: mig-native
- name: Download hosted migration image
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: migimage-hosted
path: mig-hosted
- name: Compute tag
run: |
TAG="v${{ inputs.version }}"
[[ "${{ inputs.type }}" == "beta" ]] && TAG="${TAG}-beta"
echo "TAG=$TAG" >> "$GITHUB_ENV"
- name: Assemble image + migration tarball + checksums
env:
NEW_MIGRATION: ${{ inputs.new_migration_tarball }}
run: |
set -euo pipefail
# Only some dirs may exist (downloads are continue-on-error); create
# all so find can't fail under pipefail. Prefer the hosted builder's
# artifacts (listed first); fall back to native (Pi5).
mkdir -p img-native img-hosted full-native full-hosted mig-native mig-hosted
IMAGE_SRC=$(find img-hosted img-native -type f -name '*.img.zst' -print -quit)
FULL_SRC=$(find full-hosted full-native -type f -name '*.img.zst' -print -quit)
MIG_SRC=$(find mig-hosted mig-native -type f -name '*.tar.zst' -print -quit)
if [ -z "$IMAGE_SRC" ]; then
echo "Missing artifact: sd=[$IMAGE_SRC]" >&2
exit 1
fi
if [ "$NEW_MIGRATION" = true ] && [ -z "$MIG_SRC" ]; then
echo "Missing artifact: migration=[$MIG_SRC]" >&2
exit 1
fi
mkdir -p release
cp "$IMAGE_SRC" "release/pifinder-${TAG}.img.zst"
# The full image is optional (flake.nix catalogImagesSrc).
if [ -n "$FULL_SRC" ]; then
cp "$FULL_SRC" "release/pifinder-full-${TAG}.img.zst"
fi
if [ "$NEW_MIGRATION" = true ]; then
# The migration tarball is built by Nix from the minimal migration
# system's closure (ADR 0039); publish it as is.
cp "$MIG_SRC" "release/pifinder-migration-${TAG}.tar.zst"
# Size guard. The btrfs migration reads the tarball from the card,
# not from RAM, but the download still happens over the user's
# WiFi, and the budget keeps the minimal system minimal.
MIG_MB=$(( $(stat -c%s "release/pifinder-migration-${TAG}.tar.zst") / 1048576 ))
BUDGET_MB=800
echo "migration tarball: ${MIG_MB}MB (budget ${BUDGET_MB}MB)"
if [ "${MIG_MB}" -gt "${BUDGET_MB}" ]; then
echo "::error::migration tarball ${MIG_MB}MB exceeds ${BUDGET_MB}MB budget — shrink the migration closure (check linux-firmware and friends)"
exit 1
fi
fi
# Checksum sidecars. The in-app upgrade (PiFinder.sys_utils) fetches
# <artifact-url>.sha256 and refuses to flash without a matching digest;
# it reads the first whitespace-delimited token, so sha256sum output is
# the expected format.
( cd release
shopt -s nullglob
for f in pifinder-*.img.zst pifinder-migration-*.tar.zst; do
sha256sum "$f" > "$f.sha256"
done )
- name: Tag release source commit
# Only when building this repo. A fork source_repository has a different
# `origin` we can't push to with GITHUB_TOKEN; the GitHub Release below
# still publishes the artifacts to this repo.
if: (inputs.source_repository || github.repository) == github.repository
env:
SOURCE_SHA: ${{ needs.build-hosted.outputs.source_sha || needs.build-native.outputs.source_sha }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# The base checkout is at the workflow's ref, not the built source —
# fetch and tag the commit the build job actually checked out.
git fetch --depth=1 origin "$SOURCE_SHA"
git tag "$TAG" "$SOURCE_SHA"
git push origin "$TAG"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG }}
name: PiFinder ${{ env.TAG }}
body: ${{ inputs.notes }}
prerelease: ${{ inputs.type == 'beta' }}
files: |
release/pifinder-*.zst
release/pifinder-*.sha256
- name: Update generated manifest branch
# Records this release in the metadata-only nixos-manifest branch so
# devices discover it (upgrade uses store_path; migration uses the tarball
# URL). Pushes to this repo's origin via the base checkout above, so it
# runs for fork-source builds too. source_sha is the built source commit;
# the tarball URL points at this repo's release assets.
env:
STORE_PATH: ${{ needs.build-hosted.outputs.store_path || needs.build-native.outputs.store_path }}
SOURCE_SHA: ${{ needs.build-hosted.outputs.source_sha || needs.build-native.outputs.source_sha }}
RELEASE_NOTES: ${{ inputs.notes }}
NEW_MIGRATION: ${{ inputs.new_migration_tarball }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# A new tarball is an asset of this release. Without one, the entry
# names the tarball of the newest release that has one.
if [ "$NEW_MIGRATION" = true ]; then
MIGRATION_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/pifinder-migration-${TAG}.tar.zst"
MIGRATION_ARGS=(--migration-url "$MIGRATION_URL" --migration-sha256-url "${MIGRATION_URL}.sha256")
else
MIGRATION_ARGS=(--migration-from-manifest)
fi
bash .github/scripts/publish_manifest.sh \
"chore: update release manifest [skip ci]" \
python3 .github/scripts/update_manifest.py release \
--manifest @MANIFEST@ \
--repository "${{ inputs.source_repository || github.repository }}" \
--sha "$SOURCE_SHA" \
--tag "$TAG" \
--version "${{ inputs.version }}" \
--release-type "${{ inputs.type }}" \
--store-path "$STORE_PATH" \
"${MIGRATION_ARGS[@]}" \
--title "PiFinder $TAG" \
--notes "$RELEASE_NOTES"