|
25 | 25 | fixtures: ${{ steps.filter.outputs.fixtures }} |
26 | 26 | tests: ${{ steps.filter.outputs.tests }} |
27 | 27 | ci: ${{ steps.filter.outputs.ci }} |
| 28 | + deb: ${{ steps.filter.outputs.deb }} |
28 | 29 | steps: |
29 | 30 | - uses: actions/checkout@v6 |
30 | 31 | with: |
|
37 | 38 | - 'Cargo.toml' |
38 | 39 | - 'Cargo.lock' |
39 | 40 | - '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' |
41 | 44 | - 'clippy.toml' |
42 | 45 | - 'crates/**' |
43 | 46 | - 'third_party/ghostty/**' |
|
71 | 74 | - 'Makefile' |
72 | 75 | ci: |
73 | 76 | - '.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' |
74 | 100 |
|
75 | 101 | rust-lint: |
76 | 102 | needs: changes |
@@ -680,8 +706,10 @@ jobs: |
680 | 706 | # packaging/**): this lane builds the same packaged configuration the deb |
681 | 707 | # ships, so a build-deb.sh or nfpm.yaml change must be able to reach it. |
682 | 708 | # 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' |
685 | 713 | runs-on: ubuntu-latest |
686 | 714 | timeout-minutes: 45 |
687 | 715 | steps: |
@@ -729,21 +757,55 @@ jobs: |
729 | 757 | key: cargo-iced-release-${{ runner.os }}-${{ hashFiles('**/Cargo.toml', 'rust-toolchain.toml') }} |
730 | 758 | restore-keys: cargo-iced-release-${{ runner.os }}- |
731 | 759 |
|
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 |
739 | 788 | # 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. |
744 | 794 | - name: Build Iced + roostctl (release) |
| 795 | + if: needs.changes.outputs.deb != 'true' |
745 | 796 | run: cargo build --release -p roost-iced -p roost-cli --features roost-iced/linux-package |
746 | 797 |
|
| 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 | + |
747 | 809 | - name: Run Iced release-profile E2E (Linux X11) |
748 | 810 | env: |
749 | 811 | ICED_BACKEND: wgpu |
@@ -856,6 +918,49 @@ jobs: |
856 | 918 |
|
857 | 919 | echo "OK: packaged build adopted the production namespace ($SOCKET_PATH)" |
858 | 920 |
|
| 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 | +
|
859 | 964 | - name: Collect Iced diagnostics |
860 | 965 | if: always() |
861 | 966 | shell: bash |
@@ -1335,7 +1440,13 @@ jobs: |
1335 | 1440 | steps: |
1336 | 1441 | - name: Verify no required job failed |
1337 | 1442 | 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 }}" |
1339 | 1450 | echo "job results: ${results}" |
1340 | 1451 | # Allowlist, not a denylist. A denylist of failure/cancelled let |
1341 | 1452 | # `abandoned` through — the status GitHub assigns when its own |
|
0 commit comments