-
Notifications
You must be signed in to change notification settings - Fork 0
427 lines (387 loc) · 22.6 KB
/
Copy pathci.yml
File metadata and controls
427 lines (387 loc) · 22.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
# Checks every push, on Linux, because that is the only place this software runs.
#
# Most of it is being written on a Mac, where the eBPF half cannot be compiled and a probe cannot be loaded.
# That makes CI the verifier rather than a second opinion: if a change is not checked here, it is not checked.
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
# A warning that nobody has to act on is a warning everybody learns to scroll past.
RUSTFLAGS: -D warnings
# Pinned, with its hash. The linker turns our code into the bytecode the kernel's verifier judges, so a new
# one arriving unannounced would change what the verifier sees without anything in this repository changing.
BPF_LINKER_VERSION: "0.11.1"
BPF_LINKER_SHA256: "e058a6aecc9e65fa4c977b298a8e4b738424d7629769fd352eed409fb57e16e8"
jobs:
check:
# Pinned rather than `ubuntu-latest`: that label moves without asking, and a kernel or toolchain change
# arriving on somebody else's schedule is exactly what this project cannot absorb quietly.
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# The native interface is GTK4. Installed before anything is compiled, because the bindings need the
# headers at build time and the failure without them is a wall of pkg-config.
- 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 --component clippy,rustfmt
rustup default stable
# The BPF target has no prebuilt `core`, so it is compiled from source, which is nightly-only.
rustup toolchain install nightly --profile minimal --component rust-src
rustc --version && cargo --version
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
# The prebuilt binary rather than `cargo install`: building it from source means building against a
# matching LLVM, which means installing one, which is ten minutes of CI for a tool that does not change.
- name: Install bpf-linker
run: |
set -euo pipefail
url="https://github.com/aya-rs/bpf-linker/releases/download/v${BPF_LINKER_VERSION}/bpf-linker-x86_64-unknown-linux-musl.tar.zst"
curl -fsSL -o /tmp/bpf-linker.tar.zst "$url"
echo "${BPF_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
- name: Formatting
run: cargo fmt --all --check
# Every row in the window carries text that came off the network — a host, a path, a process name — and
# libadwaita reads a row's title as Pango markup unless it is told otherwise *before* the text is set.
# A single `&` in a query string then renders as nothing at all, and a `<span>` in one would render as
# markup. Found by opening the window on a real desktop and seeing a request row with no request in it.
#
# Two checks, because the first version of this was one check and it was the wrong one. Counting
# declarations said yes while the window showed nothing; only running the thing says anything.
- name: Every row in the window is built the one way that works
run: |
set -euo pipefail
# The structural half: no row is built by a builder, because a builder cannot set the property
# before the text whatever order it lists them in.
if grep -n 'adw::ActionRow::builder()' crates/flowlight-gui/src/main.rs; then
echo "a row is built with a builder, which sets its title before it is told the title is text"
exit 1
fi
echo "every row goes through the helper"
# The desktop draws this file through gdk-pixbuf, which recognises a format by sniffing the first
# bytes of it: the SVG loader looks for `<svg` near the start. A comment written above the element
# pushed `<svg` 501 bytes in, and every consumer answered "couldn't recognize the image file format"
# while the file stayed perfectly valid SVG — so the dock, the app grid and the window all drew
# nothing, and nothing anywhere said why.
#
# It has to be this tool. `rsvg-convert` parses the file directly, accepts the broken one and exits 0,
# so a check built on it proves nothing. And the thumbnailer warns and *also* exits 0, so the check
# has to look at what came out rather than at the exit status. Both were tried against the broken
# file before this was written.
- name: The icon is an icon as far as the desktop is concerned
run: |
set -euo pipefail
sudo apt-get install -y -qq libgdk-pixbuf2.0-bin librsvg2-common
icon=packaging/desktop/com.xinbetween.Flowlight.svg
at=$(grep -abo '<svg' "$icon" | head -1 | cut -d: -f1)
echo "<svg starts at byte $at"
rm -f /tmp/icon.png
gdk-pixbuf-thumbnailer -s 64 "$icon" /tmp/icon.png || true
if [ ! -s /tmp/icon.png ]; then
echo "gdk-pixbuf cannot draw this file, so no desktop will either."
echo "<svg is $at bytes in; the loader sniffs the start of the file. Keep comments inside the element."
exit 1
fi
echo "the icon draws: $(stat -c %s /tmp/icon.png) bytes of PNG"
# Cairo draws onto a buffer and the buffer is counted — no display, no window, no screenshot. A chart
# that draws nothing looks exactly like a quiet hour in a running window, which is the one way this
# could ship broken and nobody notice. Proven against a `render` that returns early before it was
# trusted: 0 of 180000 pixels, exit 1.
- name: The chart draws something
run: |
set -euo pipefail
cargo run -q -p flowlight-gui --example chart
- name: A row shows the text it was given
run: |
set -euo pipefail
# The behavioural half, under a display, catching what GTK logs rather than what a property says.
# Proven to fail against the construction it replaced before it was trusted.
sudo apt-get install -y -qq xvfb
xvfb-run -a cargo run -q -p flowlight-gui --example markup
# `flowlight-ebpf` is excluded from the host-target commands and checked by being compiled for the BPF
# target as part of the daemon's build script. Linting it here would mean linting it for x86_64, which
# is not a target it has a meaning on.
- name: Clippy
run: cargo clippy --workspace --exclude flowlight-ebpf --all-targets --all-features
- name: Tests
run: cargo test --workspace --exclude flowlight-ebpf --all-features
# The kernel this runs on, recorded in the log. When a probe later fails to attach, the first question is
# always which kernel it was, and a run from six months ago cannot be asked afterwards.
- name: Kernel and BPF availability
run: |
uname -srm
echo "unprivileged_bpf_disabled: $(cat /proc/sys/kernel/unprivileged_bpf_disabled 2>/dev/null || echo 'unreadable')"
test -d /sys/fs/bpf && echo "bpffs: mounted" || echo "bpffs: absent"
echo "--- sock/inet_sock_set_state layout on this kernel:"
sudo cat /sys/kernel/tracing/events/sock/inet_sock_set_state/format 2>/dev/null \
|| sudo cat /sys/kernel/debug/tracing/events/sock/inet_sock_set_state/format
- name: Build the daemon and the interface
run: cargo build --release --package flowlight-daemon --package flowlight-gui
# And the daemon again, statically linked, which is the one that ships. A binary linked against this
# runner's glibc is a binary that does not start on Debian 12 or RHEL 9, and the package it went into
# said `Depends: libc6` with no version — so it installed there and then failed.
- name: Build the daemon with no libc requirement
run: |
set -euo pipefail
sudo apt-get install -y -qq musl-tools
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl --package flowlight-daemon
static=target/x86_64-unknown-linux-musl/release/flowlightd
file "$static"
# The assertion, rather than the description: `ldd` naming a shared object means something would
# have to be installed for this to run, which is the whole thing being avoided.
if ldd "$static" 2>&1 | grep -q '=>'; then
echo "the daemon is dynamically linked after all:"; ldd "$static"; exit 1
fi
"$static" --version
# The part that cannot be faked. Loads the programs into a real kernel, attaches them, makes real
# requests, and insists on finding them: the connection with the right process and port, and the
# request itself in the clear.
# Built and then installed on the machine that built it, which is the only way to find out whether a
# package works: a `.deb` that unpacks is not a `.deb` that installs.
- name: Build the packages and install them
run: |
set -euo pipefail
# The static daemon and the dynamic interface in one directory, because that is what a package
# carries: the window needs GTK at runtime and the daemon needs nothing at all.
mkdir -p target/packaged
install -m 755 target/x86_64-unknown-linux-musl/release/flowlightd target/packaged/flowlightd
install -m 755 target/release/flowlight target/packaged/flowlight
./packaging/deb/build.sh 0.0.0-ci amd64 target/packaged
ls -l target/deb
# `Depends` on the daemon at all would mean something has to be installed for it to run.
if ar p target/deb/flowlight_0.0.0-ci_amd64.deb control.tar.gz | tar -xzO ./control \
| grep -q '^Depends:'; then
echo "the daemon package declares a dependency it does not have"; exit 1
fi
sudo apt-get install -y -qq ./target/deb/flowlight_0.0.0-ci_amd64.deb
# Installed and not running, which is the whole point of shipping the unit disabled.
test -x /usr/sbin/flowlightd
systemctl is-enabled flowlightd && { echo "the unit is enabled; it must not be"; exit 1; } || true
systemctl is-active flowlightd && { echo "the daemon is running; it must not be"; exit 1; } || true
/usr/sbin/flowlightd --version
# And then started, which is how almost everybody will run it and is the one path nothing here had
# ever taken. The unit sets `ProtectSystem=full`, so a default path under `/usr` is read-only to the
# service — which is exactly what happened: interception could never start from the unit and said so
# in a line of the journal nobody reads. Installing the package and asserting it had *not* started
# was not the same as finding out whether it could.
sudo systemctl start flowlightd
sleep 8
systemctl is-active flowlightd || { echo "the unit will not run"; exit 1; }
sudo journalctl -u flowlightd --no-pager -o cat | tee /tmp/unit.log
grep -q "watching sock/inet_sock_set_state" /tmp/unit.log \
|| { echo "the unit started and is not watching"; exit 1; }
# The sandboxing and the daemon's defaults have to agree. Any read-only filesystem error means they
# do not, whichever path it was.
if grep -q "Read-only file system" /tmp/unit.log; then
echo "the unit cannot write somewhere the daemon expects to"; exit 1
fi
test -d /etc/flowlight || { echo "the certificate directory the unit grants is not there"; exit 1; }
sudo systemctl stop flowlightd
systemctl is-active flowlightd && { echo "it will not stop"; exit 1; } || true
# And it comes off again without leaving the history behind, unless asked.
sudo apt-get remove -y -qq flowlight
test ! -x /usr/sbin/flowlightd
echo "the package installs, does not start itself, runs when it is told to, and comes off again"
# The archive that apt reads, built from the packages just built. Signing needs a key that exists in one
# place and installing from it is the `apt` workflow's job; what is checked here is the part that needs
# nothing — that the index names every package with the hash it actually has.
- name: Build the apt archive
run: |
set -euo pipefail
./packaging/apt/build.sh target/deb /tmp/archive 0.0.0-ci
test -f /tmp/archive/dists/stable/Release
test -f /tmp/archive/index.html
grep -q '^Package: flowlight$' /tmp/archive/dists/stable/main/binary-amd64/Packages
# Every Filename resolves and every SHA256 is the file's own, checked rather than assumed: an index
# that names a hash nothing has is an archive that fails on somebody else's machine.
( cd /tmp/archive && awk '/^Filename: /{f=$2} /^SHA256: /{print $2" "f}' \
dists/stable/main/binary-amd64/Packages | sha256sum --check --strict )
( cd /tmp/archive/dists/stable && awk '/^SHA256:/{on=1;next} /^[A-Za-z]/{on=0} on{print $1" "$3}' \
Release | sha256sum --check --strict )
echo "the archive's index and Release agree with what is in the pool"
# Handed to the job that runs it inside other distributions, rather than built there: four containers
# each compiling the workspace would be forty minutes of CI to prove something about one binary.
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: static-daemon
path: target/x86_64-unknown-linux-musl/release/flowlightd
if-no-files-found: error
- name: Attribute and read a real request
env:
# `gh` is written in Go, so its TLS is linked into its own binary and Flowlight cannot read it.
# That is the case Coverage exists to name, and the smoke test asserts it does.
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# A client that uses GnuTLS by definition, so the GnuTLS probes are exercised rather than assumed.
# The runner's own curl and wget are both OpenSSL, which would test the same path three times.
sudo apt-get install -y -qq gnutls-bin
# The binary that ships, rather than the one that happens to be easiest to build: if the static
# daemon cannot load a probe, nobody wants to find that out from a release.
./scripts/smoke.sh target/x86_64-unknown-linux-musl/release/flowlightd
# The claim this job exists to stop anybody making on a hunch: that because the binary has no libc
# requirement and the library search reads `/proc`, it therefore works on Fedora. A container has no kernel
# of its own — these run against the runner's — so what is being tested is each distribution's *userland*:
# whether the binary runs on it, whether its TLS library is found where that distribution keeps it, and
# whether a request made by its own `curl` is read.
distributions:
needs: check
runs-on: ubuntu-24.04
strategy:
# Every distribution is reported, rather than the run stopping at the first one that fails: "it works
# everywhere except Arch" is the useful sentence, and fail-fast turns it into "it failed".
fail-fast: false
matrix:
include:
- image: debian:12
install: apt-get update -qq && apt-get install -y -qq curl jq ca-certificates
- image: fedora:41
install: dnf install -y -q curl jq
- image: archlinux:base
install: pacman -Sy --noconfirm --quiet curl jq
- image: opensuse/leap:15.6
install: zypper --non-interactive --quiet install curl jq
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: static-daemon
path: built
- name: Run it inside ${{ matrix.image }}
run: |
set -euo pipefail
chmod +x built/flowlightd
# `--privileged` because loading a BPF program needs it; the tracefs bind mount because a container
# gets a fresh `/sys` with tracefs not mounted, and the daemon reads one file out of it to learn the
# tracepoint's layout on this kernel.
docker run --rm --privileged --ulimit memlock=-1 \
-v /sys/kernel/tracing:/sys/kernel/tracing:ro \
-v "$PWD:/work" -w /work \
"${{ matrix.image }}" \
bash -c '${{ matrix.install }} >/dev/null && exec ./scripts/inside-a-distribution.sh built/flowlightd'
# The `.rpm`, built where there is an `rpmbuild` and installed where it is meant to be installed. Both
# halves matter: a package that builds is not a package that installs, and one that installs on Fedora is
# not one that installs on openSUSE until it has.
rpm:
needs: check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: static-daemon
path: built
- name: Build it in Fedora
run: |
set -euo pipefail
chmod +x built/flowlightd
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 0.0.0-ci x86_64 built target/rpm'
ls -l target/rpm
- name: Install it in Fedora and in openSUSE
run: |
set -euo pipefail
package=$(ls target/rpm/flowlight-*.x86_64.rpm | head -1)
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
# Arch, and the tarball for everything nobody has packaged. Both built and then installed, like the `.deb`
# and the `.rpm`: what a package does when it is installed is the half that cannot be read off the file.
arch-and-tarball:
needs: check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: static-daemon
path: built
- name: Build the tarball
run: |
set -euo pipefail
chmod +x built/flowlightd
# `0.0.0.ci` rather than `0.0.0-ci`: an Arch `pkgver` may not contain a hyphen, and the name of the
# directory inside the tarball has to match what the PKGBUILD looks for.
./packaging/tarball/build.sh 0.0.0.ci x86_64 built target/tarball
# In the two distributions that have nothing else: Arch, which gets a package of its own below, and
# Alpine, which has no systemd and no glibc and is therefore the honest test of "needs a kernel and
# nothing else".
- name: Install it from the tarball
run: |
set -euo pipefail
tarball=$(ls target/tarball/*.tar.gz | head -1)
for image in archlinux:base alpine:3.20; do
docker run --rm -v "$PWD:/work" -w /work "$image" \
sh -c "command -v bash >/dev/null || apk add --quiet bash; exec bash ./scripts/install-a-tarball.sh '$tarball'"
done
- name: Build and install the Arch package
run: |
set -euo pipefail
tarball=$(ls target/tarball/*.tar.gz | head -1)
docker run --rm -v "$PWD:/work" -w /work archlinux:base \
./scripts/install-on-arch.sh "$tarball" 0.0.0.ci
# And the window, which on Arch is built from source — the only shape that works for something linking
# against a rolling distribution's own GTK and glibc. The source is this working tree rather than a
# release, so what is built is the code in front of you.
- name: Build the window on Arch, from source
run: |
set -euo pipefail
tarball=$(ls target/tarball/*.tar.gz | head -1)
mkdir -p target/source
git archive --format=tar.gz --prefix=flowlight-linux-0.0.0.ci/ \
-o target/source/flowlight-linux-0.0.0.ci.tar.gz HEAD
docker run --rm -v "$PWD:/work" -w /work archlinux:base \
./scripts/the-window-on-arch.sh "$tarball" \
target/source/flowlight-linux-0.0.0.ci.tar.gz flowlight-linux-0.0.0.ci 0.0.0.ci
# The window, compiled inside the distributions it is for. Slow — a Rust build per container, with no cache —
# and worth it: this is the one package that cannot be built anywhere and carried, because it links against
# the system's GTK, libadwaita and glibc.
#
# `fail-fast: false`, because "the window builds on Fedora and not on openSUSE Leap" is the useful sentence,
# and it is also a thing worth knowing rather than a red check to retry.
window:
needs: check
runs-on: ubuntu-24.04
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: static-daemon
path: built
- name: Build the window inside ${{ matrix.image }}
run: |
set -euo pipefail
chmod +x built/flowlightd
docker run --rm -v "$PWD:/work" -w /work "${{ matrix.image }}" bash -c '
set -euo pipefail
# The daemon package first, because the window requires it — which is one of the things being
# checked here. It is the same static binary everywhere, so it is packaged rather than compiled.
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 0.0.0.ci x86_64 built target/rpm
exec ./scripts/the-window-inside.sh 0.0.0.ci x86_64'