diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 5b6a9baf..8a26959e 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -4,27 +4,55 @@ on: push: branches: [main] -# One in-flight run per ref: a push that supersedes an earlier one should not keep -# two ~35m Linux checks and two macOS builds competing for the same runner pool. +# One in-flight run per ref: a superseded PR run should not keep three Linux +# shards and a macOS build competing for hosted capacity. concurrency: group: nix-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} -# Both jobs are named for the flake system they prove, not for the runner that hosts them. -# They are deliberately not the same size — see the comment on the Darwin job. +# Linux uses four build-graph shards so each remaining cold Rust derivation gets a +# runner instead of competing with an unrelated compile. The aggregation job +# preserves the stable `check-x86_64-linux` status used by branch protection. jobs: - check-x86_64-linux: + check-x86_64-linux-shard: + name: check-x86_64-linux (${{ matrix.group }}) runs-on: ubuntu-latest - # A healthy full `nix flake check` baseline is ~35m; 90m provides - # headroom while still failing boundedly. - timeout-minutes: 90 + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - group: package + targets: >- + .#checks.x86_64-linux.st2 + .#checks.x86_64-linux.debug-assertions + .#checks.x86_64-linux.help + .#checks.x86_64-linux.completions + .#checks.x86_64-linux.hooks-replacement + .#checks.x86_64-linux.pty-fleet-contract + - group: providers + targets: >- + .#checks.x86_64-linux.provider-components + .#checks.x86_64-linux.wasip2-resource-providers + - group: integration + targets: >- + .#checks.x86_64-linux.release-integration + .#checks.x86_64-linux.pi-extension-types + - group: wasm + targets: >- + .#checks.x86_64-linux.wasm-resolver-feature + .#checks.x86_64-linux.wasm-resolver-artifact steps: - uses: actions/checkout@v4 - uses: DeterminateSystems/determinate-nix-action@v3 - # `nix flake check` rather than `nix build`: it builds the package *and* - # evaluates every `checks.*`, so fmt, clippy, the hermetic test suite, and - # the `--help` smoke test all gate the PR from one entrypoint. - - run: nix flake check --print-build-logs + - run: nix build --no-link --print-build-logs ${{ matrix.targets }} + + check-x86_64-linux: + if: always() + needs: check-x86_64-linux-shard + runs-on: ubuntu-latest + steps: + - run: test "${{ needs.check-x86_64-linux-shard.result }}" = success # st2 drives real processes — process groups, signals, pipes, reaping — and those paths # are OS behaviour, not portable Rust: #356 and #255 were aarch64-darwin failures no Linux @@ -34,8 +62,8 @@ jobs: # # Narrower than its Linux sibling on purpose: `nix build .#st2` builds the package and runs # its hermetic suite via `doCheck` — the surface those issues live on — without also - # demanding cold aarch64-darwin builds of the `pty` and `otelite` inputs that - # `checks.parked-recovery` and `checks.otel-export` pull in. Widen once this is stable. + # demanding cold aarch64-darwin builds of the inputs used by + # `checks.release-integration` on Linux. Widen once this is stable. check-aarch64-darwin: runs-on: macos-15 timeout-minutes: 90 diff --git a/flake.nix b/flake.nix index 7a30cdaf..f3345672 100644 --- a/flake.nix +++ b/flake.nix @@ -11,9 +11,8 @@ pty.url = "github:compoundingtech/pty/504ac7332895fe1fa3767b530dcd99f091f56cda"; pty.inputs.nixpkgs.follows = "nixpkgs"; # Shared tooling packages from overengineering: provides the `otelite` - # OTLP collector binary that the OTel export integration gate - # (`tests/otel_export.rs`, exposed as `checks.otel-export`) drives to - # prove real span export end-to-end. Pinned to a full rev (like `pty`) + # OTLP collector binary that `checks.release-integration` drives to prove + # real span export end-to-end. Pinned to a full rev (like `pty`) # so CI is reproducible; bump deliberately via `nix flake lock`. effect-utils.url = "github:overengineeringstudio/effect-utils/911e2ce0f4ac39d2b54f9ebd6df035234982f721"; @@ -75,6 +74,20 @@ "fish" ]; + # buildRustPackage compiles the workspace once per derivation, so a gate that differs from + # an existing derivation only by test selection is folded into that derivation's check + # phase instead of paying for a second compile. These extra runs deliberately mirror + # `cargoCheckHook`: same source, profile, offline mode and target dir, so they reuse the + # artifacts it just built. + rustHostTarget = pkgs.stdenv.hostPlatform.rust.rustcTarget; + extraCargoTest = + { label, flags }: + '' + echo "--- cargo test: ${label}" + cargo test -j "$NIX_BUILD_CORES" --release \ + --target ${rustHostTarget} --offline ${pkgs.lib.escapeShellArgs flags} + ''; + st2 = pkgs.rustPlatform.buildRustPackage { pname = "st2"; inherit version; @@ -180,6 +193,9 @@ # Production variant for catalogs that declare wasm resource-profile resolvers. Keep the # default package lightweight; consumers opt into the wasmtime closure explicitly. + # + # Doubles as `checks.wasm-resolver-feature`: the default hermetic suite runs with the + # production feature set, and the feature-gated targets reuse that same build. st2WasmResolver = st2.overrideAttrs (old: { pname = "st2-wasm-resolver"; cargoBuildFeatures = (old.cargoBuildFeatures or [ ]) ++ [ "wasm-resolver" ]; @@ -187,115 +203,96 @@ # Wasmtime's Cranelift build and the feature-gated resolver tests need the Rust toolchain # inherited from buildRustPackage plus an LLVM linker on every supported platform. nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.lld ]; + # Non-vacuous feature gate: both the runner's live resync integration and agent-spec's + # wasm ABI/containment suite execute with the same features as the production variant. + postCheck = extraCargoTest { + label = "wasm-resolver feature suite"; + flags = [ + "--features" + "wasm-resolver" + "--workspace" + "--exclude" + "st2-resource-providers" + "--exclude" + "st2-github-issue-component" + "--exclude" + "st2-github-pr-component" + "--exclude" + "st2-pty-stats-component" + "--exclude" + "st2-vista-component" + "--test" + "resync" + "--test" + "resync_notify_chain" + "--test" + "profile_wasm" + ]; + }; }); - # Non-vacuous feature gate: both the runner's live resync integration and agent-spec's wasm - # ABI/containment suite compile and execute with the same features as the production variant. - st2WasmResolverCheck = st2WasmResolver.overrideAttrs (_: { - pname = "st2-wasm-resolver-check"; - cargoTestFlags = [ - "--workspace" - "--exclude" - "st2-resource-providers" - "--exclude" - "st2-github-issue-component" - "--exclude" - "st2-github-pr-component" - "--exclude" - "st2-pty-stats-component" - "--exclude" - "st2-vista-component" - "--test" - "resync" - "--test" - "resync_notify_chain" - "--test" - "profile_wasm" - ]; - }); + providerComponentPackages = { + "st2-github-issue-component" = "st2_github_issue_component"; + "st2-github-pr-component" = "st2_github_pr_component"; + "st2-pty-stats-component" = "st2_pty_stats_component"; + "st2-vista-component" = "st2_vista_component"; + }; - # The default workspace remains Wasmtime-free; this focused gate opts the Component Model - # executor into its runtime feature and drives its fixture and cache trust boundary. - st2Wasip2ExecutorCheck = st2.overrideAttrs (old: { - pname = "st2-resource-wasip2-check"; - nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.lld ]; - cargoTestFlags = [ - "-p" - "st2-resource-wasip2" - "--features" - "runtime" - "--lib" - "--test" - "executor" + # One cargo invocation builds all four guest crates: they share the same wasm32 dependency + # graph, so a derivation per component compiled it four times. Install paths are unchanged + # and every component package attr points at this single output. + st2ProviderComponents = providerRustPlatform.buildRustPackage { + pname = "st2-provider-components"; + inherit version; + src = self; + cargoLock.lockFile = ./Cargo.lock; + buildPhase = '' + runHook preBuild + cargo build --offline --release --target wasm32-unknown-unknown \ + ${ + pkgs.lib.concatMapStringsSep " " (package: "-p ${package}") ( + pkgs.lib.attrNames providerComponentPackages + ) + } + runHook postBuild + ''; + doCheck = false; + nativeBuildInputs = [ + pkgs.lld + pkgs.wasm-tools ]; - }); - - buildProviderComponent = - { - package, - wasmName, - }: - providerRustPlatform.buildRustPackage { - pname = package; - inherit version; - src = self; - cargoLock.lockFile = ./Cargo.lock; - buildPhase = '' - runHook preBuild - cargo build --offline --release -p ${package} --target wasm32-unknown-unknown - runHook postBuild - ''; - doCheck = false; - nativeBuildInputs = [ - pkgs.lld - pkgs.wasm-tools - ]; - installPhase = '' - runHook preInstall - mkdir -p "$out/share/st2/providers" + installPhase = '' + runHook preInstall + mkdir -p "$out/share/st2/providers" + ${pkgs.lib.concatMapStringsSep "\n" (wasmName: '' wasm-tools component new \ "target/wasm32-unknown-unknown/release/${wasmName}.wasm" \ -o "$out/share/st2/providers/${wasmName}.component.wasm" - runHook postInstall - ''; - }; - - st2GitHubIssueComponent = buildProviderComponent { - package = "st2-github-issue-component"; - wasmName = "st2_github_issue_component"; - }; - - st2GitHubPrComponent = buildProviderComponent { - package = "st2-github-pr-component"; - wasmName = "st2_github_pr_component"; - }; - - st2PtyStatsComponent = buildProviderComponent { - package = "st2-pty-stats-component"; - wasmName = "st2_pty_stats_component"; + '') (pkgs.lib.attrValues providerComponentPackages)} + runHook postInstall + ''; }; - st2VistaComponent = buildProviderComponent { - package = "st2-vista-component"; - wasmName = "st2_vista_component"; - }; + providerComponentPath = + wasmName: "${st2ProviderComponents}/share/st2/providers/${wasmName}.component.wasm"; + # Production variant for catalogs whose resource profiles are WASIp2 components. + # + # Doubles as `checks.wasip2-resource-providers`: one compile of the runtime feature serves + # the provider/supervisor end-to-end targets and the Component Model executor's fixture and + # cache trust boundary. The default workspace remains covered by `checks.st2`. st2ProviderRuntime = st2.overrideAttrs (old: { pname = "st2-provider-runtime"; cargoBuildFeatures = (old.cargoBuildFeatures or [ ]) ++ [ "wasip2-provider-runtime" ]; - cargoCheckFeatures = (old.cargoCheckFeatures or [ ]) ++ [ "wasip2-provider-runtime" ]; + cargoCheckFeatures = [ ]; nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.lld pty.packages.${system}.default ]; - }); - - st2ProviderRuntimeCheck = st2ProviderRuntime.overrideAttrs (_: { - pname = "st2-provider-runtime-check"; - ST2_GITHUB_ISSUE_COMPONENT = "${st2GitHubIssueComponent}/share/st2/providers/st2_github_issue_component.component.wasm"; - ST2_GITHUB_PR_COMPONENT = "${st2GitHubPrComponent}/share/st2/providers/st2_github_pr_component.component.wasm"; - ST2_PTY_STATS_COMPONENT = "${st2PtyStatsComponent}/share/st2/providers/st2_pty_stats_component.component.wasm"; - ST2_VISTA_COMPONENT = "${st2VistaComponent}/share/st2/providers/st2_vista_component.component.wasm"; + ST2_GITHUB_ISSUE_COMPONENT = providerComponentPath "st2_github_issue_component"; + ST2_GITHUB_PR_COMPONENT = providerComponentPath "st2_github_pr_component"; + ST2_PTY_STATS_COMPONENT = providerComponentPath "st2_pty_stats_component"; + ST2_VISTA_COMPONENT = providerComponentPath "st2_vista_component"; cargoTestFlags = [ "-p" "st2-resource-providers" @@ -304,93 +301,86 @@ "github_issue_component" "--test" "github_pr_component" - "-p" - "st2" - "--features" - "st2/wasip2-provider-runtime" - "--test" - "resource_profile_supervisor_e2e" - "--test" - "resource_provider_e2e" - ]; - }); - - # Narrow sandbox-safe integration gate for the atomic snapshot boundary. The main package - # deliberately omits the broad doctor suite because some doctor cases exercise facilities - # unavailable in the Nix sandbox. A dedicated target containing exactly one test makes the - # gate structurally non-vacuous: a missing target is a cargo error, never a zero-match pass. - st2AtomicPtySnapshot = st2.overrideAttrs (_: { - pname = "st2-atomic-pty-snapshot-check"; - cargoTestFlags = [ - "--test" - "atomic_pty_snapshot" - ]; - }); - - # Bootstrap's crash/race tests use causal barrier hooks compiled only with debug - # assertions. Keep the package's release-mode test boundary unchanged and gate this - # transaction family in one explicit, non-vacuous derivation. - st2CatalogBootstrap = st2.overrideAttrs (_: { - pname = "st2-catalog-bootstrap-check"; - CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS = "true"; - cargoTestFlags = [ - "--test" - "catalog_apply" - "bootstrap_" - ]; - }); - - # Message CLI crash/recovery controls are compiled only with debug assertions. Keep the - # package's release-mode test boundary unchanged and gate the complete integration target - # in a dedicated derivation. - st2MessageCli = st2.overrideAttrs (_: { - pname = "st2-message-cli-check"; - CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS = "true"; - cargoTestFlags = [ - "--test" - "message_cli" ]; + postCheck = + extraCargoTest { + label = "wasip2 supervisor integration"; + flags = [ + "-p" + "st2" + "--features" + "wasip2-provider-runtime" + "--test" + "resource_profile_supervisor_e2e" + "--test" + "resource_provider_e2e" + ]; + } + + extraCargoTest { + label = "wasip2 resource executor"; + flags = [ + "-p" + "st2-resource-wasip2" + "--features" + "runtime" + "--lib" + "--test" + "executor" + ]; + }; }); - # The parked-task recovery episode against the real binary and real processes. It is a - # dedicated derivation because it needs a long-running supervisor: both single-pass entry - # points build a fresh `FlappingCap`, so `up --once` can never park anything, and the - # package's own test boundary would never reach this path. - st2ParkedRecovery = st2.overrideAttrs (old: { - pname = "st2-parked-recovery-check"; - # The supervisor snapshots pty sessions on every pass even for an exec-only catalog, so the - # real producer has to be on PATH — without it the pass refuses to reconcile at all and no - # task ever reaches the park this check exists to observe. + # Sandbox-safe integration episodes the package's own release-mode boundary cannot reach, + # sharing one default-feature build because they differ only by test selection: + # * `atomic_pty_snapshot` — the atomic snapshot boundary, split out of the broad doctor + # suite (some doctor cases need facilities the sandbox lacks). A target holding exactly + # one test makes the gate structurally non-vacuous: a missing target is a cargo error, + # never a zero-match pass. + # * `parked_recovery` — the parked-task recovery episode against real processes. Both + # single-pass entry points build a fresh `FlappingCap`, so `up --once` can never park + # anything and the package's boundary would never reach this path. + # * `otel_export` — OTLP span export. The test skips unless `ST2_OTELITE_BIN` points at a + # real collector; pinning `otelite` here and leaving `ST2_ALLOW_OTEL_SKIP` unset is what + # makes a broken export path fail instead of silently skipping. + st2ReleaseIntegration = st2.overrideAttrs (old: { + pname = "st2-release-integration-check"; + # The supervisor snapshots pty sessions on every pass even for an exec-only catalog, and + # both `st2 up --once` drivers shell out to `pty list --json`, so the real producer must + # be on PATH — without it no pass reconciles and no task ever reaches the park. nativeCheckInputs = (old.nativeCheckInputs or [ ]) ++ [ pty.packages.${system}.default + effect-utils.packages.${system}.otelite ]; + ST2_OTELITE_BIN = "${effect-utils.packages.${system}.otelite}/bin/otelite"; cargoTestFlags = [ + "--test" + "atomic_pty_snapshot" "--test" "parked_recovery" + "--test" + "otel_export" ]; }); - # OTLP export integration gate. `tests/otel_export.rs` is skipped - # unless `ST2_OTELITE_BIN` points at a real collector, so the package's - # own test boundary never exercises span export — this dedicated - # derivation is what makes the contract non-vacuous: it pins the exact - # `otelite` build from effect-utils and does NOT set - # `ST2_ALLOW_OTEL_SKIP`, so a broken export path fails the gate instead - # of silently skipping. - st2OtelExport = st2.overrideAttrs (old: { - pname = "st2-otel-export-check"; - # The test drives `st2 up --once`, whose reconcile pass shells out to - # `pty list --json` — the same real-producer requirement as - # st2ParkedRecovery, so the packaged pty must be on the check's PATH. - nativeCheckInputs = (old.nativeCheckInputs or [ ]) ++ [ - pty.packages.${system}.default - effect-utils.packages.${system}.otelite - ]; - ST2_OTELITE_BIN = "${effect-utils.packages.${system}.otelite}/bin/otelite"; + # Bootstrap's crash/race tests and the message CLI's crash/recovery controls are both + # compiled only with debug assertions, so they share one derivation. Keep the package's + # release-mode test boundary unchanged. `bootstrap_` is a positional name filter, so it + # needs its own invocation — in a shared one it would also filter `message_cli` to nothing. + st2DebugAssertions = st2.overrideAttrs (_: { + pname = "st2-debug-assertions-check"; + CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS = "true"; cargoTestFlags = [ "--test" - "otel_export" + "message_cli" ]; + postCheck = extraCargoTest { + label = "catalog bootstrap transactions"; + flags = [ + "--test" + "catalog_apply" + "bootstrap_" + ]; + }; }); hookSuccessorSource = pkgs.runCommand "st2-hook-successor-source" { } '' @@ -402,6 +392,10 @@ st2HookSuccessor = st2.overrideAttrs (_: { pname = "st2-hook-successor"; src = hookSuccessorSource; + # This build exists only to supply a second binary with different embedded hook bytes to + # `checks.hooks-replacement`. Its test suite is `checks.st2`'s, on source that differs + # only by an appended hook comment, so running it again buys nothing. + doCheck = false; CLI_BUILD_STAMP = builtins.toJSON { type = "nix"; inherit version; @@ -415,10 +409,11 @@ packages.st2 = st2; packages.st2-wasm-resolver = st2WasmResolver; packages.st2-provider-runtime = st2ProviderRuntime; - packages.st2-github-issue-component = st2GitHubIssueComponent; - packages.st2-github-pr-component = st2GitHubPrComponent; - packages.st2-pty-stats-component = st2PtyStatsComponent; - packages.st2-vista-component = st2VistaComponent; + # All four components come out of one build; the install paths are unchanged. + packages.st2-github-issue-component = st2ProviderComponents; + packages.st2-github-pr-component = st2ProviderComponents; + packages.st2-pty-stats-component = st2ProviderComponents; + packages.st2-vista-component = st2ProviderComponents; packages.default = st2; # `nix flake check` is the whole CI: it builds the package — which runs @@ -431,18 +426,11 @@ # commits on every rebase. The devShell ships rustfmt + clippy for whoever # wants them. checks.st2 = st2; - checks.atomic-pty-snapshot = st2AtomicPtySnapshot; - checks.catalog-bootstrap = st2CatalogBootstrap; - checks.message-cli = st2MessageCli; - checks.parked-recovery = st2ParkedRecovery; - checks.otel-export = st2OtelExport; - checks.wasm-resolver-feature = st2WasmResolverCheck; - checks.wasip2-resource-executor = st2Wasip2ExecutorCheck; - checks.wasip2-resource-providers = st2ProviderRuntimeCheck; - checks.github-issue-component = st2GitHubIssueComponent; - checks.github-pr-component = st2GitHubPrComponent; - checks.pty-stats-component = st2PtyStatsComponent; - checks.vista-component = st2VistaComponent; + checks.release-integration = st2ReleaseIntegration; + checks.debug-assertions = st2DebugAssertions; + checks.wasm-resolver-feature = st2WasmResolver; + checks.wasip2-resource-providers = st2ProviderRuntime; + checks.provider-components = st2ProviderComponents; # Exercise the shipped binary, not a cargo-side surrogate: its version entrypoint runs and # the same artifact strictly admits a catalog carrying a real wasm profile module. checks.wasm-resolver-artifact = pkgs.runCommand "st2-wasm-resolver-artifact-${version}" { } ''