From 8b8fc1ff8e0261c6ddfbbd12d398ceb4c79f952d Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Wed, 17 Jun 2026 17:23:28 +0000 Subject: [PATCH] =?UTF-8?q?claim(M1-nixenv):=20single-source=20harness=20r?= =?UTF-8?q?untime=20env=20=E2=80=94=20ccciPyEnv+ccciRuntimeTools+cc-ci-run?= =?UTF-8?q?=20in=20packages.nix,=20referenced=20by=20harness/sweep/both=20?= =?UTF-8?q?hosts;=20sweep=20execs=20cc-ci-run=20(no=20dup=20pyEnv,=20no=20?= =?UTF-8?q?DEFECT-3=20PATH=20patch);=20cc-ci=20host=20gains=20git-lfs+open?= =?UTF-8?q?ssl;=20both=20#cc-ci=20and=20#cc-ci-hetzner=20build;=20awaiting?= =?UTF-8?q?=20Adversary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- machine-docs/BACKLOG-nixenv.md | 19 +++++ machine-docs/JOURNAL-nixenv.md | 59 ++++++++++++++++ machine-docs/STATUS-nixenv.md | 65 +++++++++++++++++ nix/hosts/cc-ci-hetzner/configuration.nix | 14 ++-- nix/hosts/cc-ci/configuration.nix | 12 ++-- nix/modules/harness.nix | 25 +++---- nix/modules/nightly-sweep.nix | 26 +++---- nix/modules/packages.nix | 86 ++++++++++++++++++----- 8 files changed, 242 insertions(+), 64 deletions(-) create mode 100644 machine-docs/BACKLOG-nixenv.md create mode 100644 machine-docs/JOURNAL-nixenv.md create mode 100644 machine-docs/STATUS-nixenv.md diff --git a/machine-docs/BACKLOG-nixenv.md b/machine-docs/BACKLOG-nixenv.md new file mode 100644 index 0000000..edd7958 --- /dev/null +++ b/machine-docs/BACKLOG-nixenv.md @@ -0,0 +1,19 @@ +# BACKLOG — phase `nixenv` + +## Build backlog + +- [x] M1: define shared harness/recipe-test runtime env once (overlay in `packages.nix`): + `ccciPyEnv` + `ccciRuntimeTools` (the union tool set) + `cc-ci-run`. +- [x] M1: `harness.nix` references `pkgs.cc-ci-run` (no local pyEnv/runtimeInputs). +- [x] M1: `nightly-sweep.nix` invokes `cc-ci-run` (no duplicate pyEnv, no own tool list, DEFECT-3 patch gone). +- [x] M1: both host `configuration.nix` `systemPackages` reference `pkgs.ccciRuntimeTools` (+ openssh); end identical. +- [x] M1: grep proof — exactly one `withPackages`/`pytest playwright` in nix/ (packages.nix); no module declares its own harness tool list. +- [x] M1: `nixos-rebuild build` succeeds for both `#cc-ci` and `#cc-ci-hetzner`. +- [x] M1: CLAIM, await Adversary PASS. +- [ ] M2: deploy via `nixos-rebuild switch`; verify host health (systemctl --failed, oneshots, timer, endpoints). +- [ ] M2: live parity — gitea `test_lfs_roundtrip` green under BOTH Drone path and a real timer fire from the unified env. +- [ ] M2: canon-style sweep still promotes/SKIPs correctly (no regression). +- [ ] M2: CLAIM, await Adversary PASS → `## DONE`. + +## Adversary findings + diff --git a/machine-docs/JOURNAL-nixenv.md b/machine-docs/JOURNAL-nixenv.md new file mode 100644 index 0000000..d6a0b09 --- /dev/null +++ b/machine-docs/JOURNAL-nixenv.md @@ -0,0 +1,59 @@ +# JOURNAL — phase `nixenv` (Builder) + +## 2026-06-17 — M1: single-source the harness runtime env + +### Why this design +The phase plan §2 wants ONE definition of "what's needed to run a recipe test", referenced from +three places, so DEFECT-3 (a dep present for one path, missing for another) becomes structurally +impossible. I put the single source in `nix/modules/packages.nix` because it is the existing +"shared pkgs" overlay module already imported by both host configs — so `pkgs.ccciRuntimeTools` +and `pkgs.cc-ci-run` are reachable from every module/host without a fragile cross-module `let`. + +Three overlay defs: +- `ccciPyEnv` (let-bound, internal) — `python3.withPackages [pytest playwright]`, the ONLY pyEnv now. +- `ccciRuntimeTools` (overlay attr) — the union tool set. +- `cc-ci-run` (overlay attr) — `writeShellApplication` with `runtimeInputs = [ccciPyEnv] ++ ccciRuntimeTools`. + +Consumers: +- `harness.nix` → `environment.systemPackages = [ pkgs.cc-ci-run ]` (installs the entrypoint). +- `nightly-sweep.nix` → wrapper execs `cc-ci-run` (same binary the Drone pipeline runs), so pyEnv + + tooling + PLAYWRIGHT env are identical to the Drone path by construction. Dropped: the duplicate + pyEnv, the parallel `runtimeInputs` tool list, and the DEFECT-3 `export PATH=/run/current-system/sw/bin…` + prepend — git-lfs/bash/util-linux/openssl now come from cc-ci-run's runtimeInputs. +- both host `configuration.nix` → `systemPackages = pkgs.ccciRuntimeTools ++ [ pkgs.openssh ]`. + +### Why the union is a superset (nothing dropped) +- old cc-ci-run: `abra docker git coreutils util-linux` ⊂ set. +- old sweep: `bash abra docker git curl jq gnused gnugrep gnutar coreutils util-linux procps` ⊂ set; + its host-PATH-derived git-lfs/openssl are now EXPLICIT in the set. +- old host PATH: `curl git jq` (+ git-lfs on hetzner only) ⊂ set; `openssh` kept as host-only add. +- pyEnv (python3+pytest+playwright) + playwright browsers (via PLAYWRIGHT_BROWSERS_PATH) preserved. +Additions vs any single prior list: `git-lfs`, `openssl` (plan §2). The `cc-ci` host GAINS git-lfs, +killing the one-off hetzner-only divergence — both host configs now byte-identical. + +### Why writeShellApplication makes this work +`writeShellApplication` emits `export PATH=":$PATH"` (confirmed on the live wrapper). +So cc-ci-run's full tool set is the PATH *prefix* regardless of caller. Under Drone the inherited +suffix is `/run/current-system/sw/bin:/run/wrappers/bin`; under the sweep it's the systemd-minimal +PATH — but the harness tools all resolve from the shared prefix either way, which is the parity the +plan wants. The host `systemPackages` reference is the belt-and-suspenders path for direct +`.drone.yml` shell-outs (`abra --version`, `docker info`) that don't go through cc-ci-run. + +### buildEnv collision watch (resolved) +Worry: adding coreutils/util-linux/procps/bash/gnu* to host `systemPackages` could collide with the +NixOS base `requiredPackages`. It did not — base requiredPackages are `lowPrio`, so the normal-prio +additions override cleanly. Both `#cc-ci` and `#cc-ci-hetzner` built with no collision error. + +### Note on other modules' tool lists +`backupbot/docker-prune/drone/proxy/warm-keycloak.nix` still list gnused/gnugrep/etc. in their OWN +`runtimeInputs` — those are independent reconcile-service scripts, never part of the harness/recipe +-test env, never part of the DEFECT-3 divergence. Single-sourcing is scoped to the harness env +(pyEnv + recipe-test tooling consumed by cc-ci-run / sweep / host PATH), which is now packages.nix only. + +### Verification (local, dirty tree needs `?submodules=1` — `secrets/` is a submodule) +- `nixos-rebuild build --flake '.?submodules=1#cc-ci-hetzner'` → built `nixos-system-…dhmpm232…`. +- `nixos-rebuild build --flake '.?submodules=1#cc-ci'` → built OK. +- cc-ci-run store `zxlx9jnylh7la5m48bsqb1wfm5l9r0bd`; PATH carries all 15 tools incl git-lfs-3.6.1 + openssl-3.3.3. +- sweep wrapper `gh02w1kc…` execs the SAME `zxlx9j…/bin/cc-ci-run`. +- cc-ci host sw/bin now lists git-lfs + openssl (was missing git-lfs pre-refactor). +- `grep -rn withPackages nix/` → 1 hit (packages.nix:17). diff --git a/machine-docs/STATUS-nixenv.md b/machine-docs/STATUS-nixenv.md new file mode 100644 index 0000000..ac5812a --- /dev/null +++ b/machine-docs/STATUS-nixenv.md @@ -0,0 +1,65 @@ +# STATUS — phase `nixenv` (Builder) + +Phase plan: `/srv/cc-ci/cc-ci-plan/plan-phase-nixenv-shared-runtime-env.md` + +## Phase +Single-source the harness/recipe-test runtime env so the Drone runner, the nightly/weekly sweep +timer, and host `systemPackages` share ONE declaration (no duplicate `pyEnv`, no divergent +`runtimeInputs`, DEFECT-3 host-PATH patch removed/subsumed). + +## Gate: M1 — CLAIMED, awaiting Adversary + +**WHAT (M1 DoD).** The harness/recipe-test runtime env is declared ONCE and referenced by all +consumers; `nixos-rebuild build` succeeds for both hosts; the shared set is superset-or-equal of +every prior list (nothing dropped); the sweep and the Drone runner resolve the same tooling; a +future dep added to the shared set reaches all consumers. + +**WHERE (inputs).** All changes at the tip of `main` (commit pushed with this claim). +- Single source: `nix/modules/packages.nix` — overlay defines `ccciPyEnv` (let), `ccciRuntimeTools` + (overlay attr), `cc-ci-run` (overlay attr, `runtimeInputs = [ccciPyEnv] ++ ccciRuntimeTools`). +- Consumers: `nix/modules/harness.nix` (`systemPackages = [ pkgs.cc-ci-run ]`), + `nix/modules/nightly-sweep.nix` (wrapper execs `cc-ci-run`), + `nix/hosts/cc-ci/configuration.nix` + `nix/hosts/cc-ci-hetzner/configuration.nix` + (`systemPackages = pkgs.ccciRuntimeTools ++ [ pkgs.openssh ]`). +- `nix/modules/drone-runner.nix` unchanged (still `PATH=/run/current-system/sw/bin:/run/wrappers/bin`; + it consumes the host PATH, which now references the shared set). + +**HOW + EXPECTED (cold-verifiable; `secrets/` is a git submodule → use `?submodules=1` for a dirty +tree, or build from a `git clone --recursive`).** + +1. Builds succeed (both hosts): + - `nixos-rebuild build --flake '.?submodules=1#cc-ci-hetzner'` → builds + `nixos-system-nixos-24.11.…` (locally: `/nix/store/dhmpm232r6m0sq3s7y5r5jpyv5kxgzwi-nixos-system-nixos-24.11.20250630.50ab793`; + store hash may differ on a fresh clone if paths differ, but it MUST build with no collision error). + - `nixos-rebuild build --flake '.?submodules=1#cc-ci'` → builds OK (no collision error). + +2. Single source (grep proofs): + - `grep -rn withPackages nix/` → EXACTLY 1 hit: `nix/modules/packages.nix` (`ccciPyEnv`). + - `grep -rn "pytest playwright" nix/` → EXACTLY 1 hit: same line. (No duplicate pyEnv.) + - `grep -rn ccciRuntimeTools nix/` → defined once (packages.nix), referenced by both host configs. + - `nightly-sweep.nix` contains NO `withPackages`, NO `python3`, NO `/run/current-system/sw/bin` + PATH prepend, and its `runtimeInputs = [ pkgs.cc-ci-run ]` only; it `exec cc-ci-run …`. + +3. Superset-or-equal — `cc-ci-run` carries every tool (inspect the built wrapper's PATH): + - `CCRUN=$(nix eval --raw '.?submodules=1#nixosConfigurations.cc-ci-hetzner.pkgs.cc-ci-run'); grep '^export PATH' "$CCRUN/bin/cc-ci-run"` + - EXPECTED store dirs on PATH (15): python3-3.12.8-env, abra-0.13.0-beta, docker-27.5.1, + git-2.47.2, **git-lfs-3.6.1**, bash-5.2p37, coreutils-9.5, util-linux-2.39.4, curl-8.12.1, + jq-1.7.1, gnused-4.9, gnugrep-3.11, gnutar-1.35, **openssl-3.3.3**, procps-4.0.4. + - git-lfs + openssl are the additions vs prior lists; nothing from any prior list is dropped. + +4. Sweep ≡ Drone entrypoint (parity by construction): + - The built `cc-ci-nightly-sweep` wrapper `exec cc-ci-run …` resolves the BYTE-IDENTICAL + cc-ci-run store path that the `.drone.yml` `cc-ci-run runner/run_recipe_ci.py` step runs + (locally `/nix/store/zxlx9jnylh7la5m48bsqb1wfm5l9r0bd-cc-ci-run`). Same store path ⇒ same + pyEnv, same tooling, same PLAYWRIGHT_BROWSERS_PATH. + +5. Host divergence removed: + - Both host `configuration.nix` `systemPackages` lines are textually identical + (`pkgs.ccciRuntimeTools ++ [ pkgs.openssh ]`). The `cc-ci` host now GAINS `git-lfs`+`openssl` + on its system PATH (`ls $(nix eval --raw '.?submodules=1#nixosConfigurations.cc-ci.config.system.build.toplevel')/sw/bin/ | grep -E '^(git-lfs|openssl)$'` → both present; pre-refactor cc-ci lacked git-lfs). + +6. Future-dep propagation: adding a pkg to `ccciRuntimeTools` in packages.nix lands in cc-ci-run's + runtimeInputs (Drone + sweep) AND both hosts' systemPackages from the single edit. + +## Build backlog +See `BACKLOG-nixenv.md`. M2 (deploy + live parity witness) is gated behind the M1 PASS. diff --git a/nix/hosts/cc-ci-hetzner/configuration.nix b/nix/hosts/cc-ci-hetzner/configuration.nix index 0eaa9f3..8c9c2b2 100644 --- a/nix/hosts/cc-ci-hetzner/configuration.nix +++ b/nix/hosts/cc-ci-hetzner/configuration.nix @@ -63,13 +63,13 @@ allowedTCPPorts = [ 22 80 443 ]; }; - environment.systemPackages = with pkgs; [ - curl - git - git-lfs - jq - openssh - ]; + # Phase `nixenv`: the Drone exec runner resolves recipe shell-outs from this host PATH + # (PATH=/run/current-system/sw/bin). Reference the SINGLE shared harness tool set + # (pkgs.ccciRuntimeTools — includes git-lfs, openssl, etc.) instead of a hand-maintained list, + # so the Drone path and the harness env (cc-ci-run / sweep) can never diverge. `openssh` is a + # host-only addition (ssh client), not part of the recipe-test tool set. Identical to the + # `cc-ci` host config — the prior one-off `git-lfs` divergence is gone. + environment.systemPackages = pkgs.ccciRuntimeTools ++ [ pkgs.openssh ]; nix.settings.experimental-features = [ "nix-command" "flakes" ]; diff --git a/nix/hosts/cc-ci/configuration.nix b/nix/hosts/cc-ci/configuration.nix index 90eeb44..7239269 100644 --- a/nix/hosts/cc-ci/configuration.nix +++ b/nix/hosts/cc-ci/configuration.nix @@ -53,12 +53,12 @@ allowedTCPPorts = [ 22 ]; }; - environment.systemPackages = with pkgs; [ - curl - git - jq - openssh - ]; + # Phase `nixenv`: the Drone exec runner resolves recipe shell-outs from this host PATH + # (PATH=/run/current-system/sw/bin). Reference the SINGLE shared harness tool set + # (pkgs.ccciRuntimeTools — includes git-lfs, openssl, etc.) instead of a hand-maintained list, + # so the Drone path and the harness env (cc-ci-run / sweep) can never diverge. `openssh` is a + # host-only addition (ssh client), not part of the recipe-test tool set. + environment.systemPackages = pkgs.ccciRuntimeTools ++ [ pkgs.openssh ]; nix.settings.experimental-features = [ "nix-command" "flakes" ]; diff --git a/nix/modules/harness.nix b/nix/modules/harness.nix index 20c148a..5e0cc82 100644 --- a/nix/modules/harness.nix +++ b/nix/modules/harness.nix @@ -1,20 +1,11 @@ -# CI harness runtime (M4): a reproducible Python env with pytest + Playwright and the -# Nix-provided browsers, exposed as `cc-ci-run` on the host so the Drone exec pipeline (and -# manual dev) can run the harness with `cc-ci-run runner/run_recipe_ci.py`. Playwright on NixOS -# needs the browsers from nixpkgs (not a downloaded copy) via PLAYWRIGHT_BROWSERS_PATH. +# CI harness runtime (M4): `cc-ci-run` exposes a reproducible Python env (pytest + Playwright, +# Nix-provided browsers) plus the recipe-test tooling, so the Drone exec pipeline (and manual dev) +# can run the harness with `cc-ci-run runner/run_recipe_ci.py`. +# +# Phase `nixenv`: `cc-ci-run` (and the env it carries) is now defined ONCE in modules/packages.nix +# as `pkgs.cc-ci-run` — the SAME definition the nightly sweep execs and the same tool set the host +# `systemPackages` reference. This module just installs it on the host. { pkgs, ... }: -let - pyEnv = pkgs.python3.withPackages (ps: with ps; [ pytest playwright ]); - ccciRun = pkgs.writeShellApplication { - name = "cc-ci-run"; - runtimeInputs = [ pyEnv pkgs.abra pkgs.docker pkgs.git pkgs.coreutils pkgs.util-linux ]; - text = '' - export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers} - export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 - exec ${pyEnv}/bin/python3 "$@" - ''; - }; -in { - environment.systemPackages = [ ccciRun ]; + environment.systemPackages = [ pkgs.cc-ci-run ]; } diff --git a/nix/modules/nightly-sweep.nix b/nix/modules/nightly-sweep.nix index 17fe2b8..0a2b9bd 100644 --- a/nix/modules/nightly-sweep.nix +++ b/nix/modules/nightly-sweep.nix @@ -13,30 +13,22 @@ # and lets sweep-logic changes ship via a checkout pull without a store rebuild. { pkgs, ... }: let - # The sweep drives run_recipe_ci.py (pytest/playwright) — needs the full harness env like cc-ci-run. - pyEnv = pkgs.python3.withPackages (ps: with ps; [ pytest playwright ]); + # Phase `nixenv`: the sweep drives nightly_sweep.py (pytest/playwright) through the SAME + # entrypoint the Drone runner uses — `cc-ci-run` (pkgs.cc-ci-run, defined once in packages.nix). + # So the python env + recipe-test tooling are IDENTICAL to the Drone path by construction: no + # duplicate pyEnv, no parallel runtimeInputs list, and no host-PATH-prepend patch (the old + # DEFECT-3 fix) — git-lfs/bash/util-linux/openssl/etc. now come from cc-ci-run's runtimeInputs, + # which is the single shared `ccciRuntimeTools`. This wrapper only sets the sweep-specific + # environment (HOME, the deployed-checkout repo) before handing off. sweep = pkgs.writeShellApplication { name = "cc-ci-nightly-sweep"; - # util-linux provides `script` (abra's PTY wrapper for backup/restore TTY ops) — same as cc-ci-run. - # bash: the sweep's mirror_sync shells out to `bash scripts/recipe-mirror-sync.sh`; writeShellApplication - # sets a clean PATH limited to runtimeInputs, so bash must be listed (a real timer fire caught its - # absence — manual ssh runs had bash on PATH and masked it). - runtimeInputs = with pkgs; [ bash abra docker git curl jq gnused gnugrep gnutar coreutils util-linux procps ]; + runtimeInputs = [ pkgs.cc-ci-run ]; text = '' export HOME=/root - export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers} - export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 - # ENV PARITY with the Drone recipe-CI runner (canon DEFECT-3): the recipes + their tests shell - # out to host tooling (git-lfs for gitea, openssl, etc.). Drone's exec runner runs them with - # PATH=/run/current-system/sw/bin:/run/wrappers/bin; writeShellApplication otherwise gives a - # clean nix-only PATH, so the timer sweep silently lacked tools the recipes assume (a real fire - # caught git-lfs + bash gaps that manual ssh runs, with a login PATH, masked). Prepend the host - # system PATH so the sweep validates recipes in the SAME environment Drone does. - export PATH="/run/current-system/sw/bin:/run/wrappers/bin:$PATH" # canon M1.4: read enrollment + run the harness from the deployed checkout (has tests/). export CCCI_REPO=/etc/cc-ci cd "$CCCI_REPO" - exec ${pyEnv}/bin/python3 "$CCCI_REPO/runner/nightly_sweep.py" + exec cc-ci-run "$CCCI_REPO/runner/nightly_sweep.py" ''; }; in diff --git a/nix/modules/packages.nix b/nix/modules/packages.nix index 2ac420d..2b4d613 100644 --- a/nix/modules/packages.nix +++ b/nix/modules/packages.nix @@ -1,25 +1,77 @@ # Project package overlay. `abra` (the Co-op Cloud CLI) is exposed as `pkgs.abra` so every # module (systemPackages, the proxy/drone reconcile oneshots) can use the same pinned build. +# +# Phase `nixenv` — SINGLE SOURCE OF TRUTH for the harness/recipe-test runtime env. The Drone +# runner entrypoint (`cc-ci-run`), the nightly/weekly sweep timer, and host `systemPackages` ALL +# reference the definitions here, so a dependency can never be present for one path and missing +# for another (that was DEFECT-3, caught in `canon`: the sweep's tool list had drifted from what +# the Drone path provided). Adding the next dependency to `ccciRuntimeTools` propagates atomically +# to every consumer. _: { nixpkgs.overlays = [ - (_: prev: { - abra = prev.stdenv.mkDerivation rec { - pname = "abra"; - version = "0.13.0-beta"; - src = prev.fetchurl { - url = "https://git.coopcloud.tech/toolshed/abra/releases/download/${version}/abra_${version}_linux_amd64.tar.gz"; - sha256 = "12csk6wp1pk9cspzqfl4a6h5jdz8p055sf0ggxw9k7ljhpd5qvc6"; + (final: prev: + let + # The harness drives run_recipe_ci.py / nightly_sweep.py (pytest + Playwright). Defined + # ONCE here; consumed only via `cc-ci-run` below (no module builds its own pyEnv anymore). + ccciPyEnv = final.python3.withPackages (ps: with ps; [ pytest playwright ]); + in + { + abra = prev.stdenv.mkDerivation rec { + pname = "abra"; + version = "0.13.0-beta"; + src = prev.fetchurl { + url = "https://git.coopcloud.tech/toolshed/abra/releases/download/${version}/abra_${version}_linux_amd64.tar.gz"; + sha256 = "12csk6wp1pk9cspzqfl4a6h5jdz8p055sf0ggxw9k7ljhpd5qvc6"; + }; + sourceRoot = "."; + nativeBuildInputs = [ prev.autoPatchelfHook ]; + buildInputs = [ prev.stdenv.cc.cc.lib ]; + installPhase = '' + runHook preInstall + install -Dm755 abra "$out/bin/abra" + runHook postInstall + ''; }; - sourceRoot = "."; - nativeBuildInputs = [ prev.autoPatchelfHook ]; - buildInputs = [ prev.stdenv.cc.cc.lib ]; - installPhase = '' - runHook preInstall - install -Dm755 abra "$out/bin/abra" - runHook postInstall - ''; - }; - }) + + # === Single source of truth: the recipe-test runtime tooling === + # The union of everything a recipe test + the harness shell out to. This is a + # superset-or-equal of every prior list (cc-ci-run's old runtimeInputs, the sweep's old + # runtimeInputs, and the host PATH's hand-maintained `curl git git-lfs jq`): + # abra docker git coreutils util-linux (old cc-ci-run) + # + bash curl jq gnused gnugrep gnutar procps (old sweep) + # + git-lfs openssl (formerly only on the host PATH / plan §2) + # `util-linux` provides `script` (abra's PTY wrapper for backup/restore TTY ops). + ccciRuntimeTools = with final; [ + abra + docker + git + git-lfs + bash + coreutils + util-linux + curl + jq + gnused + gnugrep + gnutar + openssl + procps + ]; + + # === The harness entrypoint, used by BOTH the Drone exec pipeline (.drone.yml: + # `cc-ci-run runner/run_recipe_ci.py`) and the nightly sweep (which execs this same + # binary). Carries the full tool set in runtimeInputs so the recipe shell-outs resolve + # the same tooling on every path, independent of the caller's inherited PATH. === + cc-ci-run = final.writeShellApplication { + name = "cc-ci-run"; + runtimeInputs = [ ccciPyEnv ] ++ final.ccciRuntimeTools; + text = '' + export PLAYWRIGHT_BROWSERS_PATH=${final.playwright-driver.browsers} + export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 + exec ${ccciPyEnv}/bin/python3 "$@" + ''; + }; + }) ]; }