From b5d99086dc02ab5f13b49c1c42715af005890374 Mon Sep 17 00:00:00 2001 From: Long Ho Date: Sun, 13 Sep 2026 20:56:00 +0000 Subject: [PATCH 1/7] feat: replace VRT Testcontainers execution with actiond BREAKING CHANGE: VRT requires a declared browser_runtime and an actiond Linux worker. External network origins and Docker image manifests are removed. --- .github/workflows/ci.yaml | 35 - .gitignore | 2 + README.md | 23 +- docs/README.md | 4 +- docs/actiond-evaluation.md | 2 + docs/actiond-migration.md | 10 +- docs/actiond.md | 68 ++ docs/api.md | 117 +- docs/architecture.md | 54 +- docs/component-vrt.md | 39 +- docs/dependencies.md | 5 +- docs/e2e.md | 13 +- docs/getting-started.md | 16 +- docs/host-browsers.md | 115 +- docs/oss-browser-testing-plan.md | 6 +- docs/testcontainers-vrt.md | 93 +- docs/visual-testing-design.md | 12 +- examples/react/BUILD.bazel | 39 +- examples/react/isolation.spec.ts | 7 +- experiments/actiond/cancel-public.py | 39 + experiments/actiond/prepare-public.mjs | 9 + experiments/actiond/run-public-actiond.sh | 4 + internal/browser.bzl | 25 +- package.json | 1 - patches/BUILD.bazel | 1 - patches/README.md | 23 - patches/testcontainers-12.1.0.patch | 110 -- playwright/defs.bzl | 34 +- pnpm-lock.yaml | 1183 +-------------------- pnpm-workspace.yaml | 6 - runtime/BUILD.bazel | 62 +- runtime/capture-browser.test.ts | 30 +- runtime/config.test.ts | 12 +- runtime/config.ts | 23 +- runtime/container-host.ts | 22 - runtime/container.ts | 184 ---- runtime/fixtures/docker-proxy.ts | 120 --- runtime/fixtures/preload-worker.ts | 36 - runtime/images.test.ts | 36 - runtime/isolation.test.ts | 19 - runtime/network.test.ts | 32 +- runtime/network.ts | 29 - runtime/preload-browser.test.ts | 89 -- runtime/preload-lifecycle.test.ts | 73 -- runtime/relay.ts | 13 - runtime/remote-result-entry.ts | 5 +- runtime/runner.ts | 35 +- runtime/suite-config.ts | 4 +- tests/preloaded-vrt.sh | 49 - 49 files changed, 323 insertions(+), 2645 deletions(-) create mode 100644 docs/actiond.md create mode 100644 experiments/actiond/cancel-public.py delete mode 100644 patches/BUILD.bazel delete mode 100644 patches/README.md delete mode 100644 patches/testcontainers-12.1.0.patch delete mode 100644 runtime/container-host.ts delete mode 100644 runtime/container.ts delete mode 100644 runtime/fixtures/docker-proxy.ts delete mode 100644 runtime/fixtures/preload-worker.ts delete mode 100644 runtime/images.test.ts delete mode 100644 runtime/preload-browser.test.ts delete mode 100644 runtime/preload-lifecycle.test.ts delete mode 100644 runtime/relay.ts delete mode 100644 tests/preloaded-vrt.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d61486e..cc107c3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -64,40 +64,5 @@ jobs: - name: Run host browser suites without Docker working-directory: examples/react run: bazelisk test //:e2e_test //:component_test //:remote_integration_test //:native_config_test --test_output=errors - - component-vrt: - name: VRT tests (Linux amd64) - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 - with: - bazelisk-cache: true - repository-cache: true - - name: Preload declared images and verify credential-free browser startup - run: | - bazelisk build //runtime:images - manifest="$(bazelisk info bazel-bin)/runtime/images.json" - jq -r '.images[] | [.image, (.platform // "")] | @tsv' "$manifest" | - while IFS="$(printf '\t')" read -r image platform; do - if [ -n "$platform" ]; then - docker pull --platform "$platform" "$image" - else - docker pull "$image" - fi - done - bazelisk test //runtime:preload_browser_test --test_output=errors - - name: Verify VRT with only declared tools and Docker - run: bash tests/preloaded-vrt.sh - name: Verify generated capture bounds and pixels run: bazelisk test //runtime:capture_browser_test --test_output=errors - - name: Run visual tests and container isolation coverage - working-directory: examples/react - run: bazelisk test //:visual_test //:component_visual_test //:native_visual_test //:network_isolation_test --test_output=errors - - name: Upload visual test artifacts - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: browser-tests - path: examples/react/bazel-testlogs/ diff --git a/.gitignore b/.gitignore index 854ae1e..9a7bc34 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ rules_web_e2e-*.tar.gz release_notes.txt .playwright-browsers/ + +examples/react/runtime.tar diff --git a/README.md b/README.md index 1c0ce9b..6786235 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Bazel rules for Playwright E2E, component browser tests, and visual regression testing (VRT). E2E and component tests use host Playwright browsers; only VRT -uses Testcontainers and a pinned Linux Chromium image. +runs in isolated actiond Linux actions with caller-owned browser runtimes. Bring compiled tests, your own server, or a built shell/template. Your build owns typechecking and bundling; the rules own execution and baseline updates. @@ -33,6 +33,7 @@ component_browser_test( component_visual_test( name = "editor_vrt", + browser = ":linux_browser", # browser_runtime; see docs/browser-runtime.md shell = ":editor_shell", matching = ":matching", baselines = glob(["screenshots/*.png"], allow_empty = True), @@ -47,7 +48,8 @@ component_visual_test( | `server` | Compiled adapter returning a ready URL and cleanup callback | | `shell` | Built HTML/JS/CSS directory plus its entry point; served without a bundler | | `base_url` / `base_url_env` | Existing application endpoint, replacing `server` or `shell` | -| `playwright` | Optional reusable runtime target grouping client packages and a pinned image; minimum 1.63.0 | +| `playwright` | Optional reusable runtime target grouping client packages; minimum 1.63.0 | +| `browser` | Required VRT runtime containing declared Linux Chromium, Node, libraries, and fonts | | `matching` | Compiled VRT comparison policy; render settings stay in `.visual.tsx` | For example, compile this `matching.ts` module: @@ -74,21 +76,22 @@ attributes; see [migration](docs/getting-started.md#migrating-from-100). ## Try it -Install Bazelisk and provision host Chromium first (with the locked Playwright version). -Start Docker and [preload the pinned images](docs/api.md#vrt-image-manifest-and-ci-preloading) -for VRT. From this checkout: +Provision host Chromium for interaction tests. For VRT, supply the example's +`runtime.tar` from your image build and configure a patched actiond worker using +[the execution guide](docs/actiond.md). ```sh export PLAYWRIGHT_BROWSERS_PATH="$(pwd)/.playwright-browsers" pnpm exec playwright install chromium cd examples/react -bazelisk test //:e2e_test //:component_test //:component_visual_test -bazelisk run //:component_visual_test.update +bazelisk test //:e2e_test //:component_test +bazelisk test --config=vrt //:component_visual_test +bazelisk run --config=vrt //:component_visual_test.update ``` -Review PNG changes before committing. Browser targets are manual, local, and -uncached; select them explicitly in CI. Screenshot baselines are validated on -Linux amd64. Each VRT target owns a separate baseline directory. +Review PNG changes before committing. Each VRT target owns its baseline directory. +VRT capture/comparison actions are cacheable; baseline application stays local. +Host browser targets remain manual, local, and uncached. See [development and releases](docs/development.md) for build checks, hooks, and BCR publishing. diff --git a/docs/README.md b/docs/README.md index 4ae715f..2394950 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,10 +17,10 @@ | Document | Read it for | | --------------------------------------------------------- | ------------------------------------------------------------- | | [Architecture](architecture.md) | Ownership, input staging, and runtime boundaries | -| [Testcontainers and VRT stability](testcontainers-vrt.md) | Container lifecycle, reproducibility, and isolation limits | +| [VRT on actiond](actiond.md) | Worker setup, declared runtimes, and isolation limits | | [Visual testing design](visual-testing-design.md) | Shared visual modules, capture phases, and baseline ownership | | [Scope and roadmap](oss-browser-testing-plan.md) | Implemented capabilities and possible extensions | | [Development and releases](development.md) | Repository checks, hooks, and release automation | See [host browser provisioning and AGI/FormatJS migration](host-browsers.md) for -the VRT-only Testcontainers execution model. +host provisioning and the VRT actiond migration. diff --git a/docs/actiond-evaluation.md b/docs/actiond-evaluation.md index 5d1a50a..4c90853 100644 --- a/docs/actiond-evaluation.md +++ b/docs/actiond-evaluation.md @@ -1,5 +1,7 @@ # actiond evaluation for VRT +Historical evaluation; the implemented path is described in [VRT on actiond](actiond.md). + Source review on 2026-09-13 at upstream commit [`8a42c3d`](https://github.com/hermeticbuild/actiond/tree/8a42c3d481df3a1bf1b80e95a9bb991a207fc035). The initial review below is followed by a [runnable prototype](../experiments/actiond/README.md). diff --git a/docs/actiond-migration.md b/docs/actiond-migration.md index 6e15930..53c4dee 100644 --- a/docs/actiond-migration.md +++ b/docs/actiond-migration.md @@ -2,7 +2,8 @@ Active implementation plan. The existing Chromium prototype in PR #27 passes through actiond's Linux amd64 VM after applying `actiond-advice.patch` (upstream -actiond PR #48). Production VRT still uses Testcontainers. +actiond PR #48). This branch replaces the VRT backend with actiond; final +validation of the replacement remains in progress. ## Execution contract @@ -87,7 +88,10 @@ mark the goal complete based solely on the standalone prototype passing. and the real OCI-built runtime matches the archive-built browser, Node, and fixture font. The CI example now constructs and passes an OCI image target. -Next: verify the full public-rule workflow in the VM, migrate concrete -AGI/FormatJS callsites, and remove the legacy backend. Baseline application, +The legacy backend and dependencies are now removed. The actual FormatJS +editor gallery captures all eight screenshots under process isolation, and host +E2E/component suites still pass. The next VM run includes deadline coverage and +a larger worker after a 3 GiB guest ran out of memory with three parallel suites. +Next: finish full VM failure/cancellation validation and consumer migration evidence. Baseline application, failures, cancellation, and isolation still need end-to-end VM coverage through the public rules. OCI support itself does not complete the migration. diff --git a/docs/actiond.md b/docs/actiond.md new file mode 100644 index 0000000..b511889 --- /dev/null +++ b/docs/actiond.md @@ -0,0 +1,68 @@ +# VRT on actiond + +`visual_test` and `component_visual_test` run the fixture server, Playwright, +Chromium, and screenshot comparison together in an isolated Linux amd64 action. +Callers supply a [declared browser runtime](browser-runtime.md), optionally built +from their own OCI image. Host E2E and component tests use host browsers. + +## Worker and Bazel configuration + +The worker currently needs the memory-advice and declared-rootfs patches in +[`experiments/actiond`](../experiments/actiond). The memory-advice change is +[upstream PR #48](https://github.com/hermeticbuild/actiond/pull/48); the rootfs +patch is maintained locally for upstreaming. The production workflow builds and +runs that exact patched worker. Linux VM workers require KVM and vhost-vsock. + +With a patched worker listening on `127.0.0.1:8980`, put this in the consumer's +Bazel configuration: + +```text +build:vrt --jobs=2 +build:vrt --remote_executor=grpc://127.0.0.1:8980 +build:vrt --remote_cache=grpc://127.0.0.1:8980 +build:vrt --spawn_strategy=sandboxed,local +build:vrt --strategy=VrtCapture=remote +build:vrt --strategy=VrtCompare=remote +build:vrt --remote_local_fallback=false +build:vrt --remote_upload_local_results=false +build:vrt --noremote_cache_compression +build:vrt --remote_download_outputs=all +build:vrt --extra_execution_platforms=@platforms//host:host,@rules_web_e2e//internal:linux_amd64 +``` + +Use a remote worker address when appropriate. ARM64 clients need an amd64 worker +for these baselines. The validated CI worker uses 6 GiB RAM for at most two +concurrent actions; size workers for fixture and staging memory as well as +Chromium. The macOS native VM backend is not yet validated by these +checks. Local fallback must remain disabled; `/workspace` runtime executables +are meaningful inside the worker's declared rootfs. + +```sh +bazel test --config=vrt //path:visual_test +bazel run --config=vrt //path:visual_test.update +``` + +Comparison and capture are cacheable build actions. Their result directory +contains test status and artifacts even when the suite fails. A local test +wrapper reports comparison failure and copies reports into Bazel test outputs. +The local update wrapper applies successful, nonempty captures to the source +baseline directory. Failed, timed-out, and empty captures preserve references. +A cancelled build never runs the local update wrapper. + +## Declared inputs and isolation + +The runtime tree contains Node, Chromium, loaders, libraries, fonts, and shell +commands needed by fixtures. OCI extraction verifies declared blobs and never +contacts a registry. Acquisition and image construction happen before execution; +Docker, registry credentials, Testcontainers, and Ryuk are absent from the action. + +The action has loopback-only networking. Start fixture services inside it using +`server` or native Playwright `webServer`, and declare their files in `data`. +External assets and APIs need local fixtures. Live deployed checks belong in +host E2E targets. Explicit `env` is supported, including `$(rootpath ...)` inside +JSON strings; inherited environment and network origin exceptions are rejected. + +Pin the runtime image, architecture, browser/client versions, fonts, fixtures, +locale, and timezone. This makes rendering inputs controlled; tests must still +control time, randomness, animations, and their own application state. The VM +provides isolation, not a claim that arbitrary screenshot tests are deterministic. diff --git a/docs/api.md b/docs/api.md index 7430380..a938b83 100644 --- a/docs/api.md +++ b/docs/api.md @@ -19,17 +19,17 @@ load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test", "visual_test") | `server` | Unset | Target supplying one compiled default `ServerAdapter` export, with its runtime dependencies/data | | `shell` | Unset | `browser_shell` target supplying built HTML, JavaScript, CSS, and other assets | | `base_url` | Unset | Existing HTTP(S) app URL; caller owns readiness and lifetime | -| `base_url_env` | Unset | Environment variable holding an existing URL; explicitly inherited | +| `base_url_env` | Unset | Environment variable holding a URL; VRT requires its value in `env` | | `playwright` | Rules' pinned runtime | Reusable `playwright_runtime` target | +| `browser` | Required for VRT | Caller-owned `browser_runtime` with Linux amd64 Chromium, Node, libraries, and fonts | +| `target_platform` | Rules' Linux amd64 platform | VRT input platform; override for caller native ABI constraints | | `config` | Generated by the runner | Optional compiled ESM module exporting native Playwright configuration | | `data` | `[]` | Additional declared runtime files | -| `env`, `env_inherit` | `{}`, `[]` | Explicit values and inherited variable names | -| `network_origins_env` | `[]` | VRT-only variable names containing optional extra HTTP(S) origins; unset values add nothing | +| `env`, `env_inherit` | `{}`, `[]` | Explicit values; inherited names are supported only by host tests | | `args` | `[]` | Default test selection flags; same allowlist as `--test_arg` | -| `network_origins` | `[]` | VRT-only extra HTTP(S) origins permitted through the browser tunnel | | `execution_timeout_seconds` | `180` | Deadline per Playwright invocation; discovery and capture have separate limits | | `timeout` | `"long"` | Independent Bazel test timeout category | -| `tags` | `[]` | Additional tags; local/manual/uncached restrictions remain | +| `tags` | `[]` | Additional tags; host tests remain local/manual/uncached | Choose one of `server`, `shell`, `base_url`, or `base_url_env`, or supply only a `config` with `use.baseURL` and optional native `webServer`. These source attributes @@ -98,10 +98,8 @@ playwright_runtime( Set `playwright = ":playwright"` on tests to reuse the consumer's packages. The default is the rules' pinned 1.63.0 runtime. Stable versions **>=1.63.0** -are accepted. VRT and image manifests require `image` with a matching Linux -amd64 Playwright image pinned by digest when overriding the version. Host-only -runtimes do not require an image. This is one runtime declaration, -not separate knobs repeated at every test call site. +are accepted. VRT additionally requires `browser` pointing to a matching +[declared browser runtime](browser-runtime.md). The runner validates the configured version against both packages and the consumer specs' Playwright imports before starting tests. Duplicate staged @@ -184,8 +182,8 @@ Reporter modules resolve relative to the compiled consumer config, including npm packages declared in its dependencies. Local reporters must be compiled from TypeScript and included in its runtime inputs. Write report files beneath `VRT_OUTPUTS`, using names distinct from `junit.xml` and `artifacts`. Reporter -credentials must be explicitly supplied through `env`/`env_inherit`. Reporters -execute on the host; browser `network_origins` do not control their uploads. +credentials must be explicit. VRT reporters run inside the offline action; +upload their downloaded reports afterward in CI. Host reporters use host networking. VRT discovery and capture are separate Playwright invocations, so reporters run for each phase. CLI reporter overrides remain disabled. @@ -260,91 +258,18 @@ returns `Promise` using the same static server as built shells. The optional entry point defaults to `index.html`. See [custom servers](customization.md) and [remote endpoints](e2e.md#existing-application-urls). -## VRT image manifest and CI preloading +## VRT execution -```starlark -load("@rules_web_e2e//playwright:defs.bzl", "playwright_images", "playwright_runtime") - -playwright_runtime( - name = "browser_runtime", - test = ":node_modules/@playwright/test/dir", - core = ":node_modules/playwright-core/dir", -) -playwright_images(name = "browser_images", playwright = ":browser_runtime") -visual_test(name = "app_vrt", tests = ":compiled_visual_specs", shell = ":app_shell", - playwright = ":browser_runtime") -``` +See [actiond setup](actiond.md) and [browser runtime inputs](browser-runtime.md). +`browser_runtime_oci(image = ":caller_image")` consumes a declared OCI layout; +`browser_runtime_archive(archive = ":runtime_tar")` consumes a flat filesystem +archive. Both extract files through declared tools without runtime registry access. -`bazel build //path:browser_images` writes `browser_images.json` without accessing -Docker or registry credentials. The default runtime's manifest is also available -as `@rules_web_e2e//runtime:images`. Schema version 1 contains `images`, each with: - -- `image`: the full digest-pinned reference, following runtime browser overrides. -- `platform`: `linux/amd64` for browser/control relay; `null` means daemon-selected - for the reaper. Preload the daemon's native platform in that case. -- `roles`: `browser`, `control-relay`, or `reaper`. Browser and relay share one entry. - -Only VRT consumes these image definitions; E2E and component browser tests do not -use Docker or Ryuk. CI owns authentication, mirrors, -and daemon preparation. For example, in an authenticated setup step: - -```sh -bazel build //path:browser_images -manifest="$(bazel info bazel-bin)/path/browser_images.json" -jq -r '.images[] | [.image, (.platform // "")] | @tsv' "$manifest" | -while IFS="$(printf '\t')" read -r image platform; do - if [ -n "$platform" ]; then - docker pull --platform "$platform" "$image" - else - docker pull "$image" - fi -done -``` +VRT does not support inherited environment, external network origins, or runtime +`--test_arg` / `.update -- ...` arguments. Declare selectors in the target's +`args` so they participate in the action key. Use separate targets when different +selections need independent baselines. Test reports and failures are downloaded; +only the local `.update` wrapper writes source references. -Then run the test using the **same daemon**, exact image references, and correct -platforms, with an empty `DOCKER_CONFIG` and registry credentials unavailable. -The runtime requires locally available images and never attempts registry authentication or pulls. -The manifest is not an image archive; transfer/load workflows must preserve the -exact digest references. Missing or uninspectable images fail with a preload error before resource creation. - -Supported discovery is a host runner's normal Docker discovery, or a containerized -runner with an explicit TCP/HTTP(S) `DOCKER_HOST` (and optional TLS settings). -Containerized runners reject socket discovery and `.testcontainers.properties`; -these can select Testcontainers' conditional unpinned Alpine gateway helper. -The runner explicitly supplies the remote hostname to avoid that helper, including -on socket fallback. No additional gateway image is required in the supported path. -The daemon's published browser-control ports must already be reachable from the -runner; this API does not provision remote connectivity. - -Preloading is an explicit CI step, not a cacheable Bazel build action or a test -whose side effects another test depends on. The manifest covers rules-managed -browser infrastructure, not images launched by consumer fixtures or servers. - -### Existing reapers on shared daemons - -Before connecting to an existing Ryuk, the runtime verifies that its actual -container image ID matches the locally resolved pinned Ryuk image. Labels and -requested image names are not sufficient. Mismatched or unverifiable identities -fail with an actionable error; the runtime never stops another invocation's -reaper. Use a dedicated daemon or coordinate cleanup with that reaper's owner. - -A pinned [dependency patch](../patches/README.md) enforces verification before -reuse and disables pulls/auth inside Testcontainers itself, including after -preflight. Tests cover real fresh and reused reapers, a different Ryuk image, -unverifiable identity, missing images, and public comparison/update failures. - - -## Host browser execution - -`web_e2e_test` and `component_browser_test` launch Chromium on the host through -Playwright Test. Provision the selected version's browser and host dependencies -before running tests. Supply `PLAYWRIGHT_BROWSERS_PATH` as an inherited absolute -cache path or an explicit target `env` value relative to Bazel runfiles, with -browser artifacts in `data`. The runner resolves it before isolating fixture -HOME/cache paths. It never installs browsers during tests. - -Host tests reject `network_origins`, `network_origins_env`, and consumer -`use.connectOptions` (including project overrides). They use host networking; -there is no container tunnel or browser network allowlist. VRT keeps its pinned -container and exact-origin tunnel. See [provisioning and migration](host-browsers.md), -including AGI's existing Bazel browser artifacts and FormatJS's shared runtime. +Host E2E/component tests retain their browser provisioning and command-line +selection behavior; see [host browsers](host-browsers.md). diff --git a/docs/architecture.md b/docs/architecture.md index 518fbb3..f21586e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -11,7 +11,7 @@ the consuming repository. | ------------------ | ------------------------------------------------------------------------------------------------------- | | Consumer | Specs, React providers, CSS, fonts, fixtures, aliases, authentication, and server configuration. | | Bazel macro | Source staging, runner labels, runfiles locations, environment, timeouts, and target tags. | -| TypeScript runtime | VRT container lifecycle, host browser configuration, isolated capture directories, and baseline synchronization. | +| TypeScript runtime | VRT action bootstrap, host browser configuration, isolated capture directories, and baseline synchronization. | | Playwright Test | Test execution, assertions, browser automation, and screenshot comparison. | | Consumer CI | Scheduling, artifact upload, and review of baseline changes. | @@ -23,37 +23,22 @@ additional environment variables and dependencies. ```mermaid flowchart TD - Inputs[Compiled specs, shell assets and runtime packages] --> Bazel[Bazel source staging] - Inputs --> Types[Strict TypeScript checks] - Types --> Test[Compare or update target] - Bazel --> Test - Test --> Runner[TypeScript runner] - Runner --> Host[Playwright Test and app server] - Runner -->|VRT only| Container[Pinned Linux container] - Host -->|E2E/component| LocalBrowser[Host Chromium] - Host <-->|Playwright WebSocket| Container - Container --> Browser[Chromium] - Browser -->|Exact fixture endpoint tunnel| Host - Host --> Results[JUnit and screenshot artifacts] - Results --> Baselines[Compare inputs or explicit baseline update] + Inputs[Compiled specs, assets, runtime, baselines] --> Build[Bazel input build] + Build --> Action[Linux action: fixture, Playwright, Chromium] + Action --> Results[Declared status, reports, screenshots] + Results --> Compare[Local test reports status] + Results --> Update[Local update applies successful captures] ``` -The consumer build owns source compilation and the npm dependency graph. E2E and -component tests use host browsers provisioned before execution; Playwright owns -launch and cleanup. The VRT container supplies -the browser and OS rendering environment. The runner verifies the declared -`playwright-core` version and copies that package into the container before -starting the server. Playwright forwards browser requests to the application -server; Docker does not need a source-tree bind mount or an npm install. +VRT runs in an actiond Linux amd64 worker using declared runtime files as its +root filesystem. The entire suite has loopback-only networking. No browser +server tunnel or Docker daemon participates in execution. Bazel downloads +results even for suite failures; the local wrapper reports failure or explicitly +applies successful captures. See [worker setup and isolation](actiond.md). -Each VRT invocation uses Testcontainers to create a browser, control relay, and -internal network, and removes them on completion or handled termination. Playwright Test execution and Bazel have separate -timeouts. The supported contract currently requires a local Docker daemon; -remote daemons and shared-container reuse need separate validation. - -The [Testcontainers design](testcontainers-vrt.md) explains why a pinned browser -environment improves VRT stability and describes the implemented input, -environment, and network isolation boundaries. +Host E2E/component targets run their server and browser on the host with +provisioned Chromium. The shared runner checks Playwright package versions, +stages declared runfiles, and manages child processes and artifacts. ## TypeScript and Bazel inputs @@ -90,8 +75,8 @@ See the [API reference](api.md) for attributes and configuration helpers. Server adapters own readiness and teardown; a config-only target delegates its native `webServer` lifecycle to Playwright. Both paths share the same -runfiles staging and fixture isolation. VRT adds a browser container and exact-origin tunnel. Deployed -mode must explicitly opt into network access and consumer-provided auth setup; +runfiles staging and fixture isolation. VRT isolates the entire suite in Linux. +Deployed host E2E requires consumer-provided endpoints and auth setup; it must not silently fall back to a local service or ambient credentials. Native Playwright 1.63 component specs mount named browser fixtures. The consumer @@ -102,14 +87,13 @@ supports interaction tests and independent screenshot targets. See ## Implementation map - [Public macro](../vrt/defs.bzl): Bazel targets and execution contract. -- [Runner](../runtime/runner.ts): runfiles, Docker, child process, and outputs. +- [Runner](../runtime/runner.ts): runfiles, child processes, and outputs. - [Config helper](../runtime/config.ts): typed Playwright Test and browser defaults. - [Runtime build](../runtime/BUILD.bazel): compilation and declaration packaging. Playwright Test owns fixtures, browser contexts, assertions, and traces. The runtime owns managed-server readiness and cleanup; remote endpoints remain -caller-owned. Both supply `VRT_APP_URL` to the config helper. Only VRT has exact -host/port browser tunnel restrictions. [Remote E2E](e2e.md) deliberately depends +caller-owned. Both supply `VRT_APP_URL` to the config helper. VRT permits only action-local loopback networking. [Remote E2E](e2e.md) deliberately depends on external application state and host networking. ## Built artifact seam @@ -117,6 +101,6 @@ on external application state and host networking. Test call sites accept compiled specs and either a compiled server adapter, a built shell, or an existing endpoint. `browser_shell` describes the HTML entry point within a built asset directory. The static server never transforms source. -A reusable `playwright_runtime` groups client packages and the pinned image; +A reusable `playwright_runtime` groups version-matched client packages; `matching` separately supplies VRT comparison policy. Application bundlers, framework versions, and generated styles stay in the consumer build graph. diff --git a/docs/component-vrt.md b/docs/component-vrt.md index 7aef33d..befecbc 100644 --- a/docs/component-vrt.md +++ b/docs/component-vrt.md @@ -1,7 +1,7 @@ # Component visual regression tests `component_visual_test` generates Playwright Test captures from consumer `.visual.tsx` modules against a -Playwright server in a digest-pinned Linux container. It provides comparison, +caller-owned browser runtime in an isolated Linux action. It provides comparison, failure artifacts, and an explicit `.update` target. The runtime is consumed through Bazel; a separate npm publication is not required. See [architecture](architecture.md) and [visual testing design](visual-testing-design.md) @@ -10,13 +10,13 @@ and the [API reference](api.md) for all supported attributes. ## Try the standalone example -Install Bazelisk and Docker, start a local Docker daemon, and -[preload the pinned images](api.md#vrt-image-manifest-and-ci-preloading), then: +Supply `examples/react/runtime.tar` from your image build and configure a +patched worker with [the actiond guide](actiond.md), then: ```sh cd examples/react -bazelisk test //:visual_test -bazelisk run //:visual_test.update +bazelisk test --config=vrt //:visual_test +bazelisk run --config=vrt //:visual_test.update ``` Bazel installs the locked npm dependencies. The update command replaces this @@ -36,6 +36,7 @@ load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test") browser_shell(name = "gallery", assets = ":built_gallery", entry_point = "gallery.html") component_visual_test( name = "visual_test", + browser = ":linux_browser", shell = ":gallery", matching = ":matching", baselines = glob(["__screenshots__/*.png"], allow_empty = True), @@ -52,29 +53,21 @@ Playwright's pixelmatch comparator with threshold 0.1 and zero mismatched pixels Viewport, language, theme, density, and capture hooks remain visual options. The gallery waits for loaded fonts; hooks should wait for application readiness. -A custom compiled `server` or existing URL can replace `shell`; see +A custom compiled `server` or action-local URL can replace `shell`; see [customization](customization.md). Most consumers need no Playwright config. ## Execution contract - Tested versions: Bazel 8.6/9.2, Playwright Test/core 1.63.0, Vite 8.2.2, React 19.2.8. Initial screenshot support is Linux amd64. macOS/arm64 screenshot equivalence has not been validated. -- The image, including fonts and browser binaries, is pinned by digest. Its - Playwright version must match the consumer’s `playwright-core` package. - The runner copies that declared package into the container; it does not run - npm installs or mount source paths inside Docker. -- A local Docker daemon must be reachable by Testcontainers. Docker connection - variables are inherited explicitly. The browser tunnel exposes only the - fixture server’s exact host and port by default. Prefer declared fixtures; - `network_origins = ["https://fixtures.example"]` explicitly permits an - additional host/port and introduces an external dependency. -- Each invocation owns a browser container, control relay, and internal network. Tests run locally, outside - Bazel’s filesystem sandbox, and disable result caching. Docker/browser tests - are `manual`; invoke them explicitly in a dedicated CI job. -- Built inputs and dependencies are copied from the runfiles manifest into a private - tree. Compare and update use only declared - `env`/`env_inherit` variables and fresh home/cache directories. See - [the isolation boundaries](testcontainers-vrt.md). +- The declared runtime supplies Chromium, Node, libraries, and fonts. Pin it and + keep its browser compatible with the consumer's Playwright packages. +- The fixture server and browser share an action-local loopback network. External + services and inherited environment are unsupported; declare fixtures and `env`. +- Capture and comparison are cacheable remote actions. Their local wrappers + download artifacts and report results; only `.update` writes source baselines. +- Built inputs are staged from their runfiles manifest into a private tree with + fresh home/cache directories. See [the isolation boundaries](actiond.md). - Compare mode copies declared baselines to a temporary directory. Missing or changed screenshots fail without modifying source baselines. Playwright writes JUnit and image attachments under `TEST_UNDECLARED_OUTPUTS_DIR`. @@ -84,7 +77,7 @@ A custom compiled `server` or existing URL can replace `shell`; see run concurrent updates against the same baseline directory. - `execution_timeout_seconds` bounds each Playwright invocation (default: 180 seconds each for discovery and capture). Managed server startup has a separate - 30-second deadline; Bazel’s `timeout` independently bounds the whole test. Failed updates leave existing + 30-second deadline; Bazel’s `timeout` bounds the local result test, not its input build actions. Failed updates leave existing baselines intact and print the artifact directory. ## Fixed and portaled visuals diff --git a/docs/dependencies.md b/docs/dependencies.md index 35273b6..f9457e0 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -5,8 +5,7 @@ versions are exact pins; lockfiles pin transitive dependencies. | Dependency | Version | | ---------------------------------------- | ----------------------------------------- | -| Playwright Test / client / browser image | 1.63.0 | -| Testcontainers | 12.1.0 | +| Playwright Test / client | 1.63.0 | | Example build: Vite / React plugin | 8.2.2 / 6.1.1 | | TypeScript | 7.0.2 | | React / React DOM in standalone example | 19.2.8 | @@ -17,7 +16,7 @@ versions are exact pins; lockfiles pin transitive dependencies. | rules_js / rules_ts | 3.4.1 / 3.10.1 | | bazel_lib / rules_shell | 3.7.2 / 0.8.0 | -The Playwright image pins its Linux amd64 manifest digest. Keep image and npm +Callers pin their Linux amd64 runtime image or archive. Keep browser and npm versions synchronized and compare reviewed screenshots after upgrades. pnpm 12's release-age exceptions name only the exact recent releases selected here. diff --git a/docs/e2e.md b/docs/e2e.md index bb13ae2..9bd74c5 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -61,7 +61,7 @@ flowchart LR Inputs --> Runtime[Shared browser runtime] Runtime --> Server[Consumer server adapter] Runtime --> Tests[Native Playwright Test] - Tests --> Browser[Pinned Testcontainers browser] + Tests --> Browser[Host Chromium] Browser -->|Allowed fixture endpoint| Server Tests --> Results[JUnit, failure screenshots and traces] ``` @@ -72,7 +72,7 @@ route registries, and framework conventions remain in the consuming repository. Page objects and `test.extend` fixtures work normally. Prefer `page.route` or local fixture APIs for deterministic data; declare any authentication state files in `data`. Keep credentials in explicitly declared environment variables rather -than checked-in state. Host browsers use host networking; `network_origins` is reserved for VRT. +than checked-in state. Host browsers use host networking; VRT uses separate offline Linux actions. E2E is manual, local, and uncached. It requires a provisioned host browser, not Docker; see [host setup](host-browsers.md). @@ -114,8 +114,7 @@ Supply credentials through declared environment or private generated inputs. The version-matched host browser, staged specs, clean environment, and artifacts are shared with local E2E. Live data and remote deployments are external inputs, so these tests remain -uncached and do not promise reproducible application state. The same endpoint -options work with VRT, whose `.update` remains explicit. +uncached and do not promise reproducible application state. VRT fixtures must run inside their Linux action; deployed URLs belong in host E2E. `//:remote_integration_test` in the React example starts an independent fixture on a random port and verifies base paths, interactions, host-network @@ -129,10 +128,8 @@ compiled specs, config (or server/shell/URL), matching policy, and baseline inpu `bazel run //:visual_test.update` replaces baselines only after the full suite succeeds. The [native example](../examples/react/native.visual.spec.ts) exercises this path. -For VRT, if an extra service endpoint changes between environments, declare its variable -name in `network_origins_env = ["AUTH_ORIGIN"]`. Only named, nonempty variables -are read, and each must be an exact HTTP(S) origin without paths, credentials, -or wildcards. Static endpoints remain in `network_origins`. +VRT requires a declared `browser` and action-local fixture services. See +[actiond execution](actiond.md); external origin exceptions are unsupported. ## Suite selection belongs to Bazel diff --git a/docs/getting-started.md b/docs/getting-started.md index 5c1e852..996f313 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -12,18 +12,20 @@ flowchart LR Specs --> Tests[Bazel browser targets] Shell --> Tests Server[Consumer server or existing URL] --> Tests - Tests --> Browser[Host Playwright; Testcontainers for VRT] + Tests --> Browser[Host Playwright; Linux actions for VRT] Browser --> Results[Reports, traces and screenshot diffs] ``` ## Run the example Install Bazelisk and [provision host Chromium](host-browsers.md) for E2E/component -tests. Start Docker and [preload images](api.md#vrt-image-manifest-and-ci-preloading) for VRT; Linux amd64 is the validated screenshot platform. +tests. For VRT, supply the example runtime archive and configure a patched +[actiond worker](actiond.md); Linux amd64 is the screenshot platform. ```sh cd examples/react -bazelisk test //:e2e_test //:component_test //:component_visual_test +bazelisk test //:e2e_test //:component_test +bazelisk test --config=vrt //:component_visual_test ``` Bazel fetches locked dependencies, compiles specs, checks types, and builds the @@ -86,7 +88,7 @@ a successful capture. Do not share baseline directories or run concurrent update ## CI and troubleshooting Browser targets are `manual`: `bazel test //...` does not select them. List them -explicitly in host-browser and Docker-enabled VRT jobs and upload `bazel-testlogs/` on failure. +explicitly in host-browser and actiond VRT jobs and upload `bazel-testlogs/` on failure. The [CI workflow](../.github/workflows/ci.yaml) is a working example. | Symptom | Check | @@ -94,10 +96,10 @@ The [CI workflow](../.github/workflows/ci.yaml) is a working example. | Rejected source inputs | Pass emitted `.js` specs or a compiled server/config/matching module | | Missing shell entry | Check the built directory contains the declared HTML and all referenced assets | | Playwright version mismatch | Align compiler dependencies, runtime packages, and pinned browser image | -| Docker startup failure (VRT) | Start a local daemon and check its connection settings and image access | +| Worker startup failure (VRT) | Check the patched actiond endpoint, VM devices, and declared runtime | | Missing imports | Declare helpers, npm links, module markers, and generated outputs in the producer | | Empty visual catalog | Register a module with at least one visual that does not set `vrt: false` | -| Blocked browser request (VRT) | Vendor/mock the resource or add its exact origin to `network_origins` | +| Blocked browser request (VRT) | Vendor/mock the resource or serve it inside the action | | Screenshot mismatch | Inspect expected/actual/diff attachments and approve only intentional changes | ## Migrating from 1.0.0 @@ -106,7 +108,7 @@ The [CI workflow](../.github/workflows/ci.yaml) is a working example. | ------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `srcs`, `deps`, typecheck in `data` | A built `tests` target and built shell; producer owns compilation/typechecking | | `vite`, `server_config` | Consumer build action producing a directory, wrapped in `browser_shell` | -| `playwright_test`, `playwright_core`, `image`, `playwright_version` | One reusable `playwright_runtime`, selected with `playwright` | +| `playwright_test`, `playwright_core`, `image`, `playwright_version` | Client `playwright_runtime` plus a VRT `browser_runtime` selected with `browser` | | Required Playwright config | Generated defaults; optional compiled config for advanced fixtures/timeouts | | `visualConfig({tolerance})` | Compiled `VisualMatching` module selected with `matching` | diff --git a/docs/host-browsers.md b/docs/host-browsers.md index 41026f3..ea6e76a 100644 --- a/docs/host-browsers.md +++ b/docs/host-browsers.md @@ -1,15 +1,13 @@ -# Host browsers and VRT-only containers +# Host browsers and VRT actions | Target | Browser | Network behavior | | --- | --- | --- | | `web_e2e_test` | Version-matched host Chromium | Host network | | `component_browser_test` | Version-matched host Chromium | Host network | -| `visual_test` | Pinned Linux amd64 container | Declared-origin tunnel | -| `component_visual_test` | Pinned Linux amd64 container | Declared-origin tunnel | +| `visual_test` | Declared Linux amd64 runtime | Action-local loopback | +| `component_visual_test` | Declared Linux amd64 runtime | Action-local loopback | -Only VRT initializes Testcontainers, Ryuk, or the control relay, inherits Docker -settings automatically, or consumes image requirements. Existing VRT image pins, -matching policy, baseline directories, and `.update` commands remain in place. +VRT uses [actiond](actiond.md); host tests keep their existing browser setup. ## Provision once, then test @@ -40,88 +38,41 @@ Tests remain local and uncached. A host cache is an explicit environmental input Bazel-provisioned browser artifacts additionally make the browser files declared inputs. Neither choice provides VRT's controlled OS/fonts rendering environment. -## AGI migration +## Consumer migration -The inspected Playwright 1.63 migration checkout already has -`PLAYWRIGHT_PROCESS_DATA` and `PLAYWRIGHT_PROCESS_ENV` in -`tools/rules/frontend/playwright/process_runtime.bzl`. Its data supplies -`@playwright//:chromium`, `:chromium-headless-shell`, and `:ffmpeg`, and its cache -path is `$(rootpath @playwright//:chromium)/../`. Reuse those artifacts rather than -introducing a second host cache. +For AGI, keep existing host browser data and `PLAYWRIGHT_BROWSERS_PATH` wiring. +In the shared component/page VRT wrappers, add `browser` pointing to a +`browser_runtime` built from the caller's OCI image. Preserve built galleries, +custom configs, server executables, `data`, matching, and baseline directories. +The VRT rule expands `$(rootpath ...)` in server environment JSON and configures +inputs for Linux amd64. Set `target_platform` when native toolchains need +additional ABI constraints. Remove Docker preparation, image manifests, and +Docker/network tags from the VRT lane; route it to the actiond worker. -Update the two common wrappers in `tools/rules/frontend/playwright/defs.bzl`: +Existing VRT external-origin exceptions cannot carry over: vendor those assets +or serve declared fixtures inside the action. Keep live external checks in host +E2E targets. Do not silently remove assertions or retain a networked VRT fallback. -```starlark -# Arguments to web_e2e_test / component_browser_test inside AGI's wrappers: -data = _unique(PLAYWRIGHT_PROCESS_DATA + data), -env = { - "PLAYWRIGHT_BROWSERS_PATH": PLAYWRIGHT_PROCESS_ENV["PLAYWRIGHT_BROWSERS_PATH"], -} | env, -``` - -Preserve each wrapper's existing data/dependency aggregation and environment -(e.g. `_DEFAULT_ENV` for E2E). Do not copy its HOME override: this runner owns -fixture HOME isolation. Add other process-runtime environment entries only where -AGI intentionally needs them, such as an existing host-requirements policy. - -Remove E2E `network_origins` / `network_origins_env` forwarding, including the -CDN/API list passed by `tools/rules/frontend/e2e/defs.bzl`. Host tests do not enforce -that restriction. Retain those settings on `tools/rules/frontend/vrt/defs.bzl` -targets. Keep AGI's ECR image override for VRT; host suites ignore it. Keep existing -suite tags/names and deployed/local selection; CI can separate Docker preparation -by `visual_test` versus `component_browser_test` / `e2e_test` tags. - -This is a migration recipe for the inspected checkout, not a claim that AGI's -consumer changes have already landed or its full suite has been validated here. - -## FormatJS migration - -The inspected `packages/editor/vrt/BUILD.bazel` already splits `e2e_test`, -`component_test`, and `visual_test`, sharing one `:playwright` runtime. Keep those -target declarations and the existing compiled server/gallery inputs. - -Provision host Chromium using FormatJS's locked Playwright package and export -`PLAYWRIGHT_BROWSERS_PATH` before running the two interaction targets. Keep Docker -setup for `visual_test` and its update target. CI may run host interaction tests on -Linux/macOS while retaining Linux amd64 for screenshot baselines. The FormatJS -consumer suite has not been executed by this repository's checks. - -## Compatibility changes - -- E2E/component execution uses the host OS and network; diagnostic screenshots - can vary across OSes. Pixel baseline tests belong in VRT targets. -- `network_origins` and `network_origins_env` fail at analysis time on host targets. -- Consumer `use.connectOptions`, including project overrides, is rejected on - host targets because Playwright Test launches the browser directly. -- Custom runtime versions need a matching host browser. A Docker image is only - mandatory when that runtime is used by VRT or `playwright_images`. - - -## Consumer-built OCI images for VRT - -Image construction stays in the consuming repository. Pass a digest-pinned image -reference through the shared runtime's `image` attribute: +For FormatJS, the editor keeps its Vite/StyleX bundle, matching, shell, and host +interaction targets. Its VRT change is: ```starlark -playwright_runtime( - name = "playwright", - test = ":node_modules/@playwright/test/dir", - core = ":node_modules/playwright-core/dir", - version = "1.63.0", - image = "registry.example/team/vrt@sha256:", +component_visual_test( + name = "visual_test", + browser = ":linux_browser", + target_platform = "//platforms:linux_x86_64_gnu", + shell = ":editor_shell", + matching = ":vrt_matching", + playwright = ":playwright", + baselines = glob(["__screenshots__/*.png"], allow_empty = True), ) ``` -The image must support Linux amd64 and contain Node, the matching Playwright -Chromium installation under `/ms-playwright`, and the OS libraries/fonts needed -by the application. The rules copy the selected `playwright-core` into the image -at execution time and launch its browser server. The same image runs the control -relay. Build or extend the image in the consumer's OCI pipeline; there is no -image-building action in these rules. The bundled default remains a convenience. +`:linux_browser` is a caller-owned [browser_runtime](browser-runtime.md). +The actual editor gallery built and captured all eight screenshots in actiond's +process sandbox with this interface. That check does not establish full AGI or +FormatJS CI migration, or macOS worker support. -AGI can retain its ECR runtime image; FormatJS can supply a custom image with its -fonts and rendering dependencies. This image is ignored by host interaction -tests that share the runtime. Ryuk is still a separate helper requirement for -VRT, exposed by `playwright_images`; constructing a browser image does not remove -that helper. Preload all manifest images before execution; existing Ryuk image -identity must match the pin before reuse. +`playwright_runtime` now groups only matching npm packages and their version. +Move its former `image` setting into a declared OCI image target consumed by +`browser_runtime_oci`. Remove `playwright_images` and Ryuk preload jobs. diff --git a/docs/oss-browser-testing-plan.md b/docs/oss-browser-testing-plan.md index 0e8732c..3e1d1e4 100644 --- a/docs/oss-browser-testing-plan.md +++ b/docs/oss-browser-testing-plan.md @@ -4,14 +4,14 @@ The supported APIs are documented in the [API reference](api.md). Current capabilities include managed and remote E2E, native component mounts, shared visual modules, generated VRT captures, and explicit baseline updates. -The runtime uses Playwright Test and Testcontainers, with Bazel integration +The runtime uses Playwright Test and actiond, with Bazel integration through `rules_js` and strict TypeScript compilation through `rules_ts`. Applications supply their own rendering framework, servers, and fixtures. ```mermaid flowchart LR Inputs[Consumer config, specs, visuals and assets] --> Rules[Bazel targets] - Rules --> Runtime[Playwright Test and Testcontainers] + Rules --> Runtime[Playwright Test and actiond] Runtime --> Browser[Pinned Linux Chromium] Runtime --> Reports[JUnit, traces and image diffs] Runtime --> Update[Explicit baseline update] @@ -23,7 +23,7 @@ Generic `playwright_test` and page-oriented `web_visual_test` wrappers are not implemented. A Starlark `component_visual_module` macro is not exported; use the TypeScript `ComponentVisualModule` interface today. -Remote Docker daemons, shared browser container reuse, and interchangeable +Native ARM64 screenshot baselines, macOS VM validation, and interchangeable baselines across architectures need separate validation before support. Any extension should preserve declared inputs, explicit network access, reviewed baseline updates, and useful failure artifacts. diff --git a/docs/testcontainers-vrt.md b/docs/testcontainers-vrt.md index 1e42985..c59aa43 100644 --- a/docs/testcontainers-vrt.md +++ b/docs/testcontainers-vrt.md @@ -1,91 +1,4 @@ -# Testcontainers and VRT stability +# VRT execution -`visual_test` and `component_visual_test` use containers; E2E and component -browser tests launch on the host. The VRT TypeScript runtime uses Testcontainers 12.1.0 to start a fresh browser and -control relay per invocation. Both use the same digest-pinned Playwright image -and explicit `linux/amd64` platform. The runner verifies the browser's platform -and the declared Playwright package version before running tests. Ryuk, the -cleanup helper, uses a pinned image digest on the daemon-selected platform. -All images must be preloaded; execution never pulls images or authenticates to -registries. Existing Ryuk image identity is verified before reuse. - -```mermaid -flowchart LR - Bazel[Bazel declared runfiles] --> Stage[Private input tree] - Stage --> Server[Static shell or custom server] - Stage --> Test[Playwright Test] - Test --> Relay[Fixed control socket relay] - subgraph Internal Docker network - Relay --> Browser[Playwright browser server] - end - Browser -->|Exact fixture host and port via Playwright tunnel| Server -``` - -Docker cannot publish ports from an internal-only network. The relay joins that -network and a normal bridge, publishing a loopback control port. It forwards TCP -only to the browser server; it is not a general network proxy. The browser has -no external network route. Playwright's host tunnel allows only the fixture's -assigned `127.0.0.1:port` by default, so unrelated host services are not exposed. -Targets may opt into additional HTTP(S) `network_origins`; these expose the exact -host and port through the tunnel and intentionally introduce external inputs. -Wildcards, credentials, and URL paths are rejected. - -The adapter owns readiness checks, endpoint discovery, startup deadlines, and -cleanup. Both containers use an init process; Chromium has 1 GiB shared memory. -It removes resources after success, test failure, or partial startup failure. -The runtime terminates host children on cancellation and timeout, escalating to -SIGKILL if needed. Ryuk handles lost client connections, including abrupt runner -termination. A broken Docker daemon can still prevent cleanup. No browser -container reuse is enabled. - -## Controlling rendering inputs - -| Input | Control | -| ----------------------------------- | --------------------------------------------------------------------------------------------------- | -| Browser, OS libraries, system fonts | Pinned image digest and Linux amd64 platform. | -| Built application and npm packages | Materialized runfiles manifest; no source or output-tree mounts. | -| Environment | Only target `env` and `env_inherit`, with fixed locale/timezone and private home/cache directories. | -| Browser requests | Fixture endpoint only; vendor fonts and mock API responses in declared fixtures. | -| Screenshot settings | Fixed viewport, theme, locale, timezone, reduced motion, and caret behavior. | -| Application readiness | Consumer assertions and font readiness before capture. | - -Compare and update use the same input staging and environment policy. Update -changes snapshot mode and the final destination; it does not inherit additional -shell configuration. Image upgrades still require reviewing fresh captures. -Containers do not freeze clocks, random values, UI transitions, or application -state. Pixel tolerance should not hide uncontrolled inputs. - -## Scope and verification - -This is reproducible local browser testing, not a fully sandboxed Bazel action. -The host Node processes execute trusted consumer config, plugins, and tests; -those can explicitly read host files or access the network. Docker discovery, -daemon/kernel behavior, preloaded image availability, and machine resources -remain external inputs. Tests therefore remain manual, local, and uncached. -Remote Docker daemons are not supported by the loopback control binding. - -Regression tests cover staging and environment isolation, fixture access, -blocked unrelated host ports, and blocked direct public-network access. The -standalone example checks committed screenshot baselines. The built-in server reads only compiled assets; dotenv loading and source -transformation are absent during execution. Consumer builds remain responsible -for their own environment and dependency discovery. - -## Enforced runtime dependency contract - -Beyond Bazel and standard OS facilities, Docker is the only additional installed -VRT prerequisite. Node, Playwright packages, and compiled application inputs come -from Bazel; Chromium and fonts come from the pinned browser image. Preload both -browser and Ryuk images using the [manifest](api.md#vrt-image-manifest-and-ci-preloading) -before testing. Image acquisition remains a caller-owned setup step. - -CI runs `tests/preloaded-vrt.sh` in a pinned OS-only Linux container with no -installed Node, Chromium, Docker CLI, or registry credentials. It executes built -public compare/update targets using their declared runfiles and a proxy that -rejects Docker pull/auth requests. Missing-image updates must preserve baselines; -completed and failed invocations must remove their browser/relay/network resources. -The envelope uses Linux host networking so Docker's loopback relay is reachable; -it is a regression environment, not a new consumer execution requirement. - -This verifies tool provisioning, not full hermeticity of consumer code. Host-side -Node code is still trusted and unsandboxed, and application clocks, randomness, -and opted-in external services remain consumer-controlled inputs. +Testcontainers has been replaced by [actiond execution](actiond.md). See +[declared browser runtimes](browser-runtime.md) for caller-owned OCI inputs. diff --git a/docs/visual-testing-design.md b/docs/visual-testing-design.md index d417d0a..c2a4478 100644 --- a/docs/visual-testing-design.md +++ b/docs/visual-testing-design.md @@ -4,7 +4,7 @@ A screenshot is a reviewed assertion about a rendered state. Useful VRT must make that state repeatable, distinguish comparison from approval, and leave inspectable evidence when it changes. -See [Testcontainers and VRT stability](testcontainers-vrt.md) for container +See [VRT on actiond](actiond.md) for execution lifecycle, readiness, isolation, and reuse decisions. ## Stable capture @@ -56,11 +56,9 @@ review the resulting image changes before committing them. ## Execution and evidence -VRT is an explicit, `manual` test lane because it needs Docker and network -access. The `external` tag forces each test invocation to execute; `no-cache` -alone does not prevent reuse of local Bazel test results. Other execution tags -keep the current runner local and outside the filesystem sandbox. This is a -controlled rendering environment, not a fully hermetic browser action. +VRT is an explicit, `manual` lane of cacheable Linux execution actions. Runtime +files, code, fixture data, and baselines are declared inputs. The local wrapper +reports downloaded results and applies successful captures on explicit updates. Keep diagnostics separate from captured baselines. Failed runs retain outputs for inspection; successful updates may clean their temporary artifacts. A @@ -70,7 +68,7 @@ the visual change is intended. Reporting does not depend on a hosted VRT service Changes to capture behavior should exercise unchanged comparison, an intentional visual mismatch, missing baselines, and explicit update followed by comparison. Also verify type errors stop the build, repeated captures remain stable, and -owned containers are cleaned up. The [React example](../examples/react) is the +timed-out and cancelled actions release their processes. The [React example](../examples/react) is the small integration fixture; larger consumers exercise real application rendering. ## Reusable visual modules diff --git a/examples/react/BUILD.bazel b/examples/react/BUILD.bazel index 64bb3b5..ba54984 100644 --- a/examples/react/BUILD.bazel +++ b/examples/react/BUILD.bazel @@ -1,10 +1,11 @@ +load("@rules_web_e2e//playwright:archive.bzl", "browser_runtime_archive") load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_library", "js_run_binary", "js_test") load("@aspect_rules_js//npm:defs.bzl", "npm_link_package") load("@aspect_rules_ts//ts:defs.bzl", "ts_project") load("@npm//:defs.bzl", "npm_link_all_packages") load("@rules_web_e2e//component:defs.bzl", "browser_shell", "component_browser_test") load("@rules_web_e2e//e2e:defs.bzl", "web_e2e_test") -load("@rules_web_e2e//playwright:defs.bzl", "playwright_runtime") +load("@rules_web_e2e//playwright:defs.bzl", "browser_runtime", "playwright_runtime") load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test", "visual_test") npm_link_all_packages(name = "node_modules") @@ -35,6 +36,7 @@ web_e2e_test( ) visual_test( + browser = ":browser", name = "native_visual_test", baseline_dir = "__native_screenshots__", baselines = glob( @@ -149,6 +151,7 @@ js_library( ) component_visual_test( + browser = ":browser", name = "visual_test", baselines = glob( ["__screenshots__/*.png"], @@ -159,6 +162,7 @@ component_visual_test( ) component_visual_test( + browser = ":browser", name = "component_visual_test", baseline_dir = "__component_screenshots__", baselines = glob( @@ -240,33 +244,22 @@ js_library( deps = [":typecheck_project"], ) -# Network isolation is a property of the VRT container, not host-browser tests. +# VRT runs in a Linux action with loopback-only networking. visual_test( + browser = ":browser", name = "network_isolation_test", tests = ":isolation_specs", shell = ":app_shell", baseline_dir = "__isolation_screenshots__", ) -js_test( - name = "preloaded_vrt_test", - copy_data_to_bin = False, - data = [ - "package.json", - ":native_visual_test", - ":native_visual_test.update", - "__native_screenshots__/saved.png", - "@rules_web_e2e//runtime:typecheck", - "@rules_web_e2e//runtime:images", - ], - entry_point = "@rules_web_e2e//runtime:preload_lifecycle_entry", - env = { - "PRELOAD_COMPARE": "$(rlocationpath :native_visual_test)", - "PRELOAD_UPDATE": "$(rlocationpath :native_visual_test.update)", - "PRELOAD_BASELINE": "$(rlocationpath __native_screenshots__/saved.png)", - "PRELOAD_IMAGES": "$(rlocationpath @rules_web_e2e//runtime:images)", - }, - env_inherit = ["DOCKER_HOST"], - tags = ["manual", "external", "no-sandbox", "no-remote", "no-cache"], - timeout = "long", +# Supply this declared archive from your own image build; see docs/browser-runtime.md. +browser_runtime_archive(name = "runtime_files", archive = "runtime.tar") +browser_runtime( + name = "browser", + root = ":runtime_files", + executable = "chromium/chrome-headless-shell", + node = "bin/node", + library_dirs = ["lib"], + fontconfig = "etc/fonts", ) diff --git a/examples/react/isolation.spec.ts b/examples/react/isolation.spec.ts index 36dba6a..600087a 100644 --- a/examples/react/isolation.spec.ts +++ b/examples/react/isolation.spec.ts @@ -1,7 +1,7 @@ import {expect, test} from '@playwright/test' import {createServer} from 'node:http' -test('browser can reach the fixture but cannot tunnel to unrelated host ports', async ({ +test('browser and fixture services share the isolated action loopback', async ({ page, }) => { let requests = 0 @@ -21,8 +21,9 @@ test('browser can reach the fixture but cannot tunnel to unrelated host ports', await expect( page.getByRole('button', {name: 'Save', exact: true}) ).toBeVisible() - await expect(page.goto(url, {timeout: 2000})).rejects.toThrow() - expect(requests).toBe(0) + await page.goto(url) + await expect(page.locator('body')).toHaveText('undeclared service') + expect(requests).toBeGreaterThan(0) } finally { await new Promise((resolve, reject) => unrelated.close(error => (error ? reject(error) : resolve())) diff --git a/experiments/actiond/cancel-public.py b/experiments/actiond/cancel-public.py new file mode 100644 index 0000000..0ec8bfe --- /dev/null +++ b/experiments/actiond/cancel-public.py @@ -0,0 +1,39 @@ +"""Cancel an executing remote capture and verify the local updater never runs.""" + +import os +from pathlib import Path +import re +import signal +import subprocess +import sys +import time + +baseline = Path("__actiond_cancel__/keep.png") +baseline.parent.mkdir(exist_ok=True) +expected = Path("__actiond_native__/saved.png").read_bytes() +baseline.write_bytes(expected) +log = Path("../results/cancellation.log") +log.parent.mkdir(exist_ok=True) +command = [ + *sys.argv[1:3], "run", "//:actiond_cancel_test.update", *sys.argv[3:], + "--progress_report_interval=1", "--curses=no", "--color=no", +] +with log.open("w") as output: + process = subprocess.Popen(command, stdout=output, stderr=output, start_new_session=True) + try: + deadline = time.monotonic() + 90 + while not re.search(r"VrtCapture actiond_cancel_test_capture.results;[^\n]*remote", log.read_text()): + if process.poll() is not None: + raise AssertionError(f"Capture exited before cancellation: {log.read_text()}") + if time.monotonic() >= deadline: + raise AssertionError(f"Capture did not start remotely: {log.read_text()}") + time.sleep(0.2) + os.killpg(process.pid, signal.SIGINT) + code = process.wait(timeout=20) + assert code in (8, 130, -signal.SIGINT), (code, log.read_text()) + finally: + if process.poll() is None: + os.killpg(process.pid, signal.SIGKILL) + process.wait() +assert baseline.read_bytes() == expected +assert not (baseline.parent / "partial.png").exists() diff --git a/experiments/actiond/prepare-public.mjs b/experiments/actiond/prepare-public.mjs index d3b9da4..bb29a42 100644 --- a/experiments/actiond/prepare-public.mjs +++ b/experiments/actiond/prepare-public.mjs @@ -85,4 +85,13 @@ visual_test( env = {"ACTIOND_HANG": "1"}, execution_timeout_seconds = 8, ) +visual_test( + name = "actiond_cancel_test", + browser = ":actiond_browser", + config = ":native_config", + tests = ":actiond_failure_specs", + baseline_dir = "__actiond_cancel__", + env = {"ACTIOND_HANG": "1"}, + execution_timeout_seconds = 90, +) `) diff --git a/experiments/actiond/run-public-actiond.sh b/experiments/actiond/run-public-actiond.sh index b77e14a..c429531 100644 --- a/experiments/actiond/run-public-actiond.sh +++ b/experiments/actiond/run-public-actiond.sh @@ -2,6 +2,7 @@ set -euo pipefail work=${1:?usage: run-public-actiond.sh ABSOLUTE_WORK_DIRECTORY ENDPOINT} endpoint=${2:?missing actiond endpoint} +scripts=$(cd "$(dirname "$0")" && pwd) cd "$work/public" collect() { mkdir -p "$work/results/public" @@ -89,3 +90,6 @@ assert result['mode'] == 'compare' and result['exitCode'] != 0, result assert list((directory / 'artifacts').rglob('*-diff.png')), 'Missing screenshot diff' PY cp "$work/native-baseline.png" __actiond_native__/saved.png +python3 "$scripts/cancel-public.py" "${bazel_cmd[@]}" "${flags[@]}" +# The same worker must execute another action after cancellation. +"${bazel_cmd[@]}" test //:actiond_native_test "${flags[@]}" --remote_accept_cached=false --nocache_test_results --test_output=errors diff --git a/internal/browser.bzl b/internal/browser.bzl index 506fe6a..1ea6e12 100644 --- a/internal/browser.bzl +++ b/internal/browser.bzl @@ -1,6 +1,6 @@ """Execute compiled browser inputs with a reusable Playwright runtime.""" -load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_library", "js_test") +load("@aspect_rules_js//js:defs.bzl", "js_library", "js_test") load("//playwright:defs.bzl", "BrowserRuntimeInfo", "PlaywrightInfo", "runfile", _PLAYWRIGHT_IMAGE = "PLAYWRIGHT_IMAGE") load(":remote.bzl", "remote_browser_test") @@ -44,8 +44,8 @@ def _inputs_impl(ctx): fail("tests must supply compiled JavaScript specs") runtime = ctx.attr.playwright[PlaywrightInfo] visual = ctx.attr.mode in ["visual", "visual-spec"] - if visual and not ctx.attr.browser and not runtime.images: - fail("VRT requires a matching digest-pinned browser image on playwright_runtime") + if visual and not ctx.attr.browser: + fail("VRT requires a declared browser_runtime via browser") shell = ctx.attr.shell[ShellInfo] if ctx.attr.shell else None result = ctx.actions.declare_file(ctx.label.name + ".json") ctx.actions.write(result, json.encode({ @@ -54,7 +54,7 @@ def _inputs_impl(ctx): "matching": _compiled(ctx.attr.matching, "matching"), "server": _compiled(ctx.attr.server, "server"), "shell": {"directory": shell.directory, "entryPoint": shell.entry_point} if shell else None, - "playwright": {"test": runtime.test, "core": runtime.core, "version": runtime.version, "images": runtime.images if visual else []}, + "playwright": {"test": runtime.test, "core": runtime.core, "version": runtime.version}, "browser": ctx.attr.browser[BrowserRuntimeInfo].descriptor if ctx.attr.browser else None, })) inputs = ctx.runfiles(files = [result]) @@ -111,16 +111,16 @@ def browser_test( fail("VRT_* environment names are reserved for the browser runtime") if visual and component: fail("Visual and component modes are separate targets") + if visual and not browser: + fail("VRT requires browser = ") if browser and not visual: fail("browser is only supported for VRT; other browser tests use host browsers") - if browser and (network_origins or network_origins_env): - fail("Declared browser actions have loopback-only networking; supply local fixture servers") + if network_origins or network_origins_env: + fail("Network origin exceptions are unsupported: VRT uses action-local fixtures and host tests use the host network") if browser and env_inherit: fail("Remote VRT requires explicit env values instead of env_inherit") if browser and base_url_env and base_url_env not in env: fail("Remote VRT base_url_env must have an explicit env value") - if not visual and (network_origins or network_origins_env): - fail("network_origins and network_origins_env are VRT-only; host browsers use the host network") if not visual and matching: fail("matching is only supported by visual targets") if execution_timeout_seconds <= 0: @@ -136,7 +136,6 @@ def browser_test( fail("base_url must not be empty") if base_url_env and base_url_env not in env and base_url_env not in env_inherit: env_inherit = env_inherit + [base_url_env] - env_inherit = env_inherit + [key for key in network_origins_env if key not in env and key not in env_inherit] js_library(name = name + "_sources", srcs = baselines, data = data) _inputs( name = name + "_inputs", @@ -153,20 +152,18 @@ def browser_test( common = dict( copy_data_to_bin = False, entry_point = Label("//runtime:runner_entry"), - data = [":" + name + "_inputs", Label("//runtime:files")] + ([Label("//runtime:container_files")] if visual and not browser else []) + data, + data = [":" + name + "_inputs", Label("//runtime:files")] + data, env = env | { "VRT_DESCRIPTOR": "$(rlocationpath :%s_inputs)" % name, "VRT_BASE_URL": base_url or "", "VRT_BASE_URL_ENV": base_url_env or "", "VRT_MODE": "visual-spec" if visual and tests else "visual" if visual else "component" if component else "e2e", "VRT_BASELINE_RELATIVE": (native.package_name() + "/" if native.package_name() else "") + baseline_dir if visual else "", - "VRT_NETWORK_ORIGINS": json.encode(network_origins), - "VRT_NETWORK_ORIGINS_ENV": json.encode(network_origins_env), "VRT_ENV_NAMES": json.encode(env.keys() + env_inherit), "VRT_TIMEOUT_MS": str(execution_timeout_seconds * 1000), }, ) - browser_env_inherit = [] if browser else ["DOCKER_HOST", "DOCKER_CONTEXT", "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH", "DOCKER_CONFIG"] if visual else [ + browser_env_inherit = [ key for key in ["PLAYWRIGHT_BROWSERS_PATH"] if key not in env and key not in env_inherit @@ -182,5 +179,3 @@ def browser_test( timeout = timeout, **common ) - if visual: - js_binary(name = name + ".update", fixed_args = ["--update"], tags = ["manual"], **common) diff --git a/package.json b/package.json index d5d7cfd..f9b4a34 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "lefthook": "2.1.12", "playwright": "1.63.0", "playwright-core": "1.63.0", - "testcontainers": "12.1.0", "typescript": "7.0.2", "vite": "8.2.2" }, diff --git a/patches/BUILD.bazel b/patches/BUILD.bazel deleted file mode 100644 index 53e5842..0000000 --- a/patches/BUILD.bazel +++ /dev/null @@ -1 +0,0 @@ -exports_files(["testcontainers-12.1.0.patch"]) diff --git a/patches/README.md b/patches/README.md deleted file mode 100644 index 84ad72d..0000000 --- a/patches/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Testcontainers 12.1.0 execution guard - -The pnpm-locked patch combines two independent changes, enabled by `startBrowser` -in the rules' private runtime process: - -- `TESTCONTAINERS_PULL_POLICY=never` uses the implementation submitted in - [testcontainers-node#1457](https://github.com/testcontainers/testcontainers-node/pull/1457) - (commit `42c7675`). The image-client and utility hunks are the exact compiled - output of that upstream commit (`npm run build -w testcontainers`). Image startup - inspects local availability and returns without registry authentication or pulls, - including if an image disappears after preflight. Both pull paths are covered. -- `TESTCONTAINERS_PRELOADED_IMAGES_ONLY=true` retains our separate Ryuk identity - check. Existing containers are inspected before opening a cleanup connection. - Docker's actual container image ID must equal the locally resolved pinned image - ID. Cached reapers are checked too. Unverifiable or mismatched identities fail; - foreign reapers are never stopped by this check. - -This patches the pinned package rather than replacing its cleanup lifecycle or -monkey-patching shared client methods. Default Testcontainers behavior is unchanged -when the flags are absent. Replace the upstream hunks with a released dependency -once available; retain the Ryuk check until upstream supports identity verification. -Real-daemon coverage lives in `runtime/preload-browser.test.ts` and the React -example's `preloaded_vrt_test`. diff --git a/patches/testcontainers-12.1.0.patch b/patches/testcontainers-12.1.0.patch deleted file mode 100644 index 6f88e39..0000000 --- a/patches/testcontainers-12.1.0.patch +++ /dev/null @@ -1,110 +0,0 @@ -diff --git a/build/container-runtime/clients/image/docker-image-client.js b/build/container-runtime/clients/image/docker-image-client.js ---- a/build/container-runtime/clients/image/docker-image-client.js -+++ b/build/container-runtime/clients/image/docker-image-client.js -@@ -12,6 +12,7 @@ - const tar_fs_1 = __importDefault(require("tar-fs")); - const common_1 = require("../../../common"); - const get_auth_config_1 = require("../../auth/get-auth-config"); -+const use_local_image_1 = require("../../utils/use-local-image"); - class DockerImageClient { - dockerode; - indexServerAddress; -@@ -124,6 +125,9 @@ - } - async pull(imageName, opts) { - try { -+ if (await (0, use_local_image_1.useLocalImage)(this.dockerode, imageName)) { -+ return; -+ } - if (!opts?.force && (await this.exists(imageName))) { - common_1.log.debug(`Image "${imageName.string}" already exists`); - return; -diff --git a/build/container-runtime/utils/pull-image.js b/build/container-runtime/utils/pull-image.js ---- a/build/container-runtime/utils/pull-image.js -+++ b/build/container-runtime/utils/pull-image.js -@@ -8,8 +8,12 @@ - const common_1 = require("../../common"); - const get_auth_config_1 = require("../auth/get-auth-config"); - const image_exists_1 = require("./image-exists"); -+const use_local_image_1 = require("./use-local-image"); - const pullImage = async (dockerode, indexServerAddress, options) => { - try { -+ if (await (0, use_local_image_1.useLocalImage)(dockerode, options.imageName)) { -+ return; -+ } - if (!options.force && (await (0, image_exists_1.imageExists)(dockerode, options.imageName))) { - common_1.log.debug(`Not pulling image "${options.imageName.string}" as it already exists`); - return; -diff --git a/build/container-runtime/utils/use-local-image.js b/build/container-runtime/utils/use-local-image.js -new file mode 100644 ---- /dev/null -+++ b/build/container-runtime/utils/use-local-image.js -@@ -0,0 +1,16 @@ -+"use strict"; -+Object.defineProperty(exports, "__esModule", { value: true }); -+exports.useLocalImage = useLocalImage; -+async function useLocalImage(dockerode, imageName) { -+ if (process.env.TESTCONTAINERS_PULL_POLICY !== "never") { -+ return false; -+ } -+ // Bypass the existence cache: an image may have been removed since the last check. -+ try { -+ await dockerode.getImage(imageName.string).inspect(); -+ } -+ catch (cause) { -+ throw new Error(`Cannot use local image "${imageName.string}" with TESTCONTAINERS_PULL_POLICY=never; preload it before starting containers`, { cause }); -+ } -+ return true; -+} -diff --git a/build/container-runtime/utils/use-local-image.d.ts b/build/container-runtime/utils/use-local-image.d.ts -new file mode 100644 ---- /dev/null -+++ b/build/container-runtime/utils/use-local-image.d.ts -@@ -0,0 +1,3 @@ -+import Dockerode from "dockerode"; -+import { ImageName } from "../image-name"; -+export declare function useLocalImage(dockerode: Dockerode, imageName: ImageName): Promise; -diff --git a/build/reaper/reaper.js b/build/reaper/reaper.js ---- a/build/reaper/reaper.js -+++ b/build/reaper/reaper.js -@@ -25,6 +25,7 @@ - let sessionId; - async function getReaper(client) { - if (reaper) { -+ await verifyReaperImage(client, reaper.containerId); - return reaper; - } - const userId = (0, os_1.userInfo)().uid; -@@ -35,6 +36,7 @@ - return new DisabledReaper(sessionId, ""); - } - for (const reaperContainer of reaperContainers) { -+ await verifyReaperImage(client, reaperContainer.Id); - const existingSessionId = reaperContainer.Labels[labels_1.LABEL_TESTCONTAINERS_SESSION_ID] ?? new common_1.RandomUuid().nextUuid(); - try { - sessionId = existingSessionId; -@@ -52,6 +54,24 @@ - }); - reaper.addSession(sessionId); - return reaper; -+} -+// Verify before opening a cleanup connection; never remove a foreign reaper. -+async function verifyReaperImage(client, containerId) { -+ if (process.env.TESTCONTAINERS_PRELOADED_IMAGES_ONLY !== "true") return; -+ const reference = getReaperImage(); -+ if (!/@sha256:[a-f0-9]{64}$/.test(reference)) { -+ throw new Error("Ryuk requires a digest-pinned image in preloaded-only mode"); -+ } -+ let expected, actual; -+ try { -+ expected = await client.image.inspect(container_runtime_1.ImageName.fromString(reference)); -+ actual = await client.container.inspect(client.container.getById(containerId)); -+ } catch { -+ throw new Error(`Cannot verify existing Ryuk ${containerId} against ${reference}; preload the image and use a dedicated daemon`); -+ } -+ if (!expected.Id || actual.Image !== expected.Id) { -+ throw new Error(`Existing Ryuk ${containerId} does not match ${reference}; use a dedicated daemon or ask its owner to stop it`); -+ } - } - async function findReaperContainers(client) { - const containers = await client.container.list(); diff --git a/playwright/defs.bzl b/playwright/defs.bzl index bd565e3..2da5c8a 100644 --- a/playwright/defs.bzl +++ b/playwright/defs.bzl @@ -2,9 +2,7 @@ PLAYWRIGHT_IMAGE = "mcr.microsoft.com/playwright:v1.63.0-noble@sha256:bc6ab0d6d44ff4826e4cb8c1e6d801e185bfc42bb0753f8e2a30efc70db054c7" -REAPER_IMAGE = "testcontainers/ryuk:0.14.0@sha256:f0456560ea5b4acdbed0da0efc33b5f9dd6bc1e59f2337106826dcb5b0b0e981" - -PlaywrightInfo = provider(fields = ["test", "core", "version", "image", "images"]) +PlaywrightInfo = provider(fields = ["test", "core", "version"]) BrowserRuntimeInfo = provider(fields = ["descriptor"]) @@ -42,12 +40,6 @@ browser_runtime = rule( }, ) -def _images(image): - return [ - {"image": image, "platform": "linux/amd64", "roles": ["browser", "control-relay"]}, - {"image": REAPER_IMAGE, "platform": None, "roles": ["reaper"]}, - ] - def runfile(file): """Return a canonical manifest-relative path, including external repositories.""" return file.short_path[3:] if file.short_path.startswith("../") else "_main/" + file.short_path @@ -58,16 +50,13 @@ def _runtime_impl(ctx): fail("Playwright version must be a stable major.minor.patch version >= 1.63.0") if (int(parts[0]), int(parts[1]), int(parts[2])) < (1, 63, 0): fail("Playwright >= 1.63.0 is required") - image = ctx.attr.image or (PLAYWRIGHT_IMAGE if ctx.attr.version == "1.63.0" else "") - if image and "@sha256:" not in image: - fail("Browser images must be digest-pinned") files = ctx.files.test + ctx.files.core runfiles = ctx.runfiles(files = files) for target in [ctx.attr.test, ctx.attr.core]: runfiles = runfiles.merge(target[DefaultInfo].default_runfiles) return [ DefaultInfo(files = depset(files), runfiles = runfiles), - PlaywrightInfo(test = runfile(ctx.file.test), core = runfile(ctx.file.core), version = ctx.attr.version, image = image, images = _images(image) if image else []), + PlaywrightInfo(test = runfile(ctx.file.test), core = runfile(ctx.file.core), version = ctx.attr.version), ] playwright_runtime = rule( @@ -76,24 +65,5 @@ playwright_runtime = rule( "test": attr.label(mandatory = True, allow_single_file = True), "core": attr.label(mandatory = True, allow_single_file = True), "version": attr.string(default = "1.63.0"), - "image": attr.string(), - }, -) - -def _images_impl(ctx): - if not ctx.attr.playwright[PlaywrightInfo].images: - fail("Image manifests require a matching digest-pinned VRT browser image") - manifest = ctx.actions.declare_file(ctx.label.name + ".json") - ctx.actions.write(manifest, json.encode({ - "schemaVersion": 1, - "images": ctx.attr.playwright[PlaywrightInfo].images, - }) + "\n") - return [DefaultInfo(files = depset([manifest]))] - -playwright_images = rule( - implementation = _images_impl, - doc = "Docker-free image requirements for CI preloading; null platform means daemon-selected.", - attrs = { - "playwright": attr.label(mandatory = True, providers = [PlaywrightInfo]), }, ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 441cfcb..48844db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,9 +105,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -patchedDependencies: - testcontainers@12.1.0: e2b4c82137307780e2455c242a21ffc05fa282e1ec0b13c2bd63b8d39fdec30e - importers: .: @@ -133,9 +130,6 @@ importers: playwright-core: specifier: 1.63.0 version: 1.63.0 - testcontainers: - specifier: 12.1.0 - version: 12.1.0(patch_hash=e2b4c82137307780e2455c242a21ffc05fa282e1ec0b13c2bd63b8d39fdec30e) typescript: specifier: 7.0.2 version: 7.0.2 @@ -153,9 +147,6 @@ packages: resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@balena/dockerignore@1.0.2': - resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} - '@commitlint/cli@21.2.2': resolution: {integrity: sha512-a+6hQxIxnpdvSvS2apvttPNbEliYsVC3PqFYDiiB2kjbwIsQsj1urvQ4Tkf70pKYozPalKAuRQmm/GHwndduqA==} engines: {node: '>=22.12.0'} @@ -241,69 +232,14 @@ packages: resolution: {integrity: sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==} engines: {node: '>=22'} - '@grpc/grpc-js@1.14.4': - resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} - engines: {node: '>=12.10.0'} - - '@grpc/proto-loader@0.7.15': - resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} - engines: {node: '>=6'} - hasBin: true - - '@grpc/proto-loader@0.8.1': - resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} - engines: {node: '>=6'} - hasBin: true - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@js-sdsl/ordered-map@4.4.2': - resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - - '@kwsites/file-exists@1.1.1': - resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} - '@oxc-project/types@0.147.0': resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@playwright/test@1.63.0': resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} engines: {node: '>=20'} hasBin: true - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - - '@protobufjs/fetch@1.1.1': - resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.2': - resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} - '@rolldown/binding-android-arm-eabi@1.2.6': resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -411,30 +347,9 @@ packages: resolution: {integrity: sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==} engines: {node: '>=22'} - '@types/docker-modem@3.0.6': - resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} - - '@types/dockerode@4.0.1': - resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} - - '@types/node@18.19.130': - resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - - '@types/node@24.10.1': - resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} - '@types/node@26.5.0': resolution: {integrity: sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==} - '@types/ssh2-streams@0.1.13': - resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} - - '@types/ssh2@0.5.52': - resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} - - '@types/ssh2@1.15.5': - resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} - '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -555,37 +470,17 @@ packages: cpu: [x64] os: [win32] - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - ansi-regex@6.3.0: resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} engines: {node: '>=12'} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - archiver-utils@5.0.2: - resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} - engines: {node: '>= 14'} - - archiver@7.0.1: - resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} - engines: {node: '>= 14'} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -593,119 +488,14 @@ packages: resolution: {integrity: sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==} engines: {node: '>=22'} - asn1@0.2.6: - resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} - - async-lock@1.4.1: - resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} - - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - - b4a@1.8.1: - resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} - peerDependencies: - react-native-b4a: '*' - peerDependenciesMeta: - react-native-b4a: - optional: true - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - bare-events@2.9.2: - resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} - peerDependencies: - bare-abort-controller: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - - bare-fs@4.8.1: - resolution: {integrity: sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==} - engines: {bare: '>=1.28.0'} - peerDependencies: - bare-buffer: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - - bare-path@3.1.1: - resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} - - bare-stream@2.13.4: - resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} - peerDependencies: - bare-abort-controller: '*' - bare-buffer: '*' - bare-events: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - bare-buffer: - optional: true - bare-events: - optional: true - - bare-url@2.5.2: - resolution: {integrity: sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - bcrypt-pbkdf@1.0.2: - resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} - - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - - brace-expansion@2.1.4: - resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} - - buffer-crc32@1.0.0: - resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} - engines: {node: '>=8.0.0'} - - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - - buildcheck@0.0.7: - resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} - engines: {node: '>=10.0.0'} - - byline@5.0.0: - resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} - engines: {node: '>=0.10.0'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - compress-commons@6.0.2: - resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} - engines: {node: '>= 14'} - conventional-changelog-angular@9.4.0: resolution: {integrity: sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==} engines: {node: '>=22'} @@ -719,9 +509,6 @@ packages: engines: {node: '>=22'} hasBin: true - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cosmiconfig-typescript-loader@6.3.0: resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} engines: {node: '>=v18'} @@ -739,63 +526,13 @@ packages: typescript: optional: true - cpu-features@0.0.10: - resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} - engines: {node: '>=10.0.0'} - - crc-32@1.2.2: - resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} - engines: {node: '>=0.8'} - hasBin: true - - crc32-stream@6.0.0: - resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} - engines: {node: '>= 14'} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - docker-compose@1.4.2: - resolution: {integrity: sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==} - engines: {node: '>= 6.0.0'} - - docker-modem@5.0.7: - resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} - engines: {node: '>= 8.0'} - - dockerode@5.0.1: - resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==} - engines: {node: '>= 14.17'} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -810,23 +547,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - - events-universal@1.0.1: - resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} - - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fast-uri@3.1.5: resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} @@ -839,13 +562,6 @@ packages: picomatch: optional: true - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -859,32 +575,14 @@ packages: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} - get-port@5.1.1: - resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} - engines: {node: '>=8'} - - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - global-directory@5.0.0: resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} engines: {node: '>=20'} - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@6.0.0: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -892,27 +590,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -930,10 +611,6 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - lazystream@1.0.1: - resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} - engines: {node: '>= 0.6.3'} - lefthook-darwin-arm64@2.1.12: resolution: {integrity: sha512-GSUjqaCuxAYlPOovbjXEWE3HAIa/xOQkTTmZ937zieQldjPLTaC7+s5FHoxKywn+D9riBlofkDqG7Z4f5zzmPA==} cpu: [arm64] @@ -1065,59 +742,11 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - minimatch@5.1.9: - resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} - engines: {node: '>=10'} - - minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} - - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nan@2.28.0: - resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} - nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1126,14 +755,6 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1155,45 +776,6 @@ packages: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - - proper-lockfile@4.1.2: - resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - - properties-reader@3.0.1: - resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} - engines: {node: '>=18'} - - protobufjs@7.6.6: - resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} - engines: {node: '>=12.0.0'} - - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - readable-stream@4.7.0: - resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - readdir-glob@1.1.3: - resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1206,69 +788,20 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - rolldown@1.2.6: resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - split-ca@1.0.1: - resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} - - ssh-remote-port-forward@1.0.4: - resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} - - ssh2@1.17.0: - resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} - engines: {node: '>=10.16.0'} - - streamx@2.28.1: - resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -1277,43 +810,10 @@ packages: resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} engines: {node: '>=20'} - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - strip-ansi@7.2.0: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} - tar-fs@2.1.5: - resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} - - tar-fs@3.1.3: - resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - - tar-stream@3.2.1: - resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==} - - teex@1.0.1: - resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - - testcontainers@12.1.0: - resolution: {integrity: sha512-YjDLqIITuhGLMnM10yhg3oV6lIG5IMpz1R1DPBZoOOks83q7i7IVpeSWRTiyl7roozjiyLmwIoLK/KY8OnZmIA==} - engines: {node: '>= 22.22'} - - text-decoder@1.2.7: - resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} - tinyexec@1.3.0: resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} engines: {node: '>=18'} @@ -1322,34 +822,14 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tmp@0.2.7: - resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} - engines: {node: '>=14.14'} - - tweetnacl@0.14.5: - resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} - typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} hasBin: true - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici-types@8.9.0: resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} - undici@8.10.1: - resolution: {integrity: sha512-YQ3WlbqjYMmNpdvDH64jAgLjxuAR9+649calDWhbshYaeQGO2bR4nI94ORJmwI3J9YhoKQnpyGOK+0zlWS5N5Q==} - engines: {node: '>=22.19.0'} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vite@8.2.2: resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1393,26 +873,10 @@ packages: yaml: optional: true - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -1422,26 +886,14 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - yargs-parser@22.0.0: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} - yargs@17.7.3: - resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} - engines: {node: '>=12'} - yargs@18.1.0: resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} - zip-stream@6.0.1: - resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} - engines: {node: '>= 14'} - snapshots: '@babel/code-frame@7.29.7': @@ -1452,8 +904,6 @@ snapshots: '@babel/helper-validator-identifier@7.29.7': {} - '@balena/dockerignore@1.0.2': {} - '@commitlint/cli@21.2.2(@types/node@26.5.0)(conventional-commits-parser@7.1.2)(typescript@7.0.2)': dependencies: '@commitlint/config-conventional': 21.2.2 @@ -1573,71 +1023,12 @@ snapshots: '@conventional-changelog/template@1.4.0': {} - '@grpc/grpc-js@1.14.4': - dependencies: - '@grpc/proto-loader': 0.8.1 - '@js-sdsl/ordered-map': 4.4.2 - - '@grpc/proto-loader@0.7.15': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.6.6 - yargs: 17.7.3 - - '@grpc/proto-loader@0.8.1': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.6.6 - yargs: 17.7.3 - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.2.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@js-sdsl/ordered-map@4.4.2': {} - - '@kwsites/file-exists@1.1.1': - dependencies: - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - '@oxc-project/types@0.147.0': {} - '@pkgjs/parseargs@0.11.0': - optional: true - '@playwright/test@1.63.0': dependencies: playwright: 1.63.0 - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.1': {} - - '@protobufjs/fetch@1.1.1': - dependencies: - '@protobufjs/aspromise': 1.1.2 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.2': {} - '@rolldown/binding-android-arm-eabi@1.2.6': optional: true @@ -1691,42 +1082,10 @@ snapshots: '@simple-libs/stream-utils@2.0.0': {} - '@types/docker-modem@3.0.6': - dependencies: - '@types/node': 24.10.1 - '@types/ssh2': 1.15.5 - - '@types/dockerode@4.0.1': - dependencies: - '@types/docker-modem': 3.0.6 - '@types/node': 24.10.1 - '@types/ssh2': 1.15.5 - - '@types/node@18.19.130': - dependencies: - undici-types: 5.26.5 - - '@types/node@24.10.1': - dependencies: - undici-types: 7.16.0 - '@types/node@26.5.0': dependencies: undici-types: 8.9.0 - '@types/ssh2-streams@0.1.13': - dependencies: - '@types/node': 24.10.1 - - '@types/ssh2@0.5.52': - dependencies: - '@types/node': 24.10.1 - '@types/ssh2-streams': 0.1.13 - - '@types/ssh2@1.15.5': - dependencies: - '@types/node': 18.19.130 - '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -1787,10 +1146,6 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -1798,148 +1153,22 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - ansi-regex@5.0.1: {} - ansi-regex@6.3.0: {} - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - ansi-styles@6.2.3: {} - archiver-utils@5.0.2: - dependencies: - glob: 10.5.0 - graceful-fs: 4.2.11 - is-stream: 2.0.1 - lazystream: 1.0.1 - lodash: 4.18.1 - normalize-path: 3.0.0 - readable-stream: 4.7.0 - - archiver@7.0.1: - dependencies: - archiver-utils: 5.0.2 - async: 3.2.6 - buffer-crc32: 1.0.0 - readable-stream: 4.7.0 - readdir-glob: 1.1.3 - tar-stream: 3.2.1 - zip-stream: 6.0.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - argparse@2.0.1: {} argue-cli@3.1.0: {} - asn1@0.2.6: - dependencies: - safer-buffer: 2.1.2 - - async-lock@1.4.1: {} - - async@3.2.6: {} - - b4a@1.8.1: {} - - balanced-match@1.0.2: {} - - bare-events@2.9.2: {} - - bare-fs@4.8.1: - dependencies: - bare-events: 2.9.2 - bare-path: 3.1.1 - bare-stream: 2.13.4(bare-events@2.9.2) - bare-url: 2.5.2 - fast-fifo: 1.3.2 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - bare-path@3.1.1: {} - - bare-stream@2.13.4(bare-events@2.9.2): - dependencies: - b4a: 1.8.1 - streamx: 2.28.1 - teex: 1.0.1 - optionalDependencies: - bare-events: 2.9.2 - transitivePeerDependencies: - - react-native-b4a - - bare-url@2.5.2: - dependencies: - bare-path: 3.1.1 - - base64-js@1.5.1: {} - - bcrypt-pbkdf@1.0.2: - dependencies: - tweetnacl: 0.14.5 - - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - - brace-expansion@2.1.4: - dependencies: - balanced-match: 1.0.2 - - buffer-crc32@1.0.0: {} - - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - buildcheck@0.0.7: - optional: true - - byline@5.0.0: {} - callsites@3.1.0: {} - chownr@1.1.4: {} - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - cliui@9.0.1: dependencies: string-width: 7.2.0 strip-ansi: 7.2.0 wrap-ansi: 9.0.2 - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - compress-commons@6.0.2: - dependencies: - crc-32: 1.2.2 - crc32-stream: 6.0.0 - is-stream: 2.0.1 - normalize-path: 3.0.0 - readable-stream: 4.7.0 - conventional-changelog-angular@9.4.0: dependencies: '@conventional-changelog/template': 1.4.0 @@ -1953,8 +1182,6 @@ snapshots: '@simple-libs/stream-utils': 2.0.0 argue-cli: 3.1.0 - core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.3.0(@types/node@26.5.0)(cosmiconfig@9.0.2(typescript@7.0.2))(typescript@7.0.2): dependencies: '@types/node': 26.5.0 @@ -1971,67 +1198,10 @@ snapshots: optionalDependencies: typescript: 7.0.2 - cpu-features@0.0.10: - dependencies: - buildcheck: 0.0.7 - nan: 2.28.0 - optional: true - - crc-32@1.2.2: {} - - crc32-stream@6.0.0: - dependencies: - crc-32: 1.2.2 - readable-stream: 4.7.0 - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - detect-libc@2.1.2: {} - docker-compose@1.4.2: - dependencies: - yaml: 2.9.0 - - docker-modem@5.0.7: - dependencies: - debug: 4.4.3 - readable-stream: 3.6.2 - split-ca: 1.0.1 - ssh2: 1.17.0 - transitivePeerDependencies: - - supports-color - - dockerode@5.0.1: - dependencies: - '@balena/dockerignore': 1.0.2 - '@grpc/grpc-js': 1.14.4 - '@grpc/proto-loader': 0.7.15 - docker-modem: 5.0.7 - protobufjs: 7.6.6 - tar-fs: 2.1.5 - transitivePeerDependencies: - - supports-color - - eastasianwidth@0.2.0: {} - emoji-regex@10.6.0: {} - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - env-paths@2.2.1: {} error-ex@1.3.4: @@ -2042,33 +1212,14 @@ snapshots: escalade@3.2.0: {} - event-target-shim@5.0.1: {} - - events-universal@1.0.1: - dependencies: - bare-events: 2.9.2 - transitivePeerDependencies: - - bare-abort-controller - - events@3.3.0: {} - fast-deep-equal@3.1.3: {} - fast-fifo@1.3.2: {} - fast-uri@3.1.5: {} fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - fs-constants@1.0.0: {} - fsevents@2.3.3: optional: true @@ -2076,52 +1227,21 @@ snapshots: get-east-asian-width@1.6.0: {} - get-port@5.1.1: {} - - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.9 - minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - global-directory@5.0.0: dependencies: ini: 6.0.0 - graceful-fs@4.2.11: {} - - ieee754@1.2.1: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - inherits@2.0.4: {} - ini@6.0.0: {} is-arrayish@0.2.1: {} - is-fullwidth-code-point@3.0.0: {} - is-plain-obj@4.1.0: {} - is-stream@2.0.1: {} - - isarray@1.0.0: {} - - isexe@2.0.0: {} - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jiti@2.6.1: {} js-tokens@4.0.0: {} @@ -2134,10 +1254,6 @@ snapshots: json-schema-traverse@1.0.0: {} - lazystream@1.0.1: - dependencies: - readable-stream: 2.3.8 - lefthook-darwin-arm64@2.1.12: optional: true @@ -2232,43 +1348,8 @@ snapshots: lines-and-columns@1.2.4: {} - lodash.camelcase@4.3.0: {} - - lodash@4.18.1: {} - - long@5.3.2: {} - - lru-cache@10.4.3: {} - - minimatch@5.1.9: - dependencies: - brace-expansion: 2.1.4 - - minimatch@9.0.9: - dependencies: - brace-expansion: 2.1.4 - - minipass@7.1.3: {} - - mkdirp-classic@0.5.3: {} - - mkdirp@3.0.1: {} - - ms@2.1.3: {} - - nan@2.28.0: - optional: true - nanoid@3.3.18: {} - normalize-path@3.0.0: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - package-json-from-dist@1.0.1: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -2280,13 +1361,6 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - path-key@3.1.1: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.3 - picocolors@1.1.1: {} picomatch@4.0.7: {} @@ -2303,80 +1377,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - process-nextick-args@2.0.1: {} - - process@0.11.10: {} - - proper-lockfile@4.1.2: - dependencies: - graceful-fs: 4.2.11 - retry: 0.12.0 - signal-exit: 3.0.7 - - properties-reader@3.0.1: - dependencies: - '@kwsites/file-exists': 1.1.1 - mkdirp: 3.0.1 - transitivePeerDependencies: - - supports-color - - protobufjs@7.6.6: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.1 - '@protobufjs/fetch': 1.1.1 - '@protobufjs/float': 1.0.2 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.2 - '@types/node': 24.10.1 - long: 5.3.2 - - pump@3.0.4: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - readable-stream@4.7.0: - dependencies: - abort-controller: 3.0.0 - buffer: 6.0.3 - events: 3.3.0 - process: 0.11.10 - string_decoder: 1.3.0 - - readdir-glob@1.1.3: - dependencies: - minimatch: 5.1.9 - - require-directory@2.1.1: {} - require-from-string@2.0.2: {} resolve-from@4.0.0: {} resolve-from@5.0.0: {} - retry@0.12.0: {} - rolldown@1.2.6: dependencies: '@oxc-project/types': 0.147.0 @@ -2398,62 +1404,10 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.6 '@rolldown/binding-win32-x64-msvc': 1.2.6 - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} - - safer-buffer@2.1.2: {} - semver@7.8.5: {} - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - signal-exit@3.0.7: {} - - signal-exit@4.1.0: {} - source-map-js@1.2.1: {} - split-ca@1.0.1: {} - - ssh-remote-port-forward@1.0.4: - dependencies: - '@types/ssh2': 0.5.52 - ssh2: 1.17.0 - - ssh2@1.17.0: - dependencies: - asn1: 0.2.6 - bcrypt-pbkdf: 1.0.2 - optionalDependencies: - cpu-features: 0.0.10 - nan: 2.28.0 - - streamx@2.28.1: - dependencies: - events-universal: 1.0.1 - fast-fifo: 1.3.2 - text-decoder: 1.2.7 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.2.0 - string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -2465,96 +1419,10 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - strip-ansi@7.2.0: dependencies: ansi-regex: 6.3.0 - tar-fs@2.1.5: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.4 - tar-stream: 2.2.0 - - tar-fs@3.1.3: - dependencies: - pump: 3.0.4 - tar-stream: 3.2.1 - optionalDependencies: - bare-fs: 4.8.1 - bare-path: 3.1.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - - tar-stream@3.2.1: - dependencies: - b4a: 1.8.1 - bare-fs: 4.8.1 - fast-fifo: 1.3.2 - streamx: 2.28.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - teex@1.0.1: - dependencies: - streamx: 2.28.1 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - testcontainers@12.1.0(patch_hash=e2b4c82137307780e2455c242a21ffc05fa282e1ec0b13c2bd63b8d39fdec30e): - dependencies: - '@balena/dockerignore': 1.0.2 - '@types/dockerode': 4.0.1 - archiver: 7.0.1 - async-lock: 1.4.1 - byline: 5.0.0 - debug: 4.4.3 - docker-compose: 1.4.2 - dockerode: 5.0.1 - get-port: 5.1.1 - proper-lockfile: 4.1.2 - properties-reader: 3.0.1 - ssh-remote-port-forward: 1.0.4 - tar-fs: 3.1.3 - tmp: 0.2.7 - undici: 8.10.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - supports-color - - text-decoder@1.2.7: - dependencies: - b4a: 1.8.1 - transitivePeerDependencies: - - react-native-b4a - tinyexec@1.3.0: {} tinyglobby@0.2.17: @@ -2562,10 +1430,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 - tmp@0.2.7: {} - - tweetnacl@0.14.5: {} - typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -2589,16 +1453,8 @@ snapshots: '@typescript/typescript-win32-arm64': 7.0.2 '@typescript/typescript-win32-x64': 7.0.2 - undici-types@5.26.5: {} - - undici-types@7.16.0: {} - undici-types@8.9.0: {} - undici@8.10.1: {} - - util-deprecate@1.0.2: {} - vite@8.2.2(@types/node@26.5.0)(jiti@2.6.1)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 @@ -2612,48 +1468,19 @@ snapshots: jiti: 2.6.1 yaml: 2.9.0 - which@2.0.2: - dependencies: - isexe: 2.0.0 - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.2.0 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 string-width: 7.2.0 strip-ansi: 7.2.0 - wrappy@1.0.2: {} - y18n@5.0.8: {} - yaml@2.9.0: {} - - yargs-parser@21.1.1: {} + yaml@2.9.0: + optional: true yargs-parser@22.0.0: {} - yargs@17.7.3: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yargs@18.1.0: dependencies: cliui: 9.0.1 @@ -2662,9 +1489,3 @@ snapshots: string-width: 8.2.2 y18n: 5.0.8 yargs-parser: 22.0.0 - - zip-stream@6.0.1: - dependencies: - archiver-utils: 5.0.2 - compress-commons: 6.0.2 - readable-stream: 4.7.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bb85a08..870c90b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,8 +1,5 @@ allowBuilds: - cpu-features: false lefthook: true - protobufjs: false - ssh2: false # Exact releases requested for the current toolchain upgrade. minimumReleaseAgeExclude: @@ -10,6 +7,3 @@ minimumReleaseAgeExclude: - "playwright@1.63.0" - "playwright-core@1.63.0" - "@types/node@26.5.0" - -patchedDependencies: - testcontainers@12.1.0: patches/testcontainers-12.1.0.patch diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 0e690a2..7bccb51 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -1,7 +1,7 @@ load("@aspect_rules_js//js:defs.bzl", "js_library", "js_test") load("@aspect_rules_js//npm:defs.bzl", "npm_package") load("@aspect_rules_ts//ts:defs.bzl", "ts_project") -load("//playwright:defs.bzl", "playwright_images", "playwright_runtime") +load("//playwright:defs.bzl", "playwright_runtime") ts_project( name = "typecheck", @@ -30,7 +30,6 @@ ts_project( "//:node_modules/@playwright/test", "//:node_modules/@types/node", "//:node_modules/playwright", - "//:node_modules/testcontainers", ], ) @@ -57,13 +56,6 @@ js_library( visibility = ["//visibility:public"], ) -js_library( - name = "container_files", - srcs = ["container.js", "container-host.js", "relay.js", "package.json"], - deps = ["//:node_modules/testcontainers"], - visibility = ["//visibility:public"], -) - filegroup( name = "runner_entry", srcs = ["runner.js"], @@ -211,55 +203,13 @@ js_test( ) -playwright_images( - name = "images", - playwright = ":playwright", - visibility = ["//visibility:public"], -) - -playwright_runtime( - name = "manifest_override_runtime", - core = "//:node_modules/playwright-core/dir", - test = "//:node_modules/@playwright/test/dir", - image = "registry.example/browser@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - testonly = True, -) - -playwright_images( - name = "manifest_override_images", - playwright = ":manifest_override_runtime", - testonly = True, -) - -js_test( - name = "images_test", - size = "small", - data = ["package.json", ":typecheck", ":images", ":manifest_override_images"], - entry_point = "images.test.js", - args = ["$(rootpath :images)", "$(rootpath :manifest_override_images)"], -) - - -js_test( - name = "preload_browser_test", - size = "large", - copy_data_to_bin = False, - data = ["package.json", ":typecheck", ":images"], - entry_point = "preload-browser.test.js", - args = ["$(rootpath :images)"], - env_inherit = ["DOCKER_HOST"], - tags = ["manual", "no-sandbox", "no-remote", "no-cache"], -) - - js_test( name = "capture_browser_test", copy_data_to_bin = False, size = "large", - data = ["package.json", ":typecheck", ":images"], + data = ["package.json", ":typecheck"], entry_point = "capture-browser.test.js", - args = ["$(rootpath :images)"], - env_inherit = ["DOCKER_HOST", "DOCKER_CONTEXT", "DOCKER_CONFIG", "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH", "CAPTURE_CHROMIUM_EXECUTABLE"], + env_inherit = ["PLAYWRIGHT_BROWSERS_PATH", "CAPTURE_CHROMIUM_EXECUTABLE"], tags = ["manual", "no-sandbox", "no-remote", "no-cache"], ) @@ -270,9 +220,3 @@ js_test( data = ["package.json", ":typecheck"], entry_point = "host-browser.test.js", ) - -filegroup( - name = "preload_lifecycle_entry", - srcs = ["preload-lifecycle.test.js"], - visibility = ["//visibility:public"], -) diff --git a/runtime/capture-browser.test.ts b/runtime/capture-browser.test.ts index 0ba0ef6..26745b1 100644 --- a/runtime/capture-browser.test.ts +++ b/runtime/capture-browser.test.ts @@ -43,31 +43,11 @@ const address = server.address() assert(address && typeof address !== 'string') let stopBrowser: (() => Promise) | undefined try { - const executable = process.env.CAPTURE_CHROMIUM_EXECUTABLE - let endpoint: string - if (executable) { - const browser = await chromium.launchServer({executablePath: executable}) - endpoint = browser.wsEndpoint() - stopBrowser = () => browser.close() - } else { - const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')) as { - images: {image: string; platform: string | null; roles: string[]}[] - } - const browserImage = manifest.images.find(image => image.roles.includes('browser'))! - const reaperImage = manifest.images.find(image => image.roles.includes('reaper'))! - // Match the runner: configure the shared helper pin before importing Testcontainers. - for (const key of Object.keys(process.env)) - if (key.startsWith('TESTCONTAINERS_') || key.startsWith('RYUK_')) delete process.env[key] - process.env.RYUK_CONTAINER_IMAGE = reaperImage.image - const {startBrowser} = await import('./container.js') - const browser = await startBrowser( - browserImage.image, - path.dirname(createRequire(require.resolve('playwright/package.json')).resolve('playwright-core/package.json')), - browserImage.platform! - ) - endpoint = browser.endpoint - stopBrowser = browser.stop - } + const browserServer = await chromium.launchServer({ + executablePath: process.env.CAPTURE_CHROMIUM_EXECUTABLE, + }) + const endpoint = browserServer.wsEndpoint() + stopBrowser = () => browserServer.close() fs.writeFileSync(path.join(temp, 'package.json'), '{"type":"module"}') fs.mkdirSync(path.join(temp, 'node_modules', '@playwright'), {recursive:true}) fs.symlinkSync(path.dirname(fs.realpathSync(require.resolve('@playwright/test/package.json'))), diff --git a/runtime/config.test.ts b/runtime/config.test.ts index aa07353..3453c57 100644 --- a/runtime/config.test.ts +++ b/runtime/config.test.ts @@ -8,7 +8,6 @@ test('component gallery resolves base paths for host browser launch', () => { VRT_APP_URL: 'https://preview.example/app/', VRT_OUTPUTS: '/tmp/results', VRT_WS_ENDPOINT: 'ws://127.0.0.1:1234', - VRT_NETWORK_ORIGINS: '["https://auth.example"]', }) try { const config = componentBrowserConfig({ @@ -66,8 +65,7 @@ test('compiled suite config preserves runner paths and accepts a pixel-count bud VRT_APP_URL: 'http://127.0.0.1:1234/', VRT_OUTPUTS: temp, VRT_BASELINES: join(temp, 'baselines'), - VRT_WS_ENDPOINT: 'ws://127.0.0.1:5678', - VRT_NETWORK_ORIGINS: '[]', + VRT_CHROMIUM_EXECUTABLE: '/runtime/chromium', VRT_TEST_ROOT: temp, VRT_CONFIG_OVERRIDE: join(temp, 'custom.js'), VRT_MATCHING: join(temp, 'matching.js'), @@ -102,7 +100,8 @@ test('compiled suite config preserves runner paths and accepts a pixel-count bud [realpathSync(join(temp, 'reporter.js')), {project: 'example'}], ['json', {outputFile: 'extra.json'}], ]) - assert.equal(config.use?.connectOptions?.wsEndpoint, 'ws://127.0.0.1:5678') + assert.equal(config.use?.connectOptions, undefined) + assert.equal(config.use?.launchOptions?.executablePath, '/runtime/chromium') assert.deepEqual(config.use?.viewport, {width: 500, height: 300}) assert.equal(config.expect?.toHaveScreenshot?.maxDiffPixels, 7) assert.equal(config.expect?.toHaveScreenshot?.scale, 'device') @@ -165,8 +164,7 @@ test('compiled component suite preserves remote gallery paths and literal spec f VRT_MODE: 'component', VRT_APP_URL: 'https://preview.example/app/gallery.html?fixture=1', VRT_OUTPUTS: '/outputs', - VRT_WS_ENDPOINT: 'ws://127.0.0.1:5678', - VRT_NETWORK_ORIGINS: '[]', + VRT_CHROMIUM_EXECUTABLE: '/runtime/chromium', VRT_TEST_ROOT: '/compiled', VRT_TEST_FILES: JSON.stringify([file]), }) @@ -233,7 +231,7 @@ test('suite config rejects explicit native discovery instead of broadening selec Object.assign(process.env, { VRT_MODE: 'e2e', VRT_APP_URL: 'http://localhost:1234', VRT_OUTPUTS: temp, VRT_WS_ENDPOINT: 'ws://localhost:5678', - VRT_NETWORK_ORIGINS: '[]', VRT_TEST_ROOT: temp, + VRT_TEST_ROOT: temp, VRT_TEST_FILES: JSON.stringify([join(temp, 'auth.spec.js'), join(temp, 'app.spec.js')]), }) delete process.env.VRT_MATCHING diff --git a/runtime/config.ts b/runtime/config.ts index 8e16e19..23827d6 100644 --- a/runtime/config.ts +++ b/runtime/config.ts @@ -1,5 +1,4 @@ import path from 'node:path' -import {networkTargets} from './network.js' import type {PlaywrightTestConfig} from '@playwright/test' export interface BrowserConfigOptions { @@ -70,23 +69,11 @@ export function visualConfig({ ...defaults, use: { ...defaults.use, - ...(process.env.VRT_CHROMIUM_EXECUTABLE - ? { - launchOptions: { - executablePath: process.env.VRT_CHROMIUM_EXECUTABLE, - chromiumSandbox: false, - args: ['--no-zygote'], - }, - } - : { - connectOptions: { - wsEndpoint: required('VRT_WS_ENDPOINT'), - exposeNetwork: networkTargets( - required('VRT_APP_URL'), - JSON.parse(required('VRT_NETWORK_ORIGINS')) as string[] - ), - }, - }), + launchOptions: { + executablePath: required('VRT_CHROMIUM_EXECUTABLE'), + chromiumSandbox: false, + args: ['--no-zygote'], + }, }, testMatch: '**/.rules-visual.spec.ts', testIgnore: [], diff --git a/runtime/container-host.ts b/runtime/container-host.ts deleted file mode 100644 index caf9f30..0000000 --- a/runtime/container-host.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** Prevent Testcontainers' conditional, unpinned Alpine gateway probe. */ -export function containerHostOverride( - inContainer: boolean, - dockerHost: string | undefined, - hasProperties: boolean -): string | undefined { - // Testcontainers only uses the gateway probe when /.dockerenv exists. - if (!inContainer) return undefined - if (hasProperties) - throw new Error( - 'Containerized browser runners do not support .testcontainers.properties; ' + - 'configure the remote daemon with DOCKER_HOST and DOCKER_TLS_VERIFY/DOCKER_CERT_PATH' - ) - const url = dockerHost ? new URL(dockerHost) : undefined - if (!url || !['tcp:', 'http:', 'https:'].includes(url.protocol) || !url.hostname) - throw new Error( - 'Containerized browser runners require an explicit TCP/HTTP(S) DOCKER_HOST; ' + - 'socket/gateway discovery can require images outside the runtime manifest' - ) - // Also suppress gateway discovery if Testcontainers tries a socket fallback. - return url.hostname.replace(/^\[|\]$/g, '') -} diff --git a/runtime/container.ts b/runtime/container.ts deleted file mode 100644 index b1fa6b5..0000000 --- a/runtime/container.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { - GenericContainer, - getContainerRuntimeClient, - getReaper, - ImageName, - LABEL_TESTCONTAINERS_SESSION_ID, - StartedNetwork, - Wait, -} from 'testcontainers' -import {fileURLToPath} from 'node:url' -import type {StartedTestContainer} from 'testcontainers' -import {randomUUID} from 'node:crypto' -import fs from 'node:fs' -import os from 'node:os' -import path from 'node:path' -import {containerHostOverride} from './container-host.js' - -export async function startBrowser( - image: string, - core: string, - platform = 'linux/amd64' -) { - const host = containerHostOverride( - fs.existsSync('/.dockerenv'), - process.env.DOCKER_HOST, - fs.existsSync(path.join(os.homedir(), '.testcontainers.properties')) - ) - if (host) process.env.TESTCONTAINERS_HOST_OVERRIDE = host - // The pinned Testcontainers patch enforces this before auth, pulls, or reuse. - process.env.TESTCONTAINERS_PULL_POLICY = 'never' - process.env.TESTCONTAINERS_PRELOADED_IMAGES_ONLY = 'true' - const client = await getContainerRuntimeClient() - for (const reference of [image, process.env.RYUK_CONTAINER_IMAGE || '']) { - if (!/@sha256:[a-f0-9]{64}$/.test(reference)) - throw new Error(`VRT requires a digest-pinned image: ${reference}`) - try { - await client.image.inspect(ImageName.fromString(reference)) - } catch { - throw new Error(`Preload required image ${reference} before running VRT; registry access is disabled`) - } - } - const reaper = await getReaper(client) - const name = `vrt-${randomUUID()}` - const network = new StartedNetwork( - client, - name, - await client.network.create({ - Name: name, - Driver: 'bridge', - Internal: true, - Labels: { - 'org.testcontainers': 'true', - [LABEL_TESTCONTAINERS_SESSION_ID]: reaper.sessionId, - }, - }) - ) - const pending = new Set() - class BrowserContainer extends GenericContainer { - constructor(image: string) { - super(image) - this.hostConfig.Init = true - this.withLabels({'rules.web-e2e.invocation': name}) - } - protected override async containerCreated(id: string) { - pending.add(id) - } - } - const owned: StartedTestContainer[] = [] - const stop = async () => { - const failures: unknown[] = [] - for (const container of owned.splice(0).reverse()) { - try { - await container.stop() - pending.delete(container.getId()) - } catch (error) { - failures.push(error) - } - } - // Testcontainers invokes containerCreated after copying files. Discover our - // labeled resources too, so failed archive copies cannot leak containers. - for (const container of await client.container.dockerode.listContainers({ - all: true, - filters: {label: [`rules.web-e2e.invocation=${name}`]}, - })) - pending.add(container.Id) - for (const id of pending) { - try { - await client.container.getById(id).remove({force: true, v: true}) - pending.delete(id) - } catch (error) { - if ((error as {statusCode?: number}).statusCode !== 404) - failures.push(error) - } - } - try { - await network.stop() - } catch (error) { - failures.push(error) - } - if (failures.length) - throw new AggregateError(failures, 'Container cleanup failed') - } - try { - const container = await new BrowserContainer(image) - .withPlatform(platform) - .withNetwork(network) - .withSharedMemorySize(1024 * 1024 * 1024) - .withEnvironment({PLAYWRIGHT_BROWSERS_PATH: '/ms-playwright'}) - .withCopyDirectoriesToContainer([ - {source: core, target: '/opt/playwright-core'}, - ]) - .withCommand([ - 'node', - '/opt/playwright-core/cli.js', - 'run-server', - '--host', - '0.0.0.0', - '--port', - '3000', - ]) - .withWaitStrategy(Wait.forLogMessage('Listening on')) - .withStartupTimeout(120_000) - .start() - owned.push(container) - const architecture = await container.exec([ - 'node', - '-p', - 'process.platform + "/" + process.arch', - ]) - if (architecture.output.trim() !== 'linux/x64') - throw new Error(`Unexpected browser platform: ${architecture.output}`) - // Docker does not publish ports on internal-only networks. A fixed TCP relay - // exposes the control socket without giving the browser an external route. - class ControlRelay extends BrowserContainer { - constructor() { - super(image) - this.withExposedPorts(3000) - this.hostConfig.PortBindings = { - '3000/tcp': [{HostIp: '127.0.0.1', HostPort: '0'}], - } - } - protected override async containerCreated(id: string) { - await super.containerCreated(id) - await client.container.connectToNetwork( - client.container.getById(id), - client.network.getById(network.getId()), - [] - ) - } - } - const relay = await new ControlRelay() - .withPlatform(platform) - .withEnvironment({ - VRT_BROWSER_HOST: container.getIpAddress(network.getName()), - }) - .withCopyFilesToContainer([ - { - source: fileURLToPath(new URL('./relay.js', import.meta.url)), - target: '/opt/relay.js', - }, - ]) - .withCommand(['node', '/opt/relay.js']) - .withWaitStrategy( - Wait.forAll([ - Wait.forListeningPorts(), - Wait.forLogMessage('Control relay ready'), - ]) - ) - .withStartupTimeout(30_000) - .start() - owned.push(relay) - return { - endpoint: `ws://${relay.getHost()}:${relay.getMappedPort(3000)}/`, - stop, - } - } catch (error) { - try { - await stop() - } catch (cleanup) { - console.error(cleanup) - } - throw error - } -} diff --git a/runtime/fixtures/docker-proxy.ts b/runtime/fixtures/docker-proxy.ts deleted file mode 100644 index a038296..0000000 --- a/runtime/fixtures/docker-proxy.ts +++ /dev/null @@ -1,120 +0,0 @@ -// Test-only Docker boundary: real containers, with registry operations denied. -import http from 'node:http' -import {once} from 'node:events' - -export async function dockerProxy() { - const daemon = process.env.DOCKER_HOST - const url = daemon && !daemon.startsWith('unix:') - ? new URL(daemon.replace(/^tcp:/, 'http:')) : undefined - if (url && url.protocol !== 'http:') throw new Error('Test proxy requires TCP or Unix Docker') - const upstream = url - ? {hostname: url.hostname, port: url.port} - : {socketPath: daemon?.replace(/^unix:\/\//, '') || '/var/run/docker.sock'} - const state = { - forbidden: 0, - reapers: new Set(), - missingImages: new Set(), - unverifiableContainers: new Set(), - containers: [] as {id: string; image: string}[], - networks: [] as string[], - } - const api = (route: string, method = 'GET', body?: unknown): Promise => new Promise((resolve, reject) => { - const request = http.request({...upstream, path: route, method, headers: {'Content-Type': 'application/json'}}, response => { - const chunks: Buffer[] = [] - response.on('data', chunk => chunks.push(chunk)) - response.on('end', () => { - const text = Buffer.concat(chunks).toString() - if (response.statusCode! >= 400) reject(new Error(`Docker ${response.statusCode}: ${text}`)) - else resolve(text ? JSON.parse(text) : undefined) - }) - }) - request.on('error', reject) - request.end(body === undefined ? undefined : JSON.stringify(body)) - }) - const server = http.createServer(async (request, response) => { - const route = request.url! - const pathname = decodeURIComponent(route.split('?')[0].replace(/^\/v[\d.]+/, '')) - if (/^\/(images\/create|auth)$/.test(pathname)) { - state.forbidden++ - response.writeHead(403).end('Registry access forbidden') - return - } - const image = pathname.match(/^\/images\/(.+)\/json$/)?.[1] - const container = pathname.match(/^\/containers\/(.+)\/json$/)?.[1] - if (image && state.missingImages.has(image)) { - response.writeHead(404, {'Content-Type': 'application/json'}).end(JSON.stringify({message: 'No such image'})) - return - } - if (container && state.unverifiableContainers.has(container)) { - response.writeHead(500).end('Image identity unavailable') - return - } - const chunks: Buffer[] = [] - for await (const chunk of request) chunks.push(Buffer.from(chunk)) - const body = Buffer.concat(chunks) - const forward = http.request({...upstream, path: route, method: request.method, headers: request.headers}, result => { - response.on('close', () => { result.destroy(); forward.destroy() }) - const inspectResponse = pathname === '/containers/json' || pathname === '/containers/create' || pathname === '/networks/create' - if (!inspectResponse) { - response.writeHead(result.statusCode!, result.headers) - result.pipe(response) - return - } - const parts: Buffer[] = [] - result.on('data', part => parts.push(part)) - result.on('end', () => { - let payload = JSON.parse(Buffer.concat(parts).toString()) - if (result.statusCode! < 300) { - if (pathname === '/containers/json') - payload = payload.filter((item: any) => item.Labels?.['org.testcontainers.ryuk'] !== 'true' || state.reapers.has(item.Id)) - if (pathname === '/containers/create') state.containers.push({id: payload.Id, image: JSON.parse(body.toString()).Image}) - if (pathname === '/networks/create') state.networks.push(payload.Id) - } - response.writeHead(result.statusCode!, {'Content-Type': 'application/json'}) - response.end(JSON.stringify(payload)) - }) - }) - forward.on('error', error => response.destroy(error)) - forward.end(body) - }) - server.on('upgrade', (request, socket, head) => { - const forward = http.request({...upstream, path: request.url, method: request.method, headers: request.headers}) - forward.on('upgrade', (response, remote, remoteHead) => { - socket.write(`HTTP/1.1 ${response.statusCode} ${response.statusMessage}\r\n` + - Object.entries(response.headers).map(([key, value]) => `${key}: ${value}`).join('\r\n') + '\r\n\r\n') - if (head.length) remote.write(head) - if (remoteHead.length) socket.write(remoteHead) - socket.pipe(remote).pipe(socket) - socket.on('error', () => remote.destroy()) - remote.on('error', () => socket.destroy()) - socket.on('close', () => remote.destroy()) - remote.on('close', () => socket.destroy()) - }) - forward.on('error', () => socket.destroy()) - forward.end() - }) - server.listen(0, '127.0.0.1') - await once(server, 'listening') - const address = server.address() - if (!address || typeof address === 'string') throw new Error('Missing proxy address') - return { - ...state, api, host: `tcp://127.0.0.1:${address.port}`, - // Keep the counter live rather than copying its initial value. - get forbidden() { return state.forbidden }, - close() { server.closeAllConnections(); server.close() }, - } -} - -/** A different image running the real Ryuk binary, with its own cleanup lifetime. */ -export async function alternateReaper(proxy: Awaited>, original: string) { - const info = await proxy.api(`/containers/${original}/json`) - const image: string = (await proxy.api(`/commit?container=${original}&pause=false&changes=${encodeURIComponent('LABEL rules.web-e2e.test-alternate=true')}`, 'POST')).Id - const id: string = (await proxy.api('/containers/create', 'POST', { - Image: image, - Env: [...info.Config.Env.filter((entry: string) => !entry.startsWith('RYUK_CONNECTION_TIMEOUT=')), 'RYUK_CONNECTION_TIMEOUT=300s'], - Labels: {'org.testcontainers.ryuk': 'true', 'org.testcontainers.session-id': 'preload-alternate'}, - HostConfig: {Binds: info.HostConfig.Binds, PortBindings: {'8080/tcp': [{HostIp: '127.0.0.1', HostPort: '0'}]}}, - })).Id - await proxy.api(`/containers/${id}/start`, 'POST') - return {id, image} -} diff --git a/runtime/fixtures/preload-worker.ts b/runtime/fixtures/preload-worker.ts deleted file mode 100644 index a7a7e0d..0000000 --- a/runtime/fixtures/preload-worker.ts +++ /dev/null @@ -1,36 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import {createRequire} from 'node:module' -import {chromium} from 'playwright' -import {startBrowser} from '../container.js' -import {GenericContainer} from 'testcontainers' -const require = createRequire(import.meta.url) -let stop: (() => Promise) | undefined -try { - if (process.env.DIRECT_IMAGE) { - process.env.TESTCONTAINERS_PULL_POLICY = 'never' - process.env.TESTCONTAINERS_PRELOADED_IMAGES_ONLY = 'true' - await new GenericContainer(process.env.DIRECT_IMAGE).start() - throw new Error('A missing image unexpectedly started') - } - const browser = await startBrowser(process.env.BROWSER_IMAGE!, - path.dirname(createRequire(require.resolve('playwright/package.json')).resolve('playwright-core/package.json'))) - stop = browser.stop - const client = await chromium.connect(browser.endpoint) - try { - const page = await client.newPage() - await page.setContent('

Preloaded browser

') - if (await page.locator('h1').textContent() !== 'Preloaded browser') throw new Error('Browser did not render') - } finally { await client.close() } - await stop() - stop = undefined - process.send?.({ready: true}) - // Keep the original Ryuk connection alive while other workers exercise reuse. - await new Promise(resolve => process.once('message', () => resolve())) -} catch (error) { - process.send?.({error: String(error)}) - process.exitCode = 1 -} finally { - await stop?.() - process.disconnect?.() -} diff --git a/runtime/images.test.ts b/runtime/images.test.ts deleted file mode 100644 index 4b616c5..0000000 --- a/runtime/images.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import assert from 'node:assert/strict' -import fs from 'node:fs' -import {test} from 'node:test' -import {containerHostOverride} from './container-host.js' - -test('runtime manifest follows browser overrides and deduplicates the relay', () => { - const [standard, overridden] = process.argv.slice(2).map(file => - JSON.parse(fs.readFileSync(file, 'utf8')) as { - schemaVersion: number - images: {image: string; platform: string | null; roles: string[]}[] - }) - assert.equal(standard.schemaVersion, 1) - const browser = overridden.images.find(image => image.roles.includes('browser'))! - assert.equal(browser.image, - 'registry.example/browser@sha256:' + 'a'.repeat(64)) - assert.equal(browser.platform, 'linux/amd64') - assert.deepEqual(browser.roles, ['browser', 'control-relay']) - assert.notEqual(browser.image, standard.images[0].image) - assert.deepEqual(overridden.images[1], standard.images[1]) - assert.deepEqual(standard.images[1].roles, ['reaper']) - assert.equal(standard.images[1].platform, null) - for (const manifest of [standard, overridden]) { - assert.equal(manifest.images.length, 2) - assert.equal(new Set(manifest.images.map(image => JSON.stringify([image.image, image.platform]))).size, 2) - for (const image of manifest.images) assert.match(image.image, /@sha256:[a-f0-9]{64}$/) - } -}) - -test('containerized daemon discovery cannot invoke an unlisted gateway helper', () => { - assert.equal(containerHostOverride(false, undefined, false), undefined) - assert.equal(containerHostOverride(true, 'tcp://docker.example:2376', false), 'docker.example') - assert.equal(containerHostOverride(true, 'https://[::1]:2376', false), '::1') - for (const host of [undefined, 'unix:///var/run/docker.sock', 'ssh://docker.example']) - assert.throws(() => containerHostOverride(true, host, false), /explicit TCP\/HTTP/) - assert.throws(() => containerHostOverride(true, 'tcp://docker.example:2376', true), /properties/) -}) diff --git a/runtime/isolation.test.ts b/runtime/isolation.test.ts index 1ecf0db..5158945 100644 --- a/runtime/isolation.test.ts +++ b/runtime/isolation.test.ts @@ -3,7 +3,6 @@ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import {test} from 'node:test' -import {networkTargets} from './network.js' import {stageRunfiles, testEnvironment} from './isolation.js' test('staging excludes adjacent files and preserves a single npm package identity', t => { @@ -63,21 +62,3 @@ test('undeclared shell variables cannot change compare versus update', t => { assert.equal(update.FIXTURE, 'declared') assert.equal(update.HOME, path.join(root, 'home')) }) - -test('network opt-ins require explicit origins and keep port boundaries', () => { - assert.equal( - networkTargets('http://127.0.0.1:4567/', [ - 'https://fixtures.example', - 'http://localhost:8080', - ]), - '127.0.0.1:4567,fixtures.example:443,localhost:8080' - ) - for (const origin of [ - '*', - 'https://*.example', - 'https://user:pass@example.com', - 'https://example.com/path', - 'file:///tmp/file', - ]) - assert.throws(() => networkTargets('http://127.0.0.1:4567', [origin])) -}) diff --git a/runtime/network.test.ts b/runtime/network.test.ts index 3e7eb3b..f8d61f7 100644 --- a/runtime/network.test.ts +++ b/runtime/network.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import {test} from 'node:test' -import {networkTargets, remoteAppUrl, environmentOrigins} from './network.js' +import {remoteAppUrl} from './network.js' test('remote URL selection preserves paths and permits only explicit destinations', () => { const url = remoteAppUrl({ @@ -8,10 +8,6 @@ test('remote URL selection preserves paths and permits only explicit destination APP_URL: 'https://app.example/nested/?locale=fr', })! assert.equal(url, 'https://app.example/nested/?locale=fr') - assert.equal( - networkTargets(url, ['https://auth.example', 'http://127.0.0.1:8080']), - 'app.example:443,auth.example:443,127.0.0.1:8080' - ) assert.equal( remoteAppUrl({VRT_BASE_URL: 'http://localhost:8080/'}), 'http://localhost:8080/' @@ -35,30 +31,4 @@ test('remote mode fails closed for missing or malformed endpoints', () => { {VRT_BASE_URL: 'https://app.example', VRT_BASE_URL_ENV: 'APP_URL'}, ]) assert.throws(() => remoteAppUrl(env)) - for (const origin of [ - 'https://*.example', - 'https://app.example/path', - 'file:///tmp', - ]) - assert.throws(() => networkTargets('https://app.example', [origin])) -}) - -test('only declared environment endpoints join the exact origin allowlist', () => { - const env = { - BASELINE_URL: 'https://baseline.example', - UNDECLARED: 'https://other.example', - } - assert.equal( - networkTargets( - 'http://localhost:8080', - environmentOrigins(['BASELINE_URL', 'UNSET'], env) - ), - 'localhost:8080,baseline.example:443' - ) - assert.throws(() => - networkTargets( - 'http://localhost:8080', - environmentOrigins(['BASELINE_URL'], {BASELINE_URL: 'https://*.example'}) - ) - ) }) diff --git a/runtime/network.ts b/runtime/network.ts index 9375ed2..95c522e 100644 --- a/runtime/network.ts +++ b/runtime/network.ts @@ -1,32 +1,3 @@ -/** Turn explicit HTTP origins into exact Playwright tunnel host:port entries. */ -export function environmentOrigins( - names: string[], - env: NodeJS.ProcessEnv -): string[] { - return names.flatMap(name => (env[name] ? [env[name]!] : [])) -} - -export function networkTargets(fixture: string, origins: string[]): string { - return [new URL(fixture).origin, ...origins] - .map(origin => { - const url = new URL(origin) - if ( - !['http:', 'https:'].includes(url.protocol) || - url.username || - url.password || - url.pathname !== '/' || - url.search || - url.hash || - /[*,]/.test(url.hostname) - ) - throw new Error( - `Expected an HTTP(S) origin without credentials or wildcards: ${origin}` - ) - return `${url.hostname}:${url.port || (url.protocol === 'https:' ? '443' : '80')}` - }) - .join(',') -} - /** Select an explicitly declared existing endpoint without starting a server. */ export function remoteAppUrl(env: NodeJS.ProcessEnv): string | undefined { const variable = env.VRT_BASE_URL_ENV || undefined diff --git a/runtime/preload-browser.test.ts b/runtime/preload-browser.test.ts deleted file mode 100644 index 1e5e134..0000000 --- a/runtime/preload-browser.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import assert from 'node:assert/strict' -import fs from 'node:fs' -import os from 'node:os' -import path from 'node:path' -import {spawn, type ChildProcess} from 'node:child_process' -import {once} from 'node:events' -import {fileURLToPath} from 'node:url' -import {dockerProxy, alternateReaper} from './fixtures/docker-proxy.js' -import {testEnvironment} from './isolation.js' - -const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')) -const browserImage = manifest.images.find((item: any) => item.roles.includes('browser')).image as string -const reaperImage = manifest.images.find((item: any) => item.roles.includes('reaper')).image as string -const proxy = await dockerProxy() -const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'preload-browser-')) -fs.writeFileSync(path.join(temp, 'config.json'), '{}') -const workers: ChildProcess[] = [] -const run = async (expected?: RegExp, directImage?: string) => { - const child = spawn(fs.realpathSync(process.env.JS_BINARY__NODE_BINARY || process.execPath), - [fileURLToPath(new URL('./fixtures/preload-worker.js', import.meta.url))], { - env: {...testEnvironment({}, [], temp), PATH: '/usr/bin:/bin', - DOCKER_CONFIG: temp, DOCKER_HOST: proxy.host, - BROWSER_IMAGE: browserImage, RYUK_CONTAINER_IMAGE: reaperImage, DIRECT_IMAGE: directImage}, - stdio: ['ignore', 'inherit', 'inherit', 'ipc'], - }) - workers.push(child) - const [message] = await Promise.race([ - once(child, 'message'), - once(child, 'exit').then(([code]) => { throw new Error(`Worker exited before result: ${code}`) }), - ]) - if (expected) { - assert.match(message.error, expected) - const [code] = await once(child, 'exit') - assert.equal(code, 1) - } else assert.equal(message.ready, true, message.error) - return child -} -const absent = async (route: string) => assert.rejects(proxy.api(route), /Docker 404/) -let alternate: string | undefined -let alternateImage: string | undefined -try { - // Every declared image is required before any resource is created. - for (const image of [browserImage, reaperImage]) { - proxy.missingImages.add(image) - await run(/Preload required image/) - // Exercise the patched pull path too, independently of runner preflight. - await run(/TESTCONTAINERS_PULL_POLICY=never/, image) - assert.equal(proxy.containers.length, 0) - assert.equal(proxy.networks.length, 0) - proxy.missingImages.clear() - } - await run() - const original = proxy.containers.find(item => item.image === reaperImage)!.id - proxy.reapers.add(original) - await run() - assert.equal(proxy.containers.filter(item => item.image === reaperImage).length, 1, 'must reuse the real matching reaper') - for (const {id, image} of proxy.containers) - if (image === browserImage) await absent(`/containers/${id}/json`) - for (const id of proxy.networks) await absent(`/networks/${id}`) - - const created = proxy.containers.length - proxy.unverifiableContainers.add(original) - await run(/Cannot verify existing Ryuk/) - proxy.unverifiableContainers.clear() - assert.equal(proxy.containers.length, created) - assert.equal((await proxy.api(`/containers/${original}/json`)).State.Running, true) - - // Commit a different image of the actual Ryuk executable, not a label-only fake. - const modified = await alternateReaper(proxy, original) - alternate = modified.id - alternateImage = modified.image - proxy.reapers.clear() - proxy.reapers.add(alternate!) - await run(/Existing Ryuk .* does not match/) - assert.equal(proxy.containers.length, created) - assert.equal((await proxy.api(`/containers/${alternate}/json`)).State.Running, true) - assert.equal((await proxy.api(`/containers/${original}/json`)).State.Running, true) - assert.equal(proxy.forbidden, 0, 'no pull/auth attempt, including all failure cases') -} finally { - for (const child of workers) if (child.connected) child.send('stop') - await Promise.all(workers.map(child => child.exitCode === null ? once(child, 'exit') : undefined)) - // Only this test's resources; never remove an unrelated daemon's reaper. - for (const id of [...proxy.containers.map(item => item.id), ...(alternate ? [alternate] : [])]) - await proxy.api(`/containers/${id}?force=true&v=true`, 'DELETE').catch(() => {}) - for (const id of proxy.networks) await proxy.api(`/networks/${id}`, 'DELETE').catch(() => {}) - if (alternateImage) await proxy.api(`/images/${alternateImage}`, 'DELETE').catch(() => {}) - proxy.close() - fs.rmSync(temp, {recursive: true, force: true}) -} diff --git a/runtime/preload-lifecycle.test.ts b/runtime/preload-lifecycle.test.ts deleted file mode 100644 index 2d840da..0000000 --- a/runtime/preload-lifecycle.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Run public compare/update executables with declared Node and a registry-denying daemon. -import assert from 'node:assert/strict' -import fs from 'node:fs' -import os from 'node:os' -import path from 'node:path' -import {spawn} from 'node:child_process' -import {once} from 'node:events' -import {dockerProxy, alternateReaper} from './fixtures/docker-proxy.js' - -const runfiles = process.env.RUNFILES_DIR || process.env.JS_BINARY__RUNFILES! -const resolve = (name: string) => path.join(runfiles, process.env[name]!) -const manifest = JSON.parse(fs.readFileSync(resolve('PRELOAD_IMAGES'), 'utf8')) -const browser = manifest.images.find((item: any) => item.roles.includes('browser')).image as string -const reaper = manifest.images.find((item: any) => item.roles.includes('reaper')).image as string -const expected = fs.readFileSync(resolve('PRELOAD_BASELINE')) -const proxy = await dockerProxy() -const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'preload-lifecycle-')) -fs.writeFileSync(path.join(temp, 'config.json'), '{}') -// The launchers resolve Node through runfiles, not PATH. Preserve only Bazel plumbing. -const plumbing = Object.fromEntries(Object.entries(process.env).filter(([key]) => /^(RUNFILES|TEST_|BAZEL|JS_BINARY__)/.test(key))) -const run = async (update: boolean, failure?: RegExp) => { - const child = spawn(resolve(update ? 'PRELOAD_UPDATE' : 'PRELOAD_COMPARE'), [], { - env: {...plumbing, RUNFILES_DIR: runfiles, RUNFILES_MANIFEST_FILE: path.join(runfiles, 'MANIFEST'), PATH: process.env.PATH, HOME: temp, DOCKER_CONFIG: temp, - DOCKER_HOST: proxy.host, BUILD_WORKSPACE_DIRECTORY: temp}, - stdio: ['ignore', 'pipe', 'pipe'], - }) - let output = '' - for (const stream of [child.stdout, child.stderr]) stream!.on('data', data => { output += data }) - const [code] = await once(child, 'exit') - if (!failure) assert.equal(code, 0, output) - else { - assert.notEqual(code, 0, output) - assert.match(output, failure) - } - // Browser and relay must be gone after compare, update, and failed startup. - for (const {id, image} of proxy.containers) - if (image === browser) await assert.rejects(proxy.api(`/containers/${id}/json`), /Docker 404/) - for (const id of proxy.networks) await assert.rejects(proxy.api(`/networks/${id}`), /Docker 404/) -} -let alternate: {id: string; image: string} | undefined -try { - await run(false) - alternate = await alternateReaper(proxy, proxy.containers.find(item => item.image === reaper)!.id) - await run(true) - const updated = path.join(temp, '__native_screenshots__', 'saved.png') - assert.deepEqual(fs.readFileSync(updated), expected, 'update must reproduce the committed Linux baseline') - await run(false) - for (const image of [browser, reaper]) { - proxy.missingImages.add(image) - await run(true, /Preload required image/) - assert.deepEqual(fs.readFileSync(updated), expected, 'failed update must preserve baselines') - proxy.missingImages.clear() - } - proxy.reapers.add(alternate.id) - await run(true, /Existing Ryuk .* does not match/) - assert.deepEqual(fs.readFileSync(updated), expected, 'mismatched Ryuk must not modify baselines') - proxy.unverifiableContainers.add(alternate.id) - await run(true, /Cannot verify existing Ryuk/) - assert.deepEqual(fs.readFileSync(updated), expected, 'unverifiable Ryuk must not modify baselines') - assert.equal((await proxy.api(`/containers/${alternate.id}/json`)).State.Running, true, 'foreign reaper must not be stopped') - assert.deepEqual(fs.readFileSync(resolve('PRELOAD_BASELINE')), expected, 'source baseline must remain unchanged') - assert.equal(proxy.forbidden, 0, 'compare, update and failures must not attempt pulls/auth') -} finally { - for (const {id} of proxy.containers) - await proxy.api(`/containers/${id}?force=true&v=true`, 'DELETE').catch(() => {}) - for (const id of proxy.networks) await proxy.api(`/networks/${id}`, 'DELETE').catch(() => {}) - if (alternate) { - await proxy.api(`/containers/${alternate.id}?force=true&v=true`, 'DELETE') - await proxy.api(`/images/${alternate.image}`, 'DELETE') - } - proxy.close() - fs.rmSync(temp, {recursive: true, force: true}) -} diff --git a/runtime/relay.ts b/runtime/relay.ts deleted file mode 100644 index fb04dfa..0000000 --- a/runtime/relay.ts +++ /dev/null @@ -1,13 +0,0 @@ -import net from 'node:net' - -// Only forward the Playwright control socket; never act as an HTTP/SOCKS proxy. -net - .createServer(client => { - const upstream = net.connect(3000, process.env.VRT_BROWSER_HOST!) - client.on('error', () => upstream.destroy()) - upstream.on('error', () => client.destroy()) - client.on('close', () => upstream.destroy()) - upstream.on('close', () => client.destroy()) - client.pipe(upstream).pipe(client) - }) - .listen(3000, '0.0.0.0', () => console.log('Control relay ready')) diff --git a/runtime/remote-result-entry.ts b/runtime/remote-result-entry.ts index 38fa1b8..fd7bbc7 100644 --- a/runtime/remote-result-entry.ts +++ b/runtime/remote-result-entry.ts @@ -11,8 +11,9 @@ try { if (process.argv.length > 2) throw new Error('Remote VRT selection is declared by the Bazel target; use separate targets for subsets') const runfiles = process.env.RUNFILES_DIR || required('JS_BINARY__RUNFILES') + const result = path.join(runfiles, required('VRT_RESULT')) process.exitCode = consumeRemoteResult( - path.join(runfiles, required('VRT_RESULT')), + result, { artifacts: process.env.TEST_UNDECLARED_OUTPUTS_DIR, ...(process.env.VRT_APPLY_BASELINES === '1' ? { @@ -23,6 +24,8 @@ try { } : {}), } ) + if (process.exitCode) + console.error(`VRT failed (exit ${process.exitCode}); reports: ${process.env.TEST_UNDECLARED_OUTPUTS_DIR || path.join(result, 'artifacts')}`) } catch (error) { console.error(error) process.exitCode = 1 diff --git a/runtime/runner.ts b/runtime/runner.ts index eb82cde..e093c69 100644 --- a/runtime/runner.ts +++ b/runtime/runner.ts @@ -5,7 +5,7 @@ import {fileURLToPath, pathToFileURL} from 'node:url' import {createRequire} from 'node:module' import {validatePlaywrightVersions} from './versions.js' import {spawn, type ChildProcess} from 'node:child_process' -import {remoteAppUrl, networkTargets, environmentOrigins} from './network.js' +import {remoteAppUrl} from './network.js' import {testArguments} from './arguments.js' import {baselineDestination, updateBaselines} from './baselines.js' import {stageRunfiles, testEnvironment} from './isolation.js' @@ -22,17 +22,6 @@ async function main() { const gallery = required('VRT_MODE') === 'visual' const visual = gallery || required('VRT_MODE') === 'visual-spec' const remote = remoteAppUrl(process.env) - const origins = [ - ...(JSON.parse(required('VRT_NETWORK_ORIGINS')) as string[]), - ...environmentOrigins( - JSON.parse(required('VRT_NETWORK_ORIGINS_ENV')) as string[], - process.env - ), - ] - // Validate explicit tunnel destinations before allocating resources. - if (visual) networkTargets(remote || 'http://127.0.0.1', origins) - else if (origins.length) - throw new Error('network_origins is supported only for VRT') const hostEnv = visual ? {} : hostBrowserEnvironment(process.env.PLAYWRIGHT_BROWSERS_PATH) @@ -71,10 +60,10 @@ async function main() { test: string core: string version: string - images: {image: string; platform: string | null; roles: string[]}[] } } const selectors = testArguments(visual, args, descriptor.tests) + if (visual && !descriptor.browser) throw new Error("VRT requires a declared browser runtime") const declaredBrowser = descriptor.browser ? browserRuntime(inputs, descriptor.browser) : undefined @@ -158,7 +147,6 @@ async function main() { ), ...hostEnv, ...declaredBrowser?.env, - VRT_NETWORK_ORIGINS: JSON.stringify(origins), VRT_INPUTS: inputs, VRT_MODE: required('VRT_MODE'), VRT_TEST_ROOT: visual ? testRoot : inputs, @@ -189,7 +177,6 @@ async function main() { TEST_UNDECLARED_OUTPUTS_DIR: outputs, PATH: `${path.dirname(node)}:/usr/bin:/bin`, } - let browser: {endpoint: string; stop(): Promise} | undefined const children: ChildProcess[] = [] let succeeded = false const killChildren = () => { @@ -257,22 +244,6 @@ async function main() { }) }) } - if (visual && !declaredBrowser) { - networkTargets(appUrl!, origins) - // Docker settings and helper images apply only to VRT. - for (const key of Object.keys(process.env)) - if (key.startsWith('TESTCONTAINERS_') || key.startsWith('RYUK_')) - delete process.env[key] - const browserImage = descriptor.playwright.images.find(image => - image.roles.includes('browser') - )! - const reaperImage = descriptor.playwright.images.find(image => - image.roles.includes('reaper') - )! - process.env.RYUK_CONTAINER_IMAGE = reaperImage.image - const {startBrowser} = await import('./container.js') - browser = await startBrowser(browserImage.image, core, browserImage.platform!) - } if (interrupted) throw new Error('VRT interrupted') const run = (discover: boolean) => new Promise((resolve, reject) => { @@ -293,7 +264,6 @@ async function main() { ...env, VRT_DISCOVER: discover ? '1' : '0', VRT_APP_URL: appUrl, - ...(browser ? {VRT_WS_ENDPOINT: browser.endpoint} : {}), }, } ) @@ -359,7 +329,6 @@ async function main() { }) ) ) - await browser?.stop() if (interrupted) process.exitCode = 143 if (succeeded) fs.rmSync(temp, {recursive: true, force: true}) process.removeListener('SIGTERM', onSignal) diff --git a/runtime/suite-config.ts b/runtime/suite-config.ts index 4db50f5..1f25e75 100644 --- a/runtime/suite-config.ts +++ b/runtime/suite-config.ts @@ -79,7 +79,7 @@ if ( custom.projects?.some(project => project.use?.connectOptions)) ) throw new Error( - 'E2E and component tests launch host browsers; connectOptions is managed only for VRT' + 'E2E and component tests launch host browsers; connectOptions is unsupported' ) // Keep browser connections, baseline updates, and required reports managed. @@ -115,7 +115,7 @@ const testMatch = const managedUse = { ...merged.use, connectOptions: defaults.use!.connectOptions, - ...(visual && process.env.VRT_CHROMIUM_EXECUTABLE + ...(visual ? {launchOptions: defaults.use!.launchOptions} : {}), browserName: 'chromium' as const, diff --git a/tests/preloaded-vrt.sh b/tests/preloaded-vrt.sh deleted file mode 100644 index a79af3f..0000000 --- a/tests/preloaded-vrt.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# Build with Bazel, then execute the public VRT lifecycle in an OS-only runner. -set -euo pipefail -vrt_root=$(cd "$(dirname "$0")/.." && pwd) -vrt_bazel=${VRT_BAZEL:-bazelisk} -cd "$vrt_root/examples/react" -"$vrt_bazel" build //:preloaded_vrt_test -vrt_output=$("$vrt_bazel" info output_base) -vrt_install=$("$vrt_bazel" info install_base) -vrt_bin=$("$vrt_bazel" info bazel-bin) -vrt_launcher="$vrt_bin/preloaded_vrt_test_/preloaded_vrt_test" -vrt_docker_args=() -# Bazel 9 may symlink declared external repositories into its shared content cache. -# Mount only those resolved repository directories, not the developer's HOME. -declare -A vrt_mounted=() -while IFS= read -r -d '' vrt_link; do - vrt_dependency=$(readlink -f "$vrt_link") - [[ -d "$vrt_dependency" ]] || continue - case "$vrt_dependency" in "$vrt_root"|"$vrt_root"/*|"$vrt_output"/*|"$vrt_install"/*) continue ;; esac - [[ -z "${vrt_mounted[$vrt_dependency]:-}" ]] || continue - vrt_mounted[$vrt_dependency]=1 - vrt_docker_args+=(--mount "type=bind,source=$vrt_dependency,target=$vrt_dependency,readonly") -done < <(find "$vrt_output/external" -maxdepth 1 -type l -print0) -if [[ -n "${DOCKER_HOST:-}" && "$DOCKER_HOST" != unix:* ]]; then - vrt_docker_args+=(--env DOCKER_HOST) -else - vrt_socket=${DOCKER_HOST:-} - vrt_socket=${vrt_socket#unix://} - vrt_socket=${vrt_socket:-/var/run/docker.sock} - vrt_docker_args+=(--mount "type=bind,source=$vrt_socket,target=/var/run/docker.sock" --env DOCKER_HOST=unix:///var/run/docker.sock) -fi -# This image is the test envelope, not a rules-managed runtime image. It contains -# neither Node nor Chromium. Linux host networking keeps relay loopback reachable. -docker run --rm --init --platform linux/amd64 --network host \ - --mount "type=bind,source=$vrt_root,target=$vrt_root,readonly" \ - --mount "type=bind,source=$vrt_output,target=$vrt_output" \ - --mount "type=bind,source=$vrt_install,target=$vrt_install,readonly" \ - --workdir "$vrt_root/examples/react" \ - "${vrt_docker_args[@]}" \ - ubuntu:24.04@sha256:224a1869083a311ef3f13648a154ba79832fbef6364d31493642ca03082da254 \ - bash -c ' - set -eu - for program in node nodejs chromium chromium-browser google-chrome docker; do - if command -v "$program"; then echo "Unexpected host tool: $program" >&2; exit 1; fi - done - mkdir -p /tmp/home /tmp/test /tmp/outputs - export BAZEL_BINDIR=. HOME=/tmp/home TEST_TMPDIR=/tmp/test TEST_UNDECLARED_OUTPUTS_DIR=/tmp/outputs - exec "$1" - ' bash "$vrt_launcher" From d3b94011283a317644d7b1f1e3e3dbd2f5d84d8d Mon Sep 17 00:00:00 2001 From: Long Ho Date: Sun, 13 Sep 2026 21:05:30 +0000 Subject: [PATCH 2/7] test: exercise Bazel fixture servers in actiond --- docs/browser-runtime.md | 4 +++- examples/react/native.config.ts | 7 +++++-- experiments/actiond/package-runtime.sh | 6 ++++-- experiments/actiond/prepare-public.mjs | 7 +++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/browser-runtime.md b/docs/browser-runtime.md index 76de676..580a0c6 100644 --- a/docs/browser-runtime.md +++ b/docs/browser-runtime.md @@ -3,7 +3,9 @@ The actiond migration accepts caller-owned Linux runtime files through `browser_runtime`. The runtime must contain Chromium, Node, their ELF loader and shared libraries, and the fonts/fontconfig used for screenshots. Native -Playwright `webServer` commands also need `/bin/sh`. +Playwright `webServer` commands also need `/bin/sh`. Bazel `js_binary` fixture +launchers additionally need `/usr/bin/env`, Bash, and their shell utilities +(including `dirname`, `uname`, and `readlink`) in the declared runtime. A caller can produce a flattened filesystem tar and unpack it during the Bazel build: diff --git a/examples/react/native.config.ts b/examples/react/native.config.ts index 80404f2..4a7e3ad 100644 --- a/examples/react/native.config.ts +++ b/examples/react/native.config.ts @@ -1,4 +1,5 @@ import type {PlaywrightTestConfig} from '@playwright/test' +import path from 'node:path' const seed = process.env.TEST_TMPDIR ?? 'native-example' const port = @@ -11,9 +12,11 @@ const baseURL = `http://127.0.0.1:${port}` const config: PlaywrightTestConfig = { use: {baseURL}, webServer: { - command: `"${process.execPath}" native-server.js`, + command: process.env.EXAMPLE_SERVER_COMMAND + ? `"${path.join(process.env.RUNFILES_DIR!, process.env.TEST_WORKSPACE!, process.env.EXAMPLE_SERVER_COMMAND)}"` + : `"${process.execPath}" native-server.js`, url: baseURL, - env: {PORT: String(port)}, + env: {...JSON.parse(process.env.EXAMPLE_SERVER_ENV || '{}'), PORT: String(port)}, }, } export default config diff --git a/experiments/actiond/package-runtime.sh b/experiments/actiond/package-runtime.sh index 40baba5..9d86a6b 100755 --- a/experiments/actiond/package-runtime.sh +++ b/experiments/actiond/package-runtime.sh @@ -2,10 +2,12 @@ # Setup-only prototype: run inside the pinned Playwright image. set -euo pipefail out=${1:-/output} -mkdir -p "$out"/{bin,lib,lib64,chromium,etc/fonts,fonts} +mkdir -p "$out"/{bin,usr/bin,lib,lib64,chromium,etc/fonts,fonts} cp /usr/bin/node /bin/bash "$out/bin/" +cp /usr/bin/env "$out/usr/bin/" +cp /usr/bin/{dirname,uname,readlink} "$out/bin/" cp -a /ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/. "$out/chromium/" -for binary in /usr/bin/node /bin/bash /ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell; do +for binary in /usr/bin/node /bin/bash /usr/bin/{env,dirname,uname,readlink} /ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell; do ldd "$binary" | awk '/=> \// {print $3} /^\s*\/lib/ {print $1}' | while read -r lib; do cp -L "$lib" "$out/lib/" done diff --git a/experiments/actiond/prepare-public.mjs b/experiments/actiond/prepare-public.mjs index bb29a42..e2451a5 100644 --- a/experiments/actiond/prepare-public.mjs +++ b/experiments/actiond/prepare-public.mjs @@ -24,6 +24,11 @@ fs.writeFileSync(build, 'load("@rules_web_e2e//playwright:defs.bzl", "browser_runtime")\n' + fs.readFileSync(build, 'utf8') + ` runtime_image(name = "actiond_image", archive = "runtime.tar") +js_binary( + name = "actiond_fixture_server", + entry_point = "native-server.js", + data = [":typecheck_project", "package.json"], +) browser_runtime_oci(name = "actiond_runtime_files", image = ":actiond_image") browser_runtime( name = "actiond_browser", @@ -38,6 +43,8 @@ visual_test( browser = ":actiond_browser", config = ":native_config", tests = ":native_visual_specs", + data = [":actiond_fixture_server"], + env = {"EXAMPLE_SERVER_COMMAND": "$(rootpath :actiond_fixture_server)"}, baseline_dir = "__actiond_native__", baselines = glob(["__actiond_native__/*.png"], allow_empty = True), ) From 9866fc4aa26b44e6341d5d632c98ec12bd7d61bd Mon Sep 17 00:00:00 2001 From: Long Ho Date: Sun, 13 Sep 2026 21:18:10 +0000 Subject: [PATCH 3/7] test: cancel dispatched VRT without relying on executor progress --- docs/component-browser.md | 4 +- docs/customization.md | 4 +- docs/host-browsers.md | 4 +- experiments/actiond/README.md | 106 +++++++++++---------------- experiments/actiond/cancel-public.py | 12 ++- 5 files changed, 57 insertions(+), 73 deletions(-) diff --git a/docs/component-browser.md b/docs/component-browser.md index cf5367b..32e81e1 100644 --- a/docs/component-browser.md +++ b/docs/component-browser.md @@ -2,8 +2,8 @@ Playwright **1.63.0** supplies `mount()` in `@playwright/test`. Use `component_browser_test` from `@rules_web_e2e//component:defs.bzl` with a version-matched -host browser. [Provision it before testing](host-browsers.md); Docker is only -needed for the separate VRT targets. No experimental React test +host browser. [Provision it before testing](host-browsers.md); VRT uses a +separate [actiond execution path](actiond.md). No experimental React test package or second bundler is needed. See the [Playwright component guide](https://playwright.dev/docs/test-components). ```mermaid diff --git a/docs/customization.md b/docs/customization.md index 2b405f4..1fbf062 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -1,7 +1,7 @@ # Custom servers and UI shells Server startup and component rendering are independent. The runtime owns input -staging, environment isolation, host browser setup (VRT containers), artifacts, and teardown. +staging, environment isolation, host browser setup and Linux VRT actions, artifacts, and teardown. Consumers own the server implementation and the React/UI shell. ```mermaid @@ -9,7 +9,7 @@ flowchart LR Target[Bazel target] --> Runtime[VRT runtime] Runtime --> Adapter[Consumer server adapter] Adapter --> Server[Existing dev server or fixture server] - Runtime --> Browser[Host browser; container for VRT] + Runtime --> Browser[Host browser; Linux action for VRT] Browser --> Server Server --> Entry[Consumer fixture entrypoint] Entry --> Shell[Theme, routing, i18n and mock providers] diff --git a/docs/host-browsers.md b/docs/host-browsers.md index ea6e76a..f079e58 100644 --- a/docs/host-browsers.md +++ b/docs/host-browsers.md @@ -69,8 +69,8 @@ component_visual_test( ``` `:linux_browser` is a caller-owned [browser_runtime](browser-runtime.md). -The actual editor gallery built and captured all eight screenshots in actiond's -process sandbox with this interface. That check does not establish full AGI or +The actual editor gallery built, captured, locally applied references, and +compared all eight screenshots in actiond's process sandbox with this interface. That check does not establish full AGI or FormatJS CI migration, or macOS worker support. `playwright_runtime` now groups only matching npm packages and their version. diff --git a/experiments/actiond/README.md b/experiments/actiond/README.md index 1908893..bf10697 100644 --- a/experiments/actiond/README.md +++ b/experiments/actiond/README.md @@ -1,6 +1,7 @@ -# actiond Chromium prototype +# actiond validation and patches -Throwaway experiment, not a replacement for the production VRT backend. +The production backend is documented in [VRT on actiond](../../docs/actiond.md). +This directory keeps its worker patches, CI fixtures, and earlier diagnostics. The action runs a Node HTTP fixture, Chromium, and screenshot comparison together. Node, Playwright, Chromium, shared libraries, fonts, and even the ELF loader are @@ -71,65 +72,42 @@ with local fallback disabled. Both downloaded PNGs match the local hash above. [Passing VM run](https://github.com/perplexityai/rules_web_e2e/actions/runs/34775967325). The local host has no `/dev/kvm`; KVM validation ran on GitHub's Ubuntu runner. -## Decision - -The process-level proof requires **no actiond userspace changes**. The VM proof -requires the memory-advice kernel fix above. Before adding OCI runtime support -or relaxing seccomp, validate an existing editor fixture on the patched VM. Production integration still needs an optional executor -backend, reviewed baseline-update handling, amd64 worker selection on Apple -Silicon, and cleanup/isolation coverage. One stable fixture is not evidence of -cross-architecture pixel equivalence or full Chromium compatibility. - -## Production runner diagnostic - -The `codex/actiond-vrt-runtime` migration adds a diagnostic using the real native -screenshot and component gallery runners. After building `//:native_visual_test` -and `//:component_visual_test` in `examples/react`, run: - -```sh -node experiments/actiond/prepare-production.mjs /tmp/actiond-prototype examples/react/bazel-bin -bash experiments/actiond/run-sandbox.sh /tmp/actiond-prototype --production -``` - -Both suites successfully capture baseline PNGs, then compare against those -captures; JUnit reports are produced for capture and comparison. Results are -downloaded under `results/{native_visual_test,component_visual_test}`. This -diagnostic does not modify source baselines. It exercises the production -`runner.ts`, direct declared Chromium launch, and output-only baseline capture. - -Native Playwright `webServer` commands also require `/bin/sh`. This diagnostic -supplies Bash from the caller image, with the same declared ELF loader and -libraries. The VM integration still needs a declared runtime filesystem layout -for the shell and loader. These new production-runner results are process -sandbox results, not VM/REAPI validation; the earlier VM result above remains -the standalone screenshot fixture. - -## Declared runtime root patch - -`actiond-input-rootfs.patch` is a separate local actiond change based on -`8a42c3d` (local commit `66e2dca`). It adds the `input-rootfs` execution property: -runtime directories from that declared input subtree appear at normal Linux -paths. It replaces injected runtime files for that action and preserves the -executor's device, process, temporary-directory, and network isolation. -actiond's full build and both unit-test targets pass with the patch. macOS VM -execution has not been run. - -`prepare-input-rootfs.sh` restores the image's original Node and Chromium -executables and supplies the loader and shell layout. The production VM workflow -builds actiond with this patch and the separate memory-advice patch, then runs -`run-production-actiond.sh` with local execution fallback disabled. This workflow -is the validation gate for the new rootfs support; local unit tests alone do not -establish VM compatibility. - -The production workflow now uses `prepare-public.mjs` and -`run-public-actiond.sh`: it copies the actual React example, declares the runtime -archive with `browser_runtime_archive`, and runs each public `.update` target -followed by both public test targets. Source baselines are modified only in that -temporary example copy. The native and gallery capture jobs generated by these -public rules both pass under actiond's process isolation. - -The earlier hand-staged VM diagnostic failed because its `glob` omitted an npm -file beneath a nested `BUILD.bazel` package boundary. That failure is not evidence -against the runtime-root patch. The public rules collect the actual declared -runfiles and tree artifacts; their end-to-end VM result remains the validation -gate. The old scripts remain available for reproducing the diagnostic. +## Production validation + +The production workflow builds actiond at `8a42c3d` with both patches below, +starts a Linux amd64 VM, and runs `prepare-public.mjs` / `run-public-actiond.sh`. +The fixture constructs a caller-owned OCI layout through Bazel and extracts its +runtime with `browser_runtime_oci`. Public `.update` and test targets execute +capture/comparison actions remotely and consume downloaded results locally. +Only the temporary example checkout receives baseline updates. + +[Run 34781995883](https://github.com/perplexityai/rules_web_e2e/actions/runs/34781995883) +passes native/component capture and comparison, network isolation, failed/empty +captures, screenshot diffs, and execution deadlines. The replacement workflow +also checks cancellation and a Bazel `js_binary` fixture server. See the current +PR checks for the exact tested revision. + +A 3 GiB guest ran out of memory with three simultaneous suites. CI uses 6 GiB +and at most two actions; size workers for input staging and application memory. +The real FormatJS editor gallery also captures, locally applies references, and +compares all eight screenshots in the actiond process sandbox. That validation +is separate from the VM fixtures and does not establish a full consumer CI migration. + +## Local actiond patches + +- `actiond-advice.patch`: enables memory-advice syscalls in both kernel configs; + submitted as [upstream PR #48](https://github.com/hermeticbuild/actiond/pull/48). +- `actiond-input-rootfs.patch`: exposes selected directories from a declared + runtime input tree at normal Linux paths, preserving executor-owned devices, + `/proc`, temporary storage, and network isolation. `input-rootfs-env` resolves + its path from a declared command variable, which supports Bazel output paths. + Maintained separately in local actiond commits `66e2dca` and `f713bca` for + upstreaming. The full actiond build and both unit-test targets pass. + +The native macOS VM backend has not been exercised here. ARM64 clients must +select an amd64 worker for these baseline inputs. + +Earlier `prepare-production.mjs` / `run-production-actiond.sh` scripts are +historical diagnostics. Their hand-staged glob omitted an npm file beneath a +nested Bazel package; public rules collect actual runfiles and tree artifacts. +Use the production workflow above for the supported execution path. diff --git a/experiments/actiond/cancel-public.py b/experiments/actiond/cancel-public.py index 0ec8bfe..f36ff7f 100644 --- a/experiments/actiond/cancel-public.py +++ b/experiments/actiond/cancel-public.py @@ -1,4 +1,4 @@ -"""Cancel an executing remote capture and verify the local updater never runs.""" +"""Cancel a dispatched remote capture and verify the local updater never runs.""" import os from pathlib import Path @@ -22,12 +22,18 @@ process = subprocess.Popen(command, stdout=output, stderr=output, start_new_session=True) try: deadline = time.monotonic() + 90 - while not re.search(r"VrtCapture actiond_cancel_test_capture.results;[^\n]*remote", log.read_text()): + # actiond currently emits only the final Execute operation, so Bazel + # displays [Sched] even while the worker is executing the suite. + while not re.search(r"VrtCapture actiond_cancel_test_capture.results", log.read_text()): if process.poll() is not None: raise AssertionError(f"Capture exited before cancellation: {log.read_text()}") if time.monotonic() >= deadline: - raise AssertionError(f"Capture did not start remotely: {log.read_text()}") + raise AssertionError(f"Capture was not dispatched: {log.read_text()}") time.sleep(0.2) + # Inputs are already built and uploaded by the preceding cases. Leave + # time for this deliberately 90-second suite to enter the worker. + time.sleep(5) + assert process.poll() is None, log.read_text() os.killpg(process.pid, signal.SIGINT) code = process.wait(timeout=20) assert code in (8, 130, -signal.SIGINT), (code, log.read_text()) From beabf8aab47d8e01c5f2aa09fc4692ee28cf2a8c Mon Sep 17 00:00:00 2001 From: Long Ho Date: Sun, 13 Sep 2026 21:31:58 +0000 Subject: [PATCH 4/7] ci: use production VRT validation as the actiond gate --- .github/workflows/actiond-production.yaml | 4 + .github/workflows/actiond-prototype.yaml | 77 ------------ docs/actiond-migration.md | 136 +++++++--------------- 3 files changed, 43 insertions(+), 174 deletions(-) delete mode 100644 .github/workflows/actiond-prototype.yaml diff --git a/.github/workflows/actiond-production.yaml b/.github/workflows/actiond-production.yaml index b94e57a..5413ba1 100644 --- a/.github/workflows/actiond-production.yaml +++ b/.github/workflows/actiond-production.yaml @@ -7,6 +7,10 @@ on: - 'runtime/**' - 'internal/**' - 'playwright/**' + - 'examples/react/**' + - 'MODULE.bazel' + - 'package.json' + - 'pnpm-lock.yaml' - '.github/workflows/actiond-production.yaml' permissions: diff --git a/.github/workflows/actiond-prototype.yaml b/.github/workflows/actiond-prototype.yaml deleted file mode 100644 index ea1aac4..0000000 --- a/.github/workflows/actiond-prototype.yaml +++ /dev/null @@ -1,77 +0,0 @@ -name: actiond prototype - -on: - pull_request: - paths: - - 'experiments/actiond/**' - - '.github/workflows/actiond-prototype.yaml' - -permissions: - contents: read - -jobs: - chromium: - runs-on: ubuntu-24.04 - timeout-minutes: 30 - env: - USE_BAZEL_VERSION: '9.2.0' - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - with: - node-version: '24' - - uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 - with: - bazelisk-cache: true - repository-cache: true - - name: Prepare declared browser runtime - run: | - corepack enable pnpm - pnpm install --frozen-lockfile - docker pull --platform linux/amd64 mcr.microsoft.com/playwright:v1.63.0-noble@sha256:bc6ab0d6d44ff4826e4cb8c1e6d801e185bfc42bb0753f8e2a30efc70db054c7 - bash experiments/actiond/prepare.sh "$RUNNER_TEMP/actiond-prototype" - - name: Build kernel with memory-advice syscalls - run: | - work="$RUNNER_TEMP/actiond-prototype" - git -C "$work/actiond" worktree add --detach "$work/kernel-source" c99b0cafc824faa59c2b04498eb1a6e5245b53e7 - git -C "$work/kernel-source" apply "$GITHUB_WORKSPACE/experiments/actiond/actiond-advice.patch" - cd "$work/kernel-source" - bazelisk --output_base="$work/kernel-output" build --bes_backend= --remote_executor= --remote_cache= --spawn_strategy=local --jobs=2 --platforms=//platforms:linux_x86_64_musl //vm:linux_kernel.image > "$work/kernel-build.log" 2>&1 - kernel=$(bazelisk --output_base="$work/kernel-output" cquery --bes_backend= --platforms=//platforms:linux_x86_64_musl //vm:linux_kernel.image --output=files) - cp "$kernel" "$work/patched-kernel" - - name: Enable KVM and vhost-vsock - run: | - test -c /dev/kvm - sudo chmod a+rw /dev/kvm - if [[ ! -e /dev/vhost-vsock ]]; then sudo modprobe vhost_vsock; fi - test -c /dev/vhost-vsock - sudo chmod a+rw /dev/vhost-vsock - - name: Capture inside the actiond VM - env: - ACTIOND_BAZEL: bazelisk - run: | - work="$RUNNER_TEMP/actiond-prototype" - curl -fsSL https://github.com/hermeticbuild/actiond/releases/download/v0.0.6/linux-actiond_linux_x86_64 -o "$work/actiond-worker" - printf '006dc798d4363596fe8ab997606fc93766a0cc427c2d005cf4fc1765fa4c2052 %s\n' "$work/actiond-worker" | sha256sum -c - - chmod +x "$work/actiond-worker" - "$work/actiond-worker" serve-vm --kernel="$work/patched-kernel" --root="$work/vm" --listen=127.0.0.1:8980 --memory-mib=3072 --cpus=2 --cas-image-size-mib=4096 > "$work/vm.log" 2>&1 & - worker_pid=$! - trap 'kill "$worker_pid" 2>/dev/null || true' EXIT - ready=false - for attempt in $(seq 1 90); do - kill -0 "$worker_pid" - if (echo > /dev/tcp/127.0.0.1/8980) 2>/dev/null; then ready=true; break; fi - sleep 1 - done - "$ready" - bash experiments/actiond/run-actiond.sh "$work" grpc://127.0.0.1:8980 - sha256sum "$work/results/first.png" "$work/results/second.png" - cmp "$work/results/first.png" "$work/results/second.png" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - if: always() - with: - name: actiond-chromium-prototype - path: | - ${{ runner.temp }}/actiond-prototype/results/ - ${{ runner.temp }}/actiond-prototype/vm.log - ${{ runner.temp }}/actiond-prototype/kernel-build.log diff --git a/docs/actiond-migration.md b/docs/actiond-migration.md index 53c4dee..81bff9c 100644 --- a/docs/actiond-migration.md +++ b/docs/actiond-migration.md @@ -1,97 +1,39 @@ -# Replace Testcontainers with actiond - -Active implementation plan. The existing Chromium prototype in PR #27 passes -through actiond's Linux amd64 VM after applying `actiond-advice.patch` (upstream -actiond PR #48). This branch replaces the VRT backend with actiond; final -validation of the replacement remains in progress. - -## Execution contract - -The fixture server, Playwright test runner, Chromium, and screenshot comparison -execute together as a Linux Bazel action. Browser files, libraries, fonts, Node, -test code, assets, and baselines are declared inputs. The executor provides -isolation; no Docker daemon, image pulls, or reaper run inside the action. - -Callers may construct their own OCI image. Packaging must convert its pinned -contents into declared runtime files before execution, including an explicit -browser executable and architecture. Building Chromium from source is optional. -Keep the rule compatible with REAPI rather than depending on actiond's CLI inside -the test runner. - -Baseline capture produces declared downloadable outputs. A local `.update` -wrapper applies successful captures to the source tree using the existing -destination validation. Failed or empty captures must not replace baselines. - -## Implementation sequence - -1. Add declared browser runtime metadata and direct browser launch support. - Exercise the existing gallery and native screenshot runners, not just the - standalone HTML prototype. -2. Package a caller-owned pinned runtime through Bazel, removing the prototype's - manual Docker extraction prerequisite from the supported execution path. -3. Add Linux execution actions for comparison and baseline capture, plus the - local baseline application wrapper. Select amd64 explicitly; an ARM64 worker - must not silently produce shared amd64 baselines. -4. Validate on a patched actiond VM with local fallback disabled. Check artifacts, - baseline updates, failures, timeouts, cancellation, and network isolation. -5. Provide concrete AGI and FormatJS callsite migrations, including built assets, - caller fixture servers, custom Playwright configuration, and native specs. - External services must become declared local fixtures or have an explicitly - documented unsupported migration case; the VM has no external network. -6. Remove Testcontainers, Ryuk, their patches, and Docker-specific VRT plumbing - once the replacement passes these checks. Update examples, CI, and docs. - -## Completion evidence - -- Both `visual_test` and `component_visual_test` compare and update screenshots - through actiond using the production runner. -- Browser runtime acquisition is pinned and separate from offline execution. -- Screenshot outputs and reports survive remote execution and failed tests. -- Baseline application is local, explicit, and refuses failed or empty captures. -- Host E2E/component browser tests continue to work independently. -- AGI and FormatJS migration examples describe the actual supported interface. -- Any necessary actiond changes remain isolated patches with reproductions and - upstream status. The current required patch enables memory-advice syscalls; - no browser-driven relaxation of the action sandbox has been established as - necessary. - -Keep dependent PRs stacked with ordinary Git and descriptions concise. Do not -mark the goal complete based solely on the standalone prototype passing. - -## Current progress - -- Both production VRT modes capture, apply baselines, and compare through the - public rules in actiond's Linux VM (CI run 34779979065). -- Capture jobs generated by the public rules also pass process-isolation tests - with the archive-built runtime, without hand-editing their descriptors. -- Public rules with a declared browser now create remote comparison/capture - actions and local result consumers. Bazel analysis verifies Linux amd64 - constraints and declared runtime paths. Explicit remote mnemonic strategies - and disabled fallback keep these actions on the worker. -- Failed and empty captures preserve source baselines; downloaded failure - reports survive through the local test wrapper. Filesystem and subprocess - regression tests pass. -- Public-rule isolation and deliberate post-capture failure jobs pass under the - actiond process runner: Node gets `ENETUNREACH`, the browser cannot access the - external address, and failed captures retain diagnostic PNGs without exposing - eligible baseline outputs. CI also checks empty captures and mismatched - reference PNGs through the real local commands; VM results remain pending. -- The separate actiond `input-rootfs`/`input-rootfs-env` patch passes actiond's - full build, unit tests, and the public-rule production VM workflow. - The hand-staged VM diagnostic hit an undeclared npm file beneath a nested - Bazel package; CI now runs the public rules and their declared runfiles. -- `browser_runtime_archive` unpacks a caller-produced flattened runtime tar - through a declared Python toolchain. It normalizes image-root links, preserves - executables, and rejects dangling links and unflattened OCI whiteouts. -- `browser_runtime_oci` verifies and applies declared OCI layers offline, then - materializes the selected runtime subtree. Layer/whiteout/hardlink tests pass, - and the real OCI-built runtime matches the archive-built browser, Node, and - fixture font. The CI example now constructs and passes an OCI image target. - -The legacy backend and dependencies are now removed. The actual FormatJS -editor gallery captures all eight screenshots under process isolation, and host -E2E/component suites still pass. The next VM run includes deadline coverage and -a larger worker after a 3 GiB guest ran out of memory with three parallel suites. -Next: finish full VM failure/cancellation validation and consumer migration evidence. Baseline application, -failures, cancellation, and isolation still need end-to-end VM coverage through -the public rules. OCI support itself does not complete the migration. +# Actiond migration + +VRT now runs through actiond Linux amd64 actions with caller-owned runtime files +or OCI images. Testcontainers, Ryuk, the control relay, image manifests, and their +preload/patch dependencies are removed. Host E2E/component tests retain host +Chromium and host networking. + +The changes are stacked as #27 (initial proof), #28 (declared runtime/actions), +and #29 (backend replacement). + +## Validation + +[Production VM run 34783532975](https://github.com/perplexityai/rules_web_e2e/actions/runs/34783532975) +passes native/component capture and comparison, local baseline updates, network +isolation, failed/empty captures, screenshot diffs, deadlines, cancellation, and +worker recovery. The native fixture launches a real Bazel `js_binary` server. + +Bazel 8.6/9.2 build/tests and host browser suites pass on Linux and macOS. The +actual FormatJS editor gallery builds its Vite/StyleX assets, captures all eight +screenshots, applies references locally, and compares them in actiond process +isolation. See [consumer migration](host-browsers.md) for the actual interface +and the AGI wrapper requirements; this is not a claim that their full CI +migrations have landed. + +## Caller changes + +- Supply `browser` from a declared `browser_runtime`; move the former runtime + `image` setting into the caller's OCI build and `browser_runtime_oci` target. +- Configure a patched amd64 worker using [the execution guide](actiond.md). + Set `target_platform` when native dependencies need additional ABI constraints. +- Keep compiled specs, built shells, matching, server data, and baseline ownership. + `$(rootpath ...)` expands in explicit environment values, including JSON. +- Replace external VRT services with declared local fixtures. Live deployed + checks stay in host E2E targets; inherited environment and origin exceptions + do not carry over. + +The memory-advice patch is upstream PR #48; the declared-rootfs patch remains +isolated locally for upstreaming. Native macOS VM execution and cross-architecture +pixel equivalence remain unvalidated. These baselines require a Linux amd64 worker. From f073c778cdd44a5cf2276e379ec27f7112e6bd6b Mon Sep 17 00:00:00 2001 From: Long Ho Date: Sun, 13 Sep 2026 21:42:19 +0000 Subject: [PATCH 5/7] test: deliver cancellation once through Bazelisk --- experiments/actiond/cancel-public.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/experiments/actiond/cancel-public.py b/experiments/actiond/cancel-public.py index f36ff7f..513d805 100644 --- a/experiments/actiond/cancel-public.py +++ b/experiments/actiond/cancel-public.py @@ -34,7 +34,9 @@ # time for this deliberately 90-second suite to enter the worker. time.sleep(5) assert process.poll() is None, log.read_text() - os.killpg(process.pid, signal.SIGINT) + # Bazelisk forwards signals to its Bazel child. Signalling the whole + # group delivers the interrupt twice and can crash Bazel's shutdown. + process.send_signal(signal.SIGINT) code = process.wait(timeout=20) assert code in (8, 130, -signal.SIGINT), (code, log.read_text()) finally: From ddc1d7cf5561a4d9a1b5e41de2a3f85b81ef8f5b Mon Sep 17 00:00:00 2001 From: Long Ho Date: Sun, 13 Sep 2026 21:48:36 +0000 Subject: [PATCH 6/7] test: cancel the resolved Bazel client directly --- experiments/actiond/cancel-public.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/experiments/actiond/cancel-public.py b/experiments/actiond/cancel-public.py index 513d805..a74a79b 100644 --- a/experiments/actiond/cancel-public.py +++ b/experiments/actiond/cancel-public.py @@ -3,6 +3,7 @@ import os from pathlib import Path import re +import shutil import signal import subprocess import sys @@ -14,8 +15,16 @@ baseline.write_bytes(expected) log = Path("../results/cancellation.log") log.parent.mkdir(exist_ok=True) +bazel = sys.argv[1] +if Path(bazel).name == "bazelisk": + # Bazelisk prepends the selected binary's directory to PATH. Use that + # binary directly: older Bazelisk versions ignore client-only signals. + environment = subprocess.check_output([bazel, "--print_env"], text=True) + search_path = next(line.removeprefix("PATH=") for line in environment.splitlines() if line.startswith("PATH=")) + bazel = shutil.which("bazel", path=search_path) + assert bazel is not None, "Bazelisk did not expose its selected Bazel binary" command = [ - *sys.argv[1:3], "run", "//:actiond_cancel_test.update", *sys.argv[3:], + bazel, sys.argv[2], "run", "//:actiond_cancel_test.update", *sys.argv[3:], "--progress_report_interval=1", "--curses=no", "--color=no", ] with log.open("w") as output: @@ -34,8 +43,8 @@ # time for this deliberately 90-second suite to enter the worker. time.sleep(5) assert process.poll() is None, log.read_text() - # Bazelisk forwards signals to its Bazel child. Signalling the whole - # group delivers the interrupt twice and can crash Bazel's shutdown. + # Let the client cancel its server request without also signalling + # other processes in the invocation's process group. process.send_signal(signal.SIGINT) code = process.wait(timeout=20) assert code in (8, 130, -signal.SIGINT), (code, log.read_text()) From 7c1f185dba198d32b88db49e778a09d7a658225a Mon Sep 17 00:00:00 2001 From: Long Ho Date: Mon, 14 Sep 2026 01:06:34 +0000 Subject: [PATCH 7/7] ci: preserve the required VRT check name for actiond --- .github/workflows/actiond-production.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/actiond-production.yaml b/.github/workflows/actiond-production.yaml index 5413ba1..7cb3dd8 100644 --- a/.github/workflows/actiond-production.yaml +++ b/.github/workflows/actiond-production.yaml @@ -18,6 +18,7 @@ permissions: jobs: vrt: + name: VRT tests (Linux amd64) runs-on: ubuntu-24.04 timeout-minutes: 40 env: