Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/actiond-production.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: actiond production VRT

on:
pull_request:
paths:
- 'experiments/actiond/**'
- 'runtime/**'
- 'internal/**'
- 'playwright/**'
- '.github/workflows/actiond-production.yaml'

permissions:
contents: read

jobs:
vrt:
runs-on: ubuntu-24.04
timeout-minutes: 40
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
disk-cache: actiond-linux-amd64
- name: Prepare production suites and caller runtime
run: |
corepack enable pnpm
pnpm install --frozen-lockfile
docker pull --platform linux/amd64 mcr.microsoft.com/playwright:v1.63.0-noble@sha256:bc6ab0d6d44ff4826e4cb8c1e6d801e185bfc42bb0753f8e2a30efc70db054c7
work="$RUNNER_TEMP/actiond-production"
bash experiments/actiond/prepare.sh "$work"
node experiments/actiond/prepare-public.mjs "$work"
- name: Build patched actiond
run: |
work="$RUNNER_TEMP/actiond-production"
git -C "$work/actiond" apply "$GITHUB_WORKSPACE/experiments/actiond/actiond-advice.patch"
git -C "$work/actiond" apply "$GITHUB_WORKSPACE/experiments/actiond/actiond-input-rootfs.patch"
cd "$work/actiond"
bazelisk --output_base="$work/worker-output" build --bes_backend= --remote_executor= --remote_cache= --spawn_strategy=local --jobs=2 //cmd/linux-actiond:linux-actiond_linux_x86_64 > "$work/worker-build.log" 2>&1
worker=$(bazelisk --output_base="$work/worker-output" cquery --bes_backend= //cmd/linux-actiond:linux-actiond_linux_x86_64 --output=starlark '--starlark:expr=providers(target)["DefaultInfo"].files_to_run.executable.path')
cp "$worker" "$work/actiond-worker"
- 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 and compare in the VM
env:
ACTIOND_BAZEL: bazelisk
run: |
work="$RUNNER_TEMP/actiond-production"
"$work/actiond-worker" serve-vm --root="$work/vm" --listen=127.0.0.1:8980 --memory-mib=6144 --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-public-actiond.sh "$work" grpc://127.0.0.1:8980
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: actiond-production-vrt
path: |
${{ runner.temp }}/actiond-production/results/
${{ runner.temp }}/actiond-production/vm.log
${{ runner.temp }}/actiond-production/worker-build.log
2 changes: 2 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module(
)

bazel_dep(name = "rules_shell", version = "0.8.0")
bazel_dep(name = "platforms", version = "1.1.0")
bazel_dep(name = "rules_python", version = "1.7.0")
bazel_dep(name = "aspect_rules_js", version = "3.4.1")
bazel_dep(name = "bazel_lib", version = "3.7.2")
bazel_dep(name = "aspect_rules_ts", version = "3.10.1")
Expand Down
93 changes: 93 additions & 0 deletions docs/actiond-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# 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). Production VRT still uses Testcontainers.

## 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.

Next: verify the full public-rule workflow in the VM, migrate concrete
AGI/FormatJS callsites, and remove the legacy backend. 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.
14 changes: 14 additions & 0 deletions docs/actiond-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Actiond runtime validation

[VM run 34781995883](https://github.com/perplexityai/rules_web_e2e/actions/runs/34781995883)
passes native/component capture and comparison through the public Bazel rules,
network isolation, failed/empty captures, screenshot diffs, and execution deadlines.
The worker uses the two isolated patches in `experiments/actiond`.

[CI run 34782772531](https://github.com/perplexityai/rules_web_e2e/actions/runs/34782772531)
passes Bazel 8.6 and 9.2 on Linux and macOS, including OCI/archive extraction and
host browser tests. Extraction canonicalizes temporary roots before checking
image links, covering macOS's symlinked temporary directories.

Native macOS VM execution and cross-architecture screenshot equivalence are not
covered. The backend-removal PR adds cancellation and caller Bazel server coverage.
87 changes: 87 additions & 0 deletions docs/browser-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Declared browser runtimes

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`.

A caller can produce a flattened filesystem tar and unpack it during the Bazel
build:

```starlark
load("@rules_web_e2e//playwright:archive.bzl", "browser_runtime_archive")
load("@rules_web_e2e//playwright:defs.bzl", "browser_runtime")
load("@rules_web_e2e//vrt:defs.bzl", "visual_test")

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",
)

visual_test(
name = "visuals",
browser = ":browser",
tests = ":compiled_visual_specs",
shell = ":app_shell",
baselines = glob(["__screenshots__/*.png"]),
)
```

`:runtime_tar` is a declared file target. It can be built by the caller or fetched
with Bazel's downloader using a pinned checksum. Paths above describe the
prototype's layout; select paths matching the caller's runtime.

Archive extraction uses a Bazel-provided Python interpreter and makes no network
requests. Absolute image symlinks are resolved within the image root, then links
are materialized into regular files and directories for the output tree. Missing
link targets are errors, so runtime packaging cannot silently borrow host files.
Font configuration should use image paths or paths relative to the configuration
file, rather than a build-machine path.

For a caller-owned OCI image layout directory, use `browser_runtime_oci` instead:

```starlark
load("@rules_web_e2e//playwright:archive.bzl", "browser_runtime_oci")

browser_runtime_oci(
name = "runtime_files",
image = ":caller_image",
directory = "runtime",
)
```

`image` supplies one declared OCI layout directory, such as an `oci_image`
output. `directory` selects the runtime subtree after applying layers; use `.`
when the whole image is a closed browser runtime. Keep the `browser_runtime`
paths relative to that selected subtree. OCI extraction verifies SHA-256 blob
digests and sizes, selects one Linux amd64 image, applies layers in order, and
handles whiteouts before same-layer additions. It supports uncompressed and
gzip layers; unsupported layer media types fail explicitly. It never fetches
missing blobs, executes image commands, or contacts a registry.

An image tar produced by `docker save` or `oci_load` is neither a flattened
filesystem archive nor a layout directory. Pass the image layout target directly.
The runtime subtree must include every link target it needs within the declared
image, and must not rely on Docker injecting files such as `/etc/hosts`.

Execution currently requires a patched actiond Linux amd64 worker. The remote
actions produce comparison/capture results; the local test command reports their
status and `.update` applies successful captures. Host E2E/component-browser
targets continue to use their existing browser setup. Native and component
capture/comparison through the public rules passed the Linux VM workflow.

VRT inputs are configured for Linux amd64 even when the Bazel client runs on
macOS. A caller with additional native toolchain constraints can set
`target_platform = "//platforms:linux_x86_64_gnu"` on its visual target. That
platform must still target Linux amd64 and match the runtime's ABI. `data` and
`$(rootpath ...)` expressions in `env` are evaluated in this configuration,
including expressions nested inside JSON strings used by fixture servers.
9 changes: 9 additions & 0 deletions experiments/actiond/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("@rules_python//python:defs.bzl", "py_binary")

exports_files(["fixture.bzl"])

py_binary(
name = "fixture_image",
srcs = ["fixture_image.py"],
main = "fixture_image.py",
)
54 changes: 54 additions & 0 deletions experiments/actiond/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,57 @@ or relaxing seccomp, validate an existing editor fixture on the patched VM. Prod
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.
Loading
Loading