Skip to content

Commit 286c1e5

Browse files
authored
Merge pull request #317 from charliek/feature/plan-023-release-smoke
ci: exercise the release deb checks on PRs, and fix what running them found
2 parents 98f0987 + 74802f5 commit 286c1e5

7 files changed

Lines changed: 614 additions & 152 deletions

File tree

.github/workflows/ci.yml

Lines changed: 126 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
fixtures: ${{ steps.filter.outputs.fixtures }}
2626
tests: ${{ steps.filter.outputs.tests }}
2727
ci: ${{ steps.filter.outputs.ci }}
28+
deb: ${{ steps.filter.outputs.deb }}
2829
steps:
2930
- uses: actions/checkout@v6
3031
with:
@@ -37,7 +38,9 @@ jobs:
3738
- 'Cargo.toml'
3839
- 'Cargo.lock'
3940
- 'rust-toolchain.toml'
40-
- 'mise.toml'
41+
# The file is a dotfile. The old 'mise.toml' pattern could
42+
# never match, so a toolchain bump triggered no Rust job.
43+
- '.mise.toml'
4144
- 'clippy.toml'
4245
- 'crates/**'
4346
- 'third_party/ghostty/**'
@@ -71,6 +74,29 @@ jobs:
7174
- 'Makefile'
7275
ci:
7376
- '.github/workflows/ci.yml'
77+
# Deliberately NOT `linux`: that filter folds in the *rustcore
78+
# anchor, so it's true on essentially every Rust PR — gating the
79+
# deb work on it would pay an nfpm download, an nfpm package, and
80+
# a Docker Hub pull on each one. This list is only the paths that
81+
# can actually change what the .deb contains or how it's checked.
82+
# `release.yml` is here because it appears in no other filter at
83+
# all: a PR that edits only the release workflow would otherwise
84+
# trigger nothing capable of validating it. `Cargo.lock` is here
85+
# because a new dependency can pull in a new shared library and
86+
# change the package's runtime closure.
87+
# Manifests, not `crates/**`: enabling a feature on an
88+
# already-locked dependency changes what the binary dlopens
89+
# without touching Cargo.lock, and the closure check is the only
90+
# thing that would notice. Source edits still don't trigger it —
91+
# that's the cost line this filter exists to hold.
92+
deb:
93+
- 'linux/**'
94+
- 'packaging/**'
95+
- 'Cargo.toml'
96+
- 'Cargo.lock'
97+
- 'crates/*/Cargo.toml'
98+
- '.github/workflows/ci.yml'
99+
- '.github/workflows/release.yml'
74100
75101
rust-lint:
76102
needs: changes
@@ -680,8 +706,10 @@ jobs:
680706
# packaging/**): this lane builds the same packaged configuration the deb
681707
# ships, so a build-deb.sh or nfpm.yaml change must be able to reach it.
682708
# Without that, a packaging-only PR triggered no lane that compiles the
683-
# linux-package feature at all.
684-
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.tests == 'true' || needs.changes.outputs.linux == 'true' || needs.changes.outputs.ci == 'true'
709+
# linux-package feature at all. `deb` additionally turns on the packaging
710+
# steps further down (build the real .deb, then smoke it and verify its
711+
# dependency closure).
712+
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.tests == 'true' || needs.changes.outputs.linux == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.deb == 'true'
685713
runs-on: ubuntu-latest
686714
timeout-minutes: 45
687715
steps:
@@ -729,21 +757,55 @@ jobs:
729757
key: cargo-iced-release-${{ runner.os }}-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }}
730758
restore-keys: cargo-iced-release-${{ runner.os }}-
731759

732-
# One build, not two: [profile.release] is `lto = "thin"` +
733-
# `codegen-units = 1`, and a second cold LTO link would blow the
734-
# job's 45-minute budget. `linux-package` is the configuration that
735-
# actually ships (it's what makes a packaged build adopt the
736-
# production `roost` bundle profile instead of the isolated
737-
# `roost-iced` one, per `default_profile_kind()` in
738-
# `crates/roost-iced/src/main.rs`) — building it here, rather than
760+
# Must precede the build, because on the `deb` path the build is done by
761+
# build-deb.sh, which ends in `nfpm pkg`. Mirrors release.yml's install
762+
# exactly (same pinned version, same source) rather than inventing a
763+
# second mechanism — this lane is ubuntu-latest, i.e. amd64, so the arch
764+
# is unconditionally x86_64.
765+
- name: Install nfpm
766+
if: needs.changes.outputs.deb == 'true'
767+
run: |
768+
set -euo pipefail
769+
ver="2.46.3"
770+
url="https://github.com/goreleaser/nfpm/releases/download/v${ver}/nfpm_${ver}_Linux_x86_64.tar.gz"
771+
curl -fsSL "${url}" -o /tmp/nfpm.tgz
772+
sudo tar -C /usr/local/bin -xzf /tmp/nfpm.tgz nfpm
773+
nfpm --version
774+
775+
# One build, not two — and now enforced rather than assumed.
776+
# [profile.release] is `lto = "thin"` + `codegen-units = 1`, so a second
777+
# cold LTO link would blow this job's 45-minute budget. build-deb.sh's
778+
# cargo invocation is byte-identical to the one here, so on the `deb`
779+
# path it subsumes this step; the two `if:`s are exact complements, so
780+
# exactly one always runs. Splitting it this way keeps ONE source of
781+
# truth for the build command instead of relying on an unenforced
782+
# coincidence between a workflow and a script.
783+
#
784+
# `linux-package` is the configuration that actually ships (it's what
785+
# makes a packaged build adopt the production `roost` bundle profile
786+
# instead of the isolated `roost-iced` one, per `default_profile_kind()`
787+
# in `crates/roost-iced/src/main.rs`) — building it here, rather than
739788
# the featureless dev config, is what makes this lane worth gating.
740-
# The e2e step right below still exercises this same binary as an
741-
# ordinary dev instance, because it pins `ROOST_BUNDLE_PROFILE`; the
742-
# profile-adoption step further down is what actually leaves that
743-
# var unset and checks where the packaged binary lands on its own.
789+
# Either path leaves target/release/{roost-iced,roostctl} exactly where
790+
# every downstream step expects them: the e2e step below exercises the
791+
# binary as an ordinary dev instance because it pins
792+
# `ROOST_BUNDLE_PROFILE`, and the profile-adoption step further down is
793+
# what leaves that var unset and checks where it lands on its own.
744794
- name: Build Iced + roostctl (release)
795+
if: needs.changes.outputs.deb != 'true'
745796
run: cargo build --release -p roost-iced -p roost-cli --features roost-iced/linux-package
746797

