-
Notifications
You must be signed in to change notification settings - Fork 0
439 lines (405 loc) · 20.6 KB
/
Copy pathrelease.yml
File metadata and controls
439 lines (405 loc) · 20.6 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
# Builds the packages for a tag and attaches them to its release.
#
# Separate from CI because it does something CI must not: it publishes. CI proves a change works; this hands
# somebody a file they will install as root, which is a different act and deserves its own workflow with its own
# permissions.
name: Release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: The tag to build and attach to
required: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings
BPF_LINKER_VERSION: 0.11.1
jobs:
# The compiling, and nothing else. It was in the same job as the packaging until a runner was lost
# forty-seven minutes into an arm64 build — no failed step, no log, just a job that stopped — and taking the
# packaging and all the container checks down with it. They had already passed on the other architecture and
# had nothing to do with the loss.
#
# Separated, a lost runner costs the thing that was expensive and can be re-run on its own, which is what
# `gh run rerun --failed` is for.
binaries:
strategy:
fail-fast: false
matrix:
include:
# Pinned runners rather than `-latest`, for the same reason CI pins: a toolchain change arriving on
# somebody else's schedule is what this project cannot absorb quietly.
- runner: ubuntu-24.04
architecture: amd64
target: x86_64-unknown-linux-musl
daemon: x86_64-unknown-linux-musl
# The linker's own published binary, with its hash. It turns our code into the bytecode the
# kernel's verifier judges, so a substitution here is a substitution in what the kernel runs.
linker_sha256: e058a6aecc9e65fa4c977b298a8e4b738424d7629769fd352eed409fb57e16e8
- runner: ubuntu-24.04-arm
architecture: arm64
target: aarch64-unknown-linux-musl
daemon: aarch64-unknown-linux-musl
linker_sha256: 341ec1c595496877cae2b073544c2226d78a922739632b5732dbaa48507f1380
runs-on: ${{ matrix.runner }}
# A lost runner should cost a quarter of an hour of waiting rather than three quarters. The arm64 build
# takes about forty minutes with no cache, so this is generous and still finite.
timeout-minutes: 75
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ inputs.tag || github.ref }}
- name: Install the interface's libraries
run: |
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y -qq libgtk-4-dev libadwaita-1-dev
- name: Install the toolchain
run: |
set -euo pipefail
rustup toolchain install stable --profile minimal
rustup default stable
rustup toolchain install nightly --profile minimal --component rust-src
# The published binary for this architecture, with its hash — the same way CI does it. `cargo install`
# was tried first and does not work: it builds against a matching LLVM, which is not on the runner, and
# the failure is `could not find llvm-config`. Installing an LLVM to build a linker that is already
# published is work for nothing.
- name: Install bpf-linker
run: |
set -euo pipefail
url="https://github.com/aya-rs/bpf-linker/releases/download/v${BPF_LINKER_VERSION}/bpf-linker-${{ matrix.target }}.tar.zst"
curl -fsSL -o /tmp/bpf-linker.tar.zst "$url"
echo "${{ matrix.linker_sha256 }} /tmp/bpf-linker.tar.zst" | sha256sum --check --strict
mkdir -p "$HOME/.local/bin"
tar -xpf /tmp/bpf-linker.tar.zst -C "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/bpf-linker" --version
# The daemon statically against musl and the window dynamically against the system's GTK. The daemon
# is the one people download onto a machine that is not this one, and a static binary is the difference
# between "any kernel of 4.18 or later" and "any distribution whose glibc is at least as new as the one
# that built it".
- name: Build
run: |
set -euo pipefail
sudo apt-get install -y -qq musl-tools
rustup target add ${{ matrix.daemon }}
cargo build --release --target ${{ matrix.daemon }} --package flowlight-daemon
cargo build --release --package flowlight-gui
static=target/${{ matrix.daemon }}/release/flowlightd
if ldd "$static" 2>&1 | grep -q '=>'; then
echo "the daemon is dynamically linked after all:"; ldd "$static"; exit 1
fi
mkdir -p target/packaged
install -m 755 "$static" target/packaged/flowlightd
install -m 755 target/release/flowlight target/packaged/flowlight
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: binaries-${{ matrix.architecture }}
path: target/packaged/*
if-no-files-found: error
# The packaging, which needs no compiler: two binaries in, six files out, each installed where it is for.
# Minutes rather than most of an hour, and re-runnable without rebuilding anything.
packages:
needs: binaries
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
architecture: amd64
- runner: ubuntu-24.04-arm
architecture: arm64
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ inputs.tag || github.ref }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: binaries-${{ matrix.architecture }}
path: target/packaged
- name: Make them executable again
run: |
set -euo pipefail
# An artifact does not carry the executable bit, and a package built from a file that is not
# executable installs a file that is not executable.
chmod 755 target/packaged/flowlightd target/packaged/flowlight
ls -l target/packaged
# Before installing anything, because installing the window's package pulls GTK from the archive and
# the index a runner image ships with is as old as the image. v0.5.9's amd64 half failed exactly there:
# `404 Not Found` for a `libgstreamer-plugins-base` the index named and the pool no longer had.
#
# This step existed in the job this one was split out of — `apt-get update` was the first line of
# installing the interface's libraries, which went to the build job with the compiling. Splitting a job
# takes the things it quietly depended on with it.
- name: Refresh the package index
run: sudo apt-get update -qq
- name: Build the packages
id: build
run: |
set -euo pipefail
version=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')
./packaging/deb/build.sh "$version" "${{ matrix.architecture }}" target/packaged
ls -l target/deb
echo "version=$version" >>"$GITHUB_OUTPUT"
# Installed on the machine that built it, because a package that unpacks is not a package that installs.
- name: Install what was built
run: |
set -euo pipefail
version=${{ steps.build.outputs.version }}
sudo apt-get install -y -qq "./target/deb/flowlight_${version}_${{ matrix.architecture }}.deb"
test -x /usr/sbin/flowlightd
/usr/sbin/flowlightd --version
# Shipped disabled, and that is the whole point of shipping it disabled.
if systemctl is-enabled flowlightd >/dev/null 2>&1; then
echo "the unit is enabled; it must not be"; exit 1
fi
if systemctl is-active flowlightd >/dev/null 2>&1; then
echo "the daemon is running; it must not be"; exit 1
fi
sudo apt-get install -y -qq "./target/deb/flowlight-gui_${version}_${{ matrix.architecture }}.deb"
test -x /usr/bin/flowlight
echo "both packages install, and nothing started itself"
# The daemon's `.rpm`, built in a container of a distribution that has an `rpmbuild`. Only the daemon:
# the window is linked against the system's GTK and the system's glibc, so one built here does not run
# on a distribution whose glibc is older, and shipping it anyway would be shipping a package that
# installs and does not start.
- name: Build the rpm
run: |
set -euo pipefail
version=${{ steps.build.outputs.version }}
case "${{ matrix.architecture }}" in
amd64) rpm_architecture=x86_64 ;;
arm64) rpm_architecture=aarch64 ;;
*) echo "no rpm architecture for ${{ matrix.architecture }}"; exit 1 ;;
esac
docker run --rm -v "$PWD:/work" -w /work "fedora:41" bash -c "
set -euo pipefail
dnf install -y -q rpm-build >/dev/null
./packaging/rpm/build.sh '$version' '$rpm_architecture' target/packaged target/rpm"
sudo chown -R "$(id -u):$(id -g)" target/rpm
ls -l target/rpm
- name: Check the rpm installs where it is for
run: |
set -euo pipefail
package=$(ls target/rpm/flowlight-*.rpm | head -1)
# On both, because one `.rpm` that installs on Fedora is not one that installs on openSUSE until
# it has. Only where the runner's architecture matches the package's.
for image in fedora:41 opensuse/leap:15.6; do
docker run --rm -v "$PWD:/work" -w /work "$image" ./scripts/install-an-rpm.sh "$package"
done
# The tarball, for a distribution nobody has packaged — and the thing the Arch package is built from.
- name: Build the tarball
run: |
set -euo pipefail
version=${{ steps.build.outputs.version }}
case "${{ matrix.architecture }}" in
amd64) tarball_architecture=x86_64 ;;
arm64) tarball_architecture=aarch64 ;;
*) echo "no tarball architecture for ${{ matrix.architecture }}"; exit 1 ;;
esac
./packaging/tarball/build.sh "$version" "$tarball_architecture" target/packaged target/tarball
- name: Check the tarball installs where there is no package manager to do it
run: |
set -euo pipefail
tarball=$(ls target/tarball/*.tar.gz | head -1)
# Alpine: no systemd, no glibc, and nothing of ours packaged for it — which makes it the honest test
# of "needs a kernel of 4.18 and nothing else".
docker run --rm -v "$PWD:/work" -w /work alpine:3.20 \
sh -c "apk add --quiet bash; exec bash ./scripts/install-a-tarball.sh '$tarball'"
# Into one flat directory before uploading. Two globs under different parents make an artifact that
# keeps those parents, and the job that attaches them would then be looking in the wrong place — which
# is a thing to notice here rather than on a release page with half its files.
- name: Gather what will be attached
run: |
set -euo pipefail
mkdir -p target/assets
install -m 644 target/deb/*.deb target/rpm/*.rpm target/tarball/*.tar.gz target/assets/
# The checksum beside each tarball. It is written by the tarball build and was not being attached,
# which left the only published hash inside a PKGBUILD — fine for Arch and no use to anybody
# checking a download by hand.
install -m 644 target/tarball/*.sha256 target/assets/
ls -l target/assets
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: packages-${{ matrix.architecture }}
path: target/assets/*
if-no-files-found: error
# The window, compiled inside the distributions it is for. It links against the system's GTK, libadwaita and
# glibc, so unlike the daemon it cannot be built once and carried — and unlike the daemon, its package says
# which distribution built it, because that is part of what the file is.
#
# x86_64 only. An arm64 desktop exists, and the way to have one is to build from source until somebody asks
# for the package; doubling this job to guess at demand would double the slowest thing in the release.
window:
needs: binaries
runs-on: ubuntu-24.04
# It compiles the window inside a container, with no cache, twice over. The same reason the build has one.
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
image: [fedora:41, opensuse/tumbleweed]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: binaries-amd64
path: built
- name: Build and check it inside ${{ matrix.image }}
env:
TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail
chmod +x built/flowlightd
version=${TAG#v}
docker run --rm -v "$PWD:/work" -w /work -e "VERSION=$version" "${{ matrix.image }}" bash -c '
set -euo pipefail
if command -v dnf >/dev/null; then dnf install -y -q rpm-build >/dev/null; else
zypper --non-interactive --quiet install rpm-build >/dev/null; fi
./packaging/rpm/build.sh "$VERSION" x86_64 built target/rpm
exec ./scripts/the-window-inside.sh "$VERSION" x86_64'
sudo chown -R "$(id -u):$(id -g)" target/rpm-gui
ls -l target/rpm-gui
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: window-${{ strategy.job-index }}
path: target/rpm-gui/*.rpm
if-no-files-found: error
# Runs even when something above it failed, which is the whole point of it.
#
# It used to be skipped, and the result was worse than a red check: v0.5.7 was published, its amd64 packages
# were built and thrown away, the release page had nothing on it at all, and the `apt` workflow — which waits
# for a successful Release — never ran. A release that exists and offers nothing is a release somebody
# arrives at and leaves.
#
# So: upload whatever was built, say on the page itself what is missing, and then fail. The check stays red,
# which is correct, and the people who come to the page get the half that works with a sentence explaining
# the half that does not.
attach:
needs: [packages, window]
if: ${{ !cancelled() }}
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
# The one job that writes anything, and it writes only to the release for this tag.
contents: write
steps:
# `continue-on-error`, because a job that failed uploaded no artifact and a download that finds nothing
# must not be the reason this stops.
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
continue-on-error: true
with:
pattern: packages-*
merge-multiple: true
path: packages
# The windows, which are named for the distribution that built each one and therefore do not collide.
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
continue-on-error: true
with:
pattern: window-*
merge-multiple: true
path: packages
# Written here rather than in the per-architecture job, because a PKGBUILD names both tarballs and
# their checksums, and this is the first place both exist.
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
path: source
- name: Write the PKGBUILDs for this release
env:
TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail
version=${TAG#v}
# Only when both tarballs are here: a PKGBUILD names both architectures, and one written from half
# of them would be a file that fails for whoever has the other machine.
first=packages/flowlight-$version-x86_64-linux.tar.gz
second=packages/flowlight-$version-aarch64-linux.tar.gz
if [ -f "$first" ] && [ -f "$second" ]; then
./source/packaging/arch/render.sh release "$version" "$first" "$second" >packages/PKGBUILD
cat packages/PKGBUILD
# And the window's, which builds from source. The source is the archive GitHub makes for the tag,
# downloaded here so that what is in the PKGBUILD is the hash of the file somebody will actually
# fetch rather than one computed from a tree that happens to be checked out.
archive="https://github.com/$GITHUB_REPOSITORY/archive/refs/tags/$TAG.tar.gz"
curl -fsSL -o /tmp/source.tar.gz "$archive"
./source/packaging/arch/render.sh window-release "$version" /tmp/source.tar.gz \
"${GITHUB_REPOSITORY#*/}-$version" "$archive" >packages/PKGBUILD-gui
cat packages/PKGBUILD-gui
else
echo "one of the tarballs is missing, so neither PKGBUILD is written"
fi
- name: Attach what was built, and say what was not
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail
shopt -s nullglob
version=${TAG#v}
ls -l packages || true
# Everything a complete release has, by name. A list rather than a count, so that what is missing
# can be said rather than only noticed.
expected=(
"flowlight_${version}_amd64.deb"
"flowlight_${version}_arm64.deb"
"flowlight-gui_${version}_amd64.deb"
"flowlight-gui_${version}_arm64.deb"
"flowlight-${version}-1.x86_64.rpm"
"flowlight-${version}-1.aarch64.rpm"
"flowlight-${version}-x86_64-linux.tar.gz"
"flowlight-${version}-aarch64-linux.tar.gz"
"PKGBUILD"
"PKGBUILD-gui"
)
missing=()
for file in "${expected[@]}"; do
[ -f "packages/$file" ] || missing+=("$file")
done
# The window's packages are named for the distribution that built them, so they are counted rather
# than named: two of them, one per distribution.
windows=(packages/flowlight-gui-"$version"-1.*.rpm)
if [ "${#windows[@]}" -lt 2 ]; then
missing+=("the window's rpm for one or both of Fedora and openSUSE")
fi
found=(packages/*.deb packages/*.rpm packages/*.tar.gz packages/*.sha256 packages/PKGBUILD*)
if [ "${#found[@]}" -gt 0 ]; then
# `--clobber` so that re-running this for a tag replaces what is there rather than failing, which
# is what somebody wants when a release had to be built twice.
gh release upload "$TAG" "${found[@]}" --clobber --repo "$GITHUB_REPOSITORY"
echo "attached ${#found[@]} file(s) to $TAG"
else
echo "nothing was built, so nothing was attached"
fi
# The release's own notes say what is not there, under a marker so that a later run can take the
# sentence away again. A page that quietly offers half of a release is the failure this is for.
marker='<!-- flowlight-incomplete -->'
gh release view "$TAG" --json body -q .body --repo "$GITHUB_REPOSITORY" \
| awk -v marker="$marker" '$0 == marker { exit } { print }' >/tmp/notes
if [ "${#missing[@]}" -gt 0 ]; then
{
echo "$marker"
echo
echo "---"
echo
echo "**This release is incomplete.** These were not built:"
echo
for file in "${missing[@]}"; do echo "- \`$file\`"; done
echo
echo "Nothing is wrong with what is here; what is missing was lost to a failed or lost build job."
echo "Re-running the Release workflow for this tag replaces the files and takes this notice away."
} >>/tmp/notes
fi
gh release edit "$TAG" --notes-file /tmp/notes --repo "$GITHUB_REPOSITORY" >/dev/null
if [ "${#missing[@]}" -gt 0 ]; then
printf 'missing: %s\n' "${missing[@]}"
echo "### This release is incomplete" >>"$GITHUB_STEP_SUMMARY"
printf -- '- `%s`\n' "${missing[@]}" >>"$GITHUB_STEP_SUMMARY"
exit 1
fi
echo "every file a release should have is attached to $TAG"