diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32534ab3..54b57c2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -243,120 +243,6 @@ jobs: if: github.event_name != 'pull_request' run: moon run :ci - - name: Multi-window gtk3 e2e gate - # The ONE CI lane that compiles + runs the native app (Wails v3, - # go/cmd/compass-app) — the multi-window smoke gate (design record - # compass-multi-window §M4). It is a DEDICATED step, not a moon-battery - # task, for the same reason the pgtest suite above is: it is build-tagged - # (unix,gtk3) so `moon`'s untagged `go test ./...` never compiles it, and - # it needs an environment the bare moon gate has no business realizing — - # a virtual framebuffer (the real GTK3/WebKit shell opens windows) and - # the heavy WebKitGTK cgo-link closure. The per-PR moon battery stays - # GTK-free; this step realizes the closure out of band via - # tools/toolchain/gtk-e2e-env.nix (pinned to the SAME devenv.lock nixpkgs - # the dev shell links against), so a dev box and CI run byte-for-byte the - # same libraries. - # - # AFFECTED-GUARDED to match the one-job affected posture: on a PR it runs - # only when go/cmd/compass-app/ actually changed (the native app is the - # only tree whose behavior it gates), so a Go/UI/docs PR never pays the - # WebKitGTK realization. push-to-main and the nightly schedule always run - # it (the full-sweep backstop), matching the moon affected/full split. - if: success() - working-directory: go - env: - CGO_ENABLED: '1' - run: | - # Decide whether this event must run the gate. - run_it=1 - if [ "$GITHUB_EVENT_NAME" = pull_request ]; then - base="origin/${GITHUB_BASE_REF}" - # fetch-depth:0 (job checkout) gives the base ref for the diff. Fail - # LOUD if it does not resolve rather than swallowing the error as - # "not affected": an unresolvable base (e.g. a stacked PR's base - # churning under a pull_request.edited re-point) must red the gate, - # never skip it green. No `|| true` on the diff for the same reason — - # under `bash -e` a genuine diff failure aborts the step. - if ! git -C .. rev-parse --verify --quiet "$base" >/dev/null; then - echo "::error::gtk3 e2e affected gate: base ref $base does not resolve — cannot compute the affected set" - exit 1 - fi - changed=$(git -C .. diff --name-only "$base"...HEAD -- go/cmd/compass-app/) - if [ -z "$changed" ]; then - run_it=0 - fi - fi - if [ "$run_it" = 0 ]; then - echo "gtk3 e2e: PR does not touch go/cmd/compass-app/ — skipping (affected gate)" - exit 0 - fi - - # Realize the out-of-band gtk3 env, ALL parts via `nix build` (not - # `nix eval`): xvfb-run (+ its Xvfb) + pkg-config on PATH, the - # WebKitGTK pkg-config closure, and the nixpkgs C toolchain. `nix - # build` is what actually builds each into this runner's store; a `nix - # eval --raw` of a search-path string strips nix's store context, so - # the `.pc` paths it prints would name derivations nix never built and - # the cgo link fails "No package 'glib-2.0' found". All pinned to - # devenv.lock's nixpkgs via the helper. - helper=../tools/toolchain/gtk-e2e-env.nix - binenv=$(nix build --no-link --print-out-paths -f "$helper" bin) - pcenv=$(nix build --no-link --print-out-paths -f "$helper" pkgConfig) - # cc.out, not the bare `cc`: the cc-wrapper is multi-output (out, man, - # info) and `nix build --print-out-paths` of the derivation prints - # ALL of them, so a bare `cc` captures two lines and CC resolves to - # the `-man` dir. The `.out` selector pins the one output with bin/cc. - ccenv=$(nix build --no-link --print-out-paths -f "$helper" cc.out) - PKG_CONFIG_PATH="$pcenv/lib/pkgconfig:$pcenv/share/pkgconfig" - export PKG_CONFIG_PATH - export PATH="$binenv/bin:$PATH" - # Link the cgo objects with the nixpkgs toolchain, NOT the runner's - # /usr/bin/gcc: WebKitGTK from this nixpkgs is built against glibc - # 2.42, so the system gcc's older libc fails to link - # libwebkit2gtk-4.1.so ("undefined reference to - # `__inet_pton_chk@GLIBC_2.42'"). The cc-wrapper carries the matching - # glibc and rpaths + interp-stamps the store libs, so the test binary - # is self-contained on a non-NixOS runner. This is the toolchain a dev - # box links with implicitly, so CI now matches it byte-for-byte. - export CC="$ccenv/bin/cc" - export CXX="$ccenv/bin/c++" - - # xvfb-run -a auto-picks a free display. The Xvfb SERVER log goes to a - # real file (-e), NOT /dev/stdout: with the step's own `>… 2>&1` - # redirect already pointing fd 1 at the go-test log, `-e /dev/stdout` - # makes xvfb-run reopen that fd for the server log and the fd juggling - # swallows the child's stdout — the go-test output never reaches the - # file, so the PASS-line guard below fires on an empty log even though - # the e2e passed. A dedicated server-log file avoids the collision and - # is replayed only on failure, where a bring-up error is what matters. - # Capture-then-replay + explicit exit so a FAIL is never masked (same - # discipline as the pgtest step above). - # - # No -race here (the pgtest step uses it): this is a cgo+GTK smoke lane - # where -race is slow and noisy, and the driver-goroutine / InvokeSync - # handoffs it would cover are already exercised under -race by the - # untagged unit suite. The omission is deliberate. - rc=0 - xvfb-run -a -e /tmp/xvfb-server.log \ - go test -tags 'unix gtk3' -run 'E2E' -count=1 -v -timeout 10m \ - ./cmd/compass-app/ >/tmp/gtk3-e2e.log 2>&1 || rc=$? - cat /tmp/gtk3-e2e.log - # Guard against a silent no-op: the gate must have PASSED the e2e, not - # merely started it. Assert the PASS line, not `=== RUN`: `go test -v` - # prints `=== RUN ` BEFORE the body runs, so a `t.Skip` (no - # display came up, or any future skip condition) still emits RUN — a - # RUN-grep would read a skipped, assertion-free run as green. The PASS - # grep fails on skip, on no-run, and on a build-tag regression that - # drops the test; a genuine FAIL is still caught by `exit "$rc"`. This - # is the polarity the pgtest step uses (it fails on the SKIP message). - if ! grep -q -- '--- PASS: TestMultiWindowCloseCancelsOnlyClosingWindowE2E' /tmp/gtk3-e2e.log; then - echo "::error::gtk3 e2e gate did not PASS TestMultiWindowCloseCancelsOnlyClosingWindowE2E — it skipped (no display came up), the build tag broke, or the test did not run" - echo "--- Xvfb server log (bring-up diagnostics) ---" - cat /tmp/xvfb-server.log 2>/dev/null || echo "(no Xvfb server log)" - exit 1 - fi - exit "$rc" - - name: Retrospect # Collapse the single job's flat task fan-out into per-task sections in # the Actions log, so a failure is one expand instead of a scroll. Reads @@ -1010,6 +896,190 @@ jobs: fi echo "forge oracle: the live-contract suite ran both the GitHub and Linear legs against the testbed and reported ok" + gtk3-e2e: + name: gtk3-e2e (multi-window native app gate) + runs-on: ubuntu-latest + # Peeled out of the moon gate into its own lane behind the CI rollup; the + # native GTK3/WebKit app gate realizes a heavy out-of-band WebKitGTK/xvfb + # closure the bare moon gate has no business building. The in-step affected + # guard stays as defense-in-depth; runs its own bootstrap to reach that guard + # until T5 hoists the affected detection into setup and adds a job-level + # gtk3_affected gate. No privileged container. + if: >- + github.event_name != 'workflow_dispatch' && + (github.event_name != 'pull_request' || + github.event.action != 'edited' || + github.event.changes.base != null) + timeout-minutes: 30 + env: + TMPDIR: /tmp + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # The gtk3 affected `git -C ..` diff needs the base branch history. + fetch-depth: 0 + + - uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31 + with: + # The forks' functional CI is `nix build` of their own flakes, and the + # whole toolchain — language runtimes AND the nixpkgs tools — is built + # from nix below. Flakes are needed for the former; nix-command for both. + # + # The substituters below are declared here rather than delegated via + # `accept-flake-config = true`. That setting makes nix accept the + # `nixConfig` of ANY flake it evaluates, and the RigelBuild/devenv + # flake the build fetches carries exactly such a block — so a PR could add its own substituter + # AND its own trusted public key, and CI would fetch and run binaries + # signed by the attacker's key, with signature verification satisfied + # by the key the same PR supplied. Naming the two caches the forks + # legitimately use keeps that trust in a reviewed file: adding a third + # is a change to this workflow, not to a vendored tree. + extra_nix_config: | + experimental-features = nix-command flakes + extra-substituters = https://devenv.cachix.org https://cachix.cachix.org + extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= cachix.cachix.org-1:eWNHQldwUO7G2VkjpnjDbWwy4KQ/HNxht7H4SSoMckM= + + - name: Put the language toolchains on PATH + # Phase one of the two-phase bootstrap. The language runtimes + # (bun/node/moon/go) come from nix, not `setup-*` actions: gate-tools.nix's + # `langs` output resolves the identical derivations the dev shell does — + # bun/node/moon from tools/toolchain/toolchain-tools.nix, go from the + # go-overlay applied to the devenv.lock-pinned nixpkgs — so CI runs the + # pinned versions byte-for-byte and the parity gate has a concrete store + # path to check each against. `langs` is a closed set, so it needs no + # `--arg attrs` (the head defaults it). + # + # This must precede phase two: `--print-nix-attrs` runs under `bun`, so + # bun has to be on PATH before that parse can run. + run: | + stores=$(nix eval --json -f tools/toolchain/gate-tools.nix langs \ + | jq -r '.[].store') + # Fail closed locally rather than leaning on the absence of a + # root-level flake.nix: with no installables `nix build` would build a + # default package if one existed, so an empty `langs` must error here. + [ -n "$stores" ] || { + echo "::error::gate-tools.nix langs produced no store paths" + exit 1 + } + nix build --no-link $stores + for store in $stores; do + echo "$store/bin" >>"$GITHUB_PATH" + done + + - name: Multi-window gtk3 e2e gate + # The ONE CI lane that compiles + runs the native app (Wails v3, + # go/cmd/compass-app) — the multi-window smoke gate (design record + # compass-multi-window §M4). It is a DEDICATED step, not a moon-battery + # task, for the same reason the pgtest suite above is: it is build-tagged + # (unix,gtk3) so `moon`'s untagged `go test ./...` never compiles it, and + # it needs an environment the bare moon gate has no business realizing — + # a virtual framebuffer (the real GTK3/WebKit shell opens windows) and + # the heavy WebKitGTK cgo-link closure. The per-PR moon battery stays + # GTK-free; this step realizes the closure out of band via + # tools/toolchain/gtk-e2e-env.nix (pinned to the SAME devenv.lock nixpkgs + # the dev shell links against), so a dev box and CI run byte-for-byte the + # same libraries. + # + # AFFECTED-GUARDED to match the one-job affected posture: on a PR it runs + # only when go/cmd/compass-app/ actually changed (the native app is the + # only tree whose behavior it gates), so a Go/UI/docs PR never pays the + # WebKitGTK realization. push-to-main and the nightly schedule always run + # it (the full-sweep backstop), matching the moon affected/full split. + if: success() + working-directory: go + env: + CGO_ENABLED: '1' + run: | + # Decide whether this event must run the gate. + run_it=1 + if [ "$GITHUB_EVENT_NAME" = pull_request ]; then + base="origin/${GITHUB_BASE_REF}" + # fetch-depth:0 (job checkout) gives the base ref for the diff. Fail + # LOUD if it does not resolve rather than swallowing the error as + # "not affected": an unresolvable base (e.g. a stacked PR's base + # churning under a pull_request.edited re-point) must red the gate, + # never skip it green. No `|| true` on the diff for the same reason — + # under `bash -e` a genuine diff failure aborts the step. + if ! git -C .. rev-parse --verify --quiet "$base" >/dev/null; then + echo "::error::gtk3 e2e affected gate: base ref $base does not resolve — cannot compute the affected set" + exit 1 + fi + changed=$(git -C .. diff --name-only "$base"...HEAD -- go/cmd/compass-app/) + if [ -z "$changed" ]; then + run_it=0 + fi + fi + if [ "$run_it" = 0 ]; then + echo "gtk3 e2e: PR does not touch go/cmd/compass-app/ — skipping (affected gate)" + exit 0 + fi + + # Realize the out-of-band gtk3 env, ALL parts via `nix build` (not + # `nix eval`): xvfb-run (+ its Xvfb) + pkg-config on PATH, the + # WebKitGTK pkg-config closure, and the nixpkgs C toolchain. `nix + # build` is what actually builds each into this runner's store; a `nix + # eval --raw` of a search-path string strips nix's store context, so + # the `.pc` paths it prints would name derivations nix never built and + # the cgo link fails "No package 'glib-2.0' found". All pinned to + # devenv.lock's nixpkgs via the helper. + helper=../tools/toolchain/gtk-e2e-env.nix + binenv=$(nix build --no-link --print-out-paths -f "$helper" bin) + pcenv=$(nix build --no-link --print-out-paths -f "$helper" pkgConfig) + # cc.out, not the bare `cc`: the cc-wrapper is multi-output (out, man, + # info) and `nix build --print-out-paths` of the derivation prints + # ALL of them, so a bare `cc` captures two lines and CC resolves to + # the `-man` dir. The `.out` selector pins the one output with bin/cc. + ccenv=$(nix build --no-link --print-out-paths -f "$helper" cc.out) + PKG_CONFIG_PATH="$pcenv/lib/pkgconfig:$pcenv/share/pkgconfig" + export PKG_CONFIG_PATH + export PATH="$binenv/bin:$PATH" + # Link the cgo objects with the nixpkgs toolchain, NOT the runner's + # /usr/bin/gcc: WebKitGTK from this nixpkgs is built against glibc + # 2.42, so the system gcc's older libc fails to link + # libwebkit2gtk-4.1.so ("undefined reference to + # `__inet_pton_chk@GLIBC_2.42'"). The cc-wrapper carries the matching + # glibc and rpaths + interp-stamps the store libs, so the test binary + # is self-contained on a non-NixOS runner. This is the toolchain a dev + # box links with implicitly, so CI now matches it byte-for-byte. + export CC="$ccenv/bin/cc" + export CXX="$ccenv/bin/c++" + + # xvfb-run -a auto-picks a free display. The Xvfb SERVER log goes to a + # real file (-e), NOT /dev/stdout: with the step's own `>… 2>&1` + # redirect already pointing fd 1 at the go-test log, `-e /dev/stdout` + # makes xvfb-run reopen that fd for the server log and the fd juggling + # swallows the child's stdout — the go-test output never reaches the + # file, so the PASS-line guard below fires on an empty log even though + # the e2e passed. A dedicated server-log file avoids the collision and + # is replayed only on failure, where a bring-up error is what matters. + # Capture-then-replay + explicit exit so a FAIL is never masked (same + # discipline as the pgtest step above). + # + # No -race here (the pgtest step uses it): this is a cgo+GTK smoke lane + # where -race is slow and noisy, and the driver-goroutine / InvokeSync + # handoffs it would cover are already exercised under -race by the + # untagged unit suite. The omission is deliberate. + rc=0 + xvfb-run -a -e /tmp/xvfb-server.log \ + go test -tags 'unix gtk3' -run 'E2E' -count=1 -v -timeout 10m \ + ./cmd/compass-app/ >/tmp/gtk3-e2e.log 2>&1 || rc=$? + cat /tmp/gtk3-e2e.log + # Guard against a silent no-op: the gate must have PASSED the e2e, not + # merely started it. Assert the PASS line, not `=== RUN`: `go test -v` + # prints `=== RUN ` BEFORE the body runs, so a `t.Skip` (no + # display came up, or any future skip condition) still emits RUN — a + # RUN-grep would read a skipped, assertion-free run as green. The PASS + # grep fails on skip, on no-run, and on a build-tag regression that + # drops the test; a genuine FAIL is still caught by `exit "$rc"`. This + # is the polarity the pgtest step uses (it fails on the SKIP message). + if ! grep -q -- '--- PASS: TestMultiWindowCloseCancelsOnlyClosingWindowE2E' /tmp/gtk3-e2e.log; then + echo "::error::gtk3 e2e gate did not PASS TestMultiWindowCloseCancelsOnlyClosingWindowE2E — it skipped (no display came up), the build tag broke, or the test did not run" + echo "--- Xvfb server log (bring-up diagnostics) ---" + cat /tmp/xvfb-server.log 2>/dev/null || echo "(no Xvfb server log)" + exit 1 + fi + exit "$rc" + dogfood-e2e: name: Dogfood e2e (deterministic full-stack tier) runs-on: ubuntu-latest @@ -1486,7 +1556,7 @@ jobs: # `!cancelled()` must be parenthesized against the OR group: `&&` binds tighter # than `||`, so without the parens the trailing `|| changes.base != null` would # detach from the AND. - needs: [gates, dogfood-e2e, pgtest, microvm, forge-oracle] + needs: [gates, dogfood-e2e, pgtest, microvm, forge-oracle, gtk3-e2e] if: >- !cancelled() && github.event_name != 'workflow_dispatch' && ( @@ -1514,9 +1584,10 @@ jobs: pgtest='${{ needs.pgtest.result }}' microvm='${{ needs.microvm.result }}' forge='${{ needs.forge-oracle.result }}' - echo "gates=$gates dogfood-e2e=$dogfood pgtest=$pgtest microvm=$microvm forge-oracle=$forge" - if [ "$gates" != "success" ] || [ "$dogfood" != "success" ] || [ "$pgtest" != "success" ] || [ "$microvm" != "success" ] || [ "$forge" != "success" ]; then - echo "::error::a required work job did not succeed (gates=$gates, dogfood-e2e=$dogfood, pgtest=$pgtest, microvm=$microvm, forge-oracle=$forge)" + gtk3='${{ needs.gtk3-e2e.result }}' + echo "gates=$gates dogfood-e2e=$dogfood pgtest=$pgtest microvm=$microvm forge-oracle=$forge gtk3-e2e=$gtk3" + if [ "$gates" != "success" ] || [ "$dogfood" != "success" ] || [ "$pgtest" != "success" ] || [ "$microvm" != "success" ] || [ "$forge" != "success" ] || [ "$gtk3" != "success" ]; then + echo "::error::a required work job did not succeed (gates=$gates, dogfood-e2e=$dogfood, pgtest=$pgtest, microvm=$microvm, forge-oracle=$forge, gtk3-e2e=$gtk3)" exit 1 fi echo "all work jobs succeeded"