798+
# build-deb.sh also re-runs third_party/ghostty/build.sh. That's
799+
# idempotent on a cache hit, but it needs `zig` on PATH *before* it
800+
# reaches its cache check — satisfied by the jdx/mise-action@v4 step
801+
# above, which is why this can't move ahead of it.
802+
#
803+
# Version 0.0.0-ci: nfpm normalizes `-` to `~`, yielding `0.0.0~ci` — a
804+
# valid Debian version that can never be mistaken for a real release.
805+
- name: Build the .deb (also produces the release binaries)
806+
if: needs.changes.outputs.deb == 'true'
807+
run: ./linux/scripts/build-deb.sh 0.0.0-ci
808+
747809
- name: Run Iced release-profile E2E (Linux X11)
748810
env:
749811
ICED_BACKEND: wgpu
@@ -856,6 +918,49 @@ jobs:
856918
857919
echo "OK: packaged build adopted the production namespace ($SOCKET_PATH)"
858920
921+
# ---- Packaging checks (deb-gated) -------------------------------------
922+
# These three steps run the *same* scripts release.yml runs. Until now
923+
# their first execution was during an actual release — i.e. after the
924+
# GitHub Release had already been created, with a broken artifact
925+
# already attached to it. Running them on ordinary PRs that touch the
926+
# packaging paths is the whole point: the release path's only real
927+
# artifact checks get proven somewhere other than a real release.
928+
# They sit after the profile-adoption assertion above so the cheap,
929+
# already-proven check still reports first.
930+
931+
# Release-critical shell with no other lint anywhere in the repo.
932+
# shellcheck ships preinstalled on GitHub's ubuntu runners, so this is
933+
# seconds; `bash -n` additionally catches syntax errors in code paths
934+
# shellcheck may not flag.
935+
- name: Lint the release-path shell scripts
936+
if: needs.changes.outputs.deb == 'true'
937+
run: |
938+
set -euo pipefail
939+
shellcheck linux/scripts/*.sh
940+
for f in linux/scripts/*.sh; do
941+
bash -n "$f"
942+
done
943+
944+
- name: Smoke the packaged artifact
945+
if: needs.changes.outputs.deb == 'true'
946+
run: |
947+
set -euo pipefail
948+
deb="$(./linux/scripts/resolve-one-deb.sh out)"
949+
./linux/scripts/smoke-deb.sh "${deb}" \
950+
--work-dir "${RUNNER_TEMP}/roost-deb-smoke" \
951+
--expect-version 0.0.0~ci
952+
953+
# The smoke above extracts the .deb, so it proves the payload but not
954+
# the `Depends:` line — this runner already carries the whole graphics
955+
# stack from the build, so a missing dependency would still launch
956+
# here. The container run is what catches that.
957+
- name: Verify the dependency closure
958+
if: needs.changes.outputs.deb == 'true'
959+
run: |
960+
set -euo pipefail
961+
deb="$(./linux/scripts/resolve-one-deb.sh out)"
962+
./linux/scripts/verify-deb-closure.sh "${deb}"
963+
859964
- name: Collect Iced diagnostics
860965
if: always()
861966
shell: bash
@@ -1335,7 +1440,13 @@ jobs:
13351440
steps:
13361441
- name: Verify no required job failed
13371442
run: |
1338-
results="${{ needs.rust-lint.result }} ${{ needs.harness-unit.result }} ${{ needs.themes-parity.result }} ${{ needs.rust-build.result }} ${{ needs.swift-mac.result }} ${{ needs.gtk-build.result }} ${{ needs.iced-build-e2e.result }} ${{ needs.iced-release.result }} ${{ needs.e2e-gtk.result }} ${{ needs.e2e-mac.result }}"
1443+
# `changes` is first for a reason: it gates every other job, so if it
1444+
# fails, all of them report `skipped` — which the allowlist below
1445+
# accepts — and this gate went GREEN with nothing having been built.
1446+
# Same greenwash shape as the #306 incident, one level further up.
1447+
# `release.yml`'s ci-gate trusts this check, so that hole reached the
1448+
# release path.
1449+
results="${{ needs.changes.result }} ${{ needs.rust-lint.result }} ${{ needs.harness-unit.result }} ${{ needs.themes-parity.result }} ${{ needs.rust-build.result }} ${{ needs.swift-mac.result }} ${{ needs.gtk-build.result }} ${{ needs.iced-build-e2e.result }} ${{ needs.iced-release.result }} ${{ needs.e2e-gtk.result }} ${{ needs.e2e-mac.result }}"
13391450
echo "job results: ${results}"
13401451
# Allowlist, not a denylist. A denylist of failure/cancelled let
13411452
# `abandoned` through — the status GitHub assigns when its own

0 commit comments

Comments
 (0)