1name: CI23on:4 pull_request:5 push:6 branches: [main]78permissions:9 contents: read1011concurrency:12 group: ci-${{ github.workflow }}-${{ github.ref }}13 cancel-in-progress: true1415# The complete demo gate used to be one serial 30-minute job. It is now the16# same set of checks split into parallel legs that each restore a Cargo cache,17# with `demo-gate` as the single required status check: it fails unless every18# leg either succeeded or was a leg the audited docs-only scope may skip.19#20# Caches are restored everywhere but written only on a push to main. A cache21# entry is scoped to the branch that wrote it, so saving on pull requests gave22# every open pull request its own ~9GB set and the repository blew through the23# 10GB ceiling, evicting the entries the legs depend on. Pull requests read24# main's entries through restore-keys instead, so the repository holds one set.25#26# scripts/verify_ci_cross_build_policy.py enforces that structure, and27# scripts/tests/demo-gate-stages.sh proves the legs still spend every check the28# single job used to run. Adding a leg means adding it to `demo-gate.needs`;29# the policy checker fails the build if you forget.30jobs:31 scope:32 name: Change scope33 runs-on: ubuntu-24.0434 timeout-minutes: 535 outputs:36 code: ${{ steps.classify.outputs.code }}37 steps:38 - name: Classify the change scope39 id: classify40 env:41 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}42 EVENT_NAME: ${{ github.event_name }}43 REPOSITORY: ${{ github.repository }}44 PULL_NUMBER: ${{ github.event.number }}45 run: |46 # Fail closed. Only a pull request whose every changed path is47 # documentation may skip the compile-heavy legs, and the lint and48 # repository-test legs run unconditionally either way, so the49 # website, docs, and README claim contracts keep their coverage.50 if [[ "$EVENT_NAME" != 'pull_request' ]]; then51 printf 'code=true\n' >>"$GITHUB_OUTPUT"52 echo 'scope=full (event is not a pull request)'53 exit 054 fi55 if ! changed="$(gh api --paginate \56 "repos/$REPOSITORY/pulls/$PULL_NUMBER/files" \57 --jq '.[].filename')"; then58 printf 'code=true\n' >>"$GITHUB_OUTPUT"59 echo 'scope=full (could not enumerate the pull request diff)'60 exit 061 fi62 if [[ -z "$changed" ]]; then63 printf 'code=true\n' >>"$GITHUB_OUTPUT"64 echo 'scope=full (empty diff listing)'65 exit 066 fi67 code=false68 while IFS= read -r changed_path; do69 [[ -n "$changed_path" ]] || continue70 case "$changed_path" in71 docs/* | website/* | *.md | LICENSE) ;;72 *)73 code=true74 printf 'code_path=%s\n' "$changed_path"75 ;;76 esac77 done <<<"$changed"78 printf 'code=%s\n' "$code" >>"$GITHUB_OUTPUT"79 if [[ "$code" == true ]]; then80 echo 'scope=full'81 else82 echo 'scope=docs-only'83 fi8485 quick-checks:86 name: Format, syntax, and lint87 runs-on: ubuntu-24.0488 timeout-minutes: 1589 env:90 CARGO_TERM_COLOR: always91 RUST_BACKTRACE: "1"92 steps:93 - name: Check out repository94 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.19596 - name: Install Linux build dependencies97 run: ./scripts/install-linux-build-deps.sh --profile lint9899 - name: Run the quick gate stage100 run: ./scripts/verify-demo-build.sh --stage quick101102 repo-tests:103 name: Repository and packaging tests104 runs-on: ubuntu-24.04105 timeout-minutes: 30106 env:107 CARGO_TERM_COLOR: always108 RUST_BACKTRACE: "1"109 steps:110 - name: Check out repository111 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1112113 - name: Install Linux build dependencies114 run: ./scripts/install-linux-build-deps.sh --profile full115116 - name: Restore the Cargo build cache117 id: cargo-cache118 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4119 with:120 path: |121 ~/.cargo/registry/index122 ~/.cargo/registry/cache123 ~/.cargo/git/db124 target125 key: cargo-repo-tests-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}126 restore-keys: |127 cargo-repo-tests-${{ runner.os }}-128129 - name: Run the repository test stage130 run: ./scripts/verify-demo-build.sh --stage repo-tests131132 - name: Save the Cargo build cache133 if: github.event_name == 'push' && steps.cargo-cache.outputs.cache-hit != 'true'134 uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4135 with:136 path: |137 ~/.cargo/registry/index138 ~/.cargo/registry/cache139 ~/.cargo/git/db140 target141 key: cargo-repo-tests-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}142143 native-tests:144 name: Workspace tests and Clippy145 runs-on: ubuntu-24.04146 timeout-minutes: 40147 needs: scope148 if: needs.scope.outputs.code == 'true'149 env:150 CARGO_TERM_COLOR: always151 RUST_BACKTRACE: "1"152 steps:153 - name: Check out repository154 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1155156 - name: Install Linux build dependencies157 run: ./scripts/install-linux-build-deps.sh --profile full158159 - name: Restore the Cargo build cache160 id: cargo-cache161 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4162 with:163 path: |164 ~/.cargo/registry/index165 ~/.cargo/registry/cache166 ~/.cargo/git/db167 target168 key: cargo-native-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}169 restore-keys: |170 cargo-native-${{ runner.os }}-171172 - name: Run the native gate stage173 run: ./scripts/verify-demo-build.sh --stage native174175 - name: Save the Cargo build cache176 if: github.event_name == 'push' && steps.cargo-cache.outputs.cache-hit != 'true'177 uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4178 with:179 path: |180 ~/.cargo/registry/index181 ~/.cargo/registry/cache182 ~/.cargo/git/db183 target184 key: cargo-native-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}185186 cross-build:187 name: Linux glibc cross build188 runs-on: ubuntu-24.04189 timeout-minutes: 40190 needs: scope191 if: needs.scope.outputs.code == 'true'192 env:193 CARGO_TERM_COLOR: always194 RUST_BACKTRACE: "1"195 steps:196 - name: Check out repository197 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1198199 - name: Install Linux build dependencies200 run: ./scripts/install-linux-build-deps.sh --profile full201202 # A megabyte, and worth writing from every branch so a pull request never203 # pays the cargo-zigbuild compile.204 - name: Restore pinned cargo-zigbuild205 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4206 with:207 path: ~/.cargo/bin/cargo-zigbuild208 key: cargo-zigbuild-${{ runner.os }}-${{ runner.arch }}-0.23.0209210 - name: Restore the Cargo build cache211 id: cargo-cache212 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4213 with:214 path: |215 ~/.cargo/registry/index216 ~/.cargo/registry/cache217 ~/.cargo/git/db218 target219 key: cargo-cross-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}220 restore-keys: |221 cargo-cross-${{ runner.os }}-222223 - name: Install pinned cross-build tools224 run: |225 python3 -m venv "$RUNNER_TEMP/zig-venv"226 printf '%s\n' \227 'ziglang==0.16.0 --hash=sha256:9fcda73f62b851dd72a54b710ad40a209896db14cfb13649e62191243556342b' \228 | "$RUNNER_TEMP/zig-venv/bin/pip" install \229 --disable-pip-version-check \230 --only-binary=:all: \231 --require-hashes \232 --requirement /dev/stdin233 install -d "$RUNNER_TEMP/ci-bin"234 # Expand runner paths when the generated wrapper executes.235 # shellcheck disable=SC2016236 printf '%s\n' \237 '#!/usr/bin/env bash' \238 'exec "$RUNNER_TEMP/zig-venv/bin/python" -m ziglang "$@"' \239 >"$RUNNER_TEMP/ci-bin/zig"240 chmod 0755 "$RUNNER_TEMP/ci-bin/zig"241 echo "$RUNNER_TEMP/ci-bin" >>"$GITHUB_PATH"242 export PATH="$RUNNER_TEMP/ci-bin:$PATH"243 if ! cargo-zigbuild --version 2>/dev/null \244 | grep -Fxq 'cargo-zigbuild 0.23.0'; then245 cargo install cargo-zigbuild --locked --version 0.23.0246 fi247 test "$(zig version)" = 0.16.0248 test "$(cargo-zigbuild --version)" = 'cargo-zigbuild 0.23.0'249250 - name: Run the Linux cross-build gate stage251 env:252 GOQ_VERIFY_IN_PROCESS_GSTREAMER: "1"253 GOQ_REQUIRE_LINUX_CROSS_BUILD: "1"254 run: ./scripts/verify-demo-build.sh --stage cross255256 - name: Save the Cargo build cache257 if: github.event_name == 'push' && steps.cargo-cache.outputs.cache-hit != 'true'258 uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4259 with:260 path: |261 ~/.cargo/registry/index262 ~/.cargo/registry/cache263 ~/.cargo/git/db264 target265 key: cargo-cross-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}266267 gstreamer-gate:268 name: In-process GStreamer gate269 runs-on: ubuntu-24.04270 timeout-minutes: 40271 needs: scope272 if: needs.scope.outputs.code == 'true'273 env:274 CARGO_TERM_COLOR: always275 RUST_BACKTRACE: "1"276 steps:277 - name: Check out repository278 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1279280 - name: Install Linux build dependencies281 run: ./scripts/install-linux-build-deps.sh --profile full282283 - name: Restore the Cargo build cache284 id: cargo-cache285 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4286 with:287 path: |288 ~/.cargo/registry/index289 ~/.cargo/registry/cache290 ~/.cargo/git/db291 target292 key: cargo-gstreamer-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}293 restore-keys: |294 cargo-gstreamer-${{ runner.os }}-295296 - name: Run the in-process GStreamer gate stage297 env:298 GOQ_VERIFY_IN_PROCESS_GSTREAMER: "1"299 run: ./scripts/verify-demo-build.sh --stage gstreamer300301 - name: Save the Cargo build cache302 if: github.event_name == 'push' && steps.cargo-cache.outputs.cache-hit != 'true'303 uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4304 with:305 path: |306 ~/.cargo/registry/index307 ~/.cargo/registry/cache308 ~/.cargo/git/db309 target310 key: cargo-gstreamer-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}311312 loopback:313 name: Loopback proof ${{ matrix.case }}314 runs-on: ubuntu-24.04315 timeout-minutes: 40316 needs: scope317 if: needs.scope.outputs.code == 'true'318 strategy:319 fail-fast: false320 # Cases 1-3 are all debug-profile builds of the same binaries, so they321 # share one cache entry instead of storing three near-identical copies.322 # One designated case per entry writes it, which also means no two323 # concurrent matrix legs race to reserve the same key.324 matrix:325 include:326 - case: 1327 cache: debug328 save: "true"329 - case: 2330 cache: debug331 save: "false"332 - case: 3333 cache: debug334 save: "false"335 - case: 4336 cache: release337 save: "true"338 env:339 CARGO_TERM_COLOR: always340 RUST_BACKTRACE: "1"341 steps:342 - name: Check out repository343 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1344345 - name: Install Linux build dependencies346 run: ./scripts/install-linux-build-deps.sh --profile full347348 - name: Restore the Cargo build cache349 id: cargo-cache350 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4351 with:352 path: |353 ~/.cargo/registry/index354 ~/.cargo/registry/cache355 ~/.cargo/git/db356 target357 key: cargo-loopback-${{ matrix.cache }}-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}358 restore-keys: |359 cargo-loopback-${{ matrix.cache }}-${{ runner.os }}-360361 - name: Run the loopback proof362 run: ./scripts/verify-demo-build.sh --stage loopback --loopback-case ${{ matrix.case }}363364 - name: Save the Cargo build cache365 if: github.event_name == 'push' && steps.cargo-cache.outputs.cache-hit != 'true' && matrix.save == 'true'366 uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4367 with:368 path: |369 ~/.cargo/registry/index370 ~/.cargo/registry/cache371 ~/.cargo/git/db372 target373 key: cargo-loopback-${{ matrix.cache }}-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}374375 release-containment:376 name: Release-profile bypass containment377 runs-on: ubuntu-24.04378 timeout-minutes: 40379 needs: scope380 if: needs.scope.outputs.code == 'true'381 env:382 CARGO_TERM_COLOR: always383 RUST_BACKTRACE: "1"384 steps:385 - name: Check out repository386 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1387388 - name: Install Linux build dependencies389 run: ./scripts/install-linux-build-deps.sh --profile full390391 - name: Restore the Cargo build cache392 id: cargo-cache393 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4394 with:395 path: |396 ~/.cargo/registry/index397 ~/.cargo/registry/cache398 ~/.cargo/git/db399 target400 key: cargo-containment-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}401 restore-keys: |402 cargo-containment-${{ runner.os }}-403404 - name: Run the release containment stage405 run: ./scripts/verify-demo-build.sh --stage containment406407 - name: Save the Cargo build cache408 if: github.event_name == 'push' && steps.cargo-cache.outputs.cache-hit != 'true'409 uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4410 with:411 path: |412 ~/.cargo/registry/index413 ~/.cargo/registry/cache414 ~/.cargo/git/db415 target416 key: cargo-containment-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}417418 decky-gate:419 name: Decky plugin gate420 runs-on: ubuntu-24.04421 timeout-minutes: 10422 steps:423 - name: Check out repository424 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1425426 - name: Install pinned pnpm427 uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4428 with:429 version: 9.4.0430 run_install: false431432 - name: Set up Node.js433 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0434 with:435 node-version: "22"436 cache: pnpm437 cache-dependency-path: decky/pnpm-lock.yaml438439 - name: Install Decky dependencies440 working-directory: decky441 run: pnpm install --frozen-lockfile442443 - name: Test and build Decky plugin444 working-directory: decky445 run: |446 pnpm test447 pnpm typecheck448 pnpm build449450 - name: Verify deterministic Decky package451 run: |452 python3 scripts/package-decky-plugin.py "$RUNNER_TEMP/decky-artifacts"453 python3 -m unittest scripts.tests.test_decky_package -v454455 portal-target-matrix:456 name: Portal ${{ matrix.target }} compile guard457 runs-on: ${{ matrix.os }}458 timeout-minutes: 40459 needs: scope460 if: needs.scope.outputs.code == 'true'461 strategy:462 fail-fast: false463 matrix:464 include:465 - os: macos-26466 target: x86_64-apple-darwin467 - os: windows-2025468 target: x86_64-pc-windows-msvc469 steps:470 - name: Check out repository471 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1472473 - name: Add the release matrix target474 run: rustup target add ${{ matrix.target }}475476 # These targets are built and attested for every release but are never477 # published, so nothing else in CI would notice them breaking. This job478 # keeps them compiling under both documented pointer policies.479 - name: Compile Portal under both pointer policies480 # Windows runners default to PowerShell, which reads the backslash481 # continuation as an incomplete unary operator and fails to parse the482 # step before compiling anything. Pin bash so both matrix legs run the483 # same commands.484 shell: bash485 run: |486 cargo check --locked -p portal --all-targets --target ${{ matrix.target }}487 cargo check --locked -p portal --all-targets --target ${{ matrix.target }} \488 --features experimental-non-macos-pointer-capture489490 demo-gate:491 name: Complete demo gate492 runs-on: ubuntu-24.04493 timeout-minutes: 5494 # Runs even when a leg fails, because this job is what reports that495 # failure. It never passes on its own: the step below inspects every496 # dependency result and exits non-zero unless each one is accounted for.497 if: always()498 needs:499 - scope500 - quick-checks501 - repo-tests502 - native-tests503 - cross-build504 - gstreamer-gate505 - loopback506 - release-containment507 - decky-gate508 - portal-target-matrix509 env:510 CARGO_TERM_COLOR: always511 RUST_BACKTRACE: "1"512 steps:513 - name: Require every gate leg to have passed514 env:515 LEG_RESULTS: ${{ toJSON(needs) }}516 SCOPE_CODE: ${{ needs.scope.outputs.code }}517 run: |518 # A leg may only be missing when the audited docs-only scope skipped519 # it. Every other non-success result, including cancelled, fails the520 # required check.521 skippable='native-tests cross-build gstreamer-gate loopback release-containment portal-target-matrix'522 docs_only=false523 if [[ "$SCOPE_CODE" == 'false' ]]; then524 docs_only=true525 fi526 failed=false527 while IFS=$'\t' read -r leg result; do528 [[ -n "$leg" ]] || continue529 case "$result" in530 success)531 printf 'leg_passed=%s\n' "$leg"532 ;;533 skipped)534 if [[ "$docs_only" == true && " $skippable " == *" $leg "* ]]; then535 printf 'leg_skipped_docs_only=%s\n' "$leg"536 else537 printf 'leg_must_not_be_skipped=%s\n' "$leg"538 failed=true539 fi540 ;;541 *)542 printf 'leg_failed=%s result=%s\n' "$leg" "$result"543 failed=true544 ;;545 esac546 done < <(jq -r 'to_entries[] | "\(.key)\t\(.value.result)"' <<<"$LEG_RESULTS")547 if [[ "$failed" == true ]]; then548 echo 'complete demo gate failed' >&2549 exit 1550 fi551 echo 'complete_demo_gate=ok'