fix(release): backport canonical browser workspace identity repair

Backport PR1948 for the candidate preflight EACCES regression. Resolve main-only context by preserving release/v6.4 fixed smoke ports and omitting its unavailable PULSE_E2E_BASE_URL variable. No runtime changes, ACL widening, CLI download fallback or gate waiver. Supersedes the proposed ae1ac6ae95 and 0d0b105352 stack; normal reviewed source landing and exact qualification remain required.

(cherry picked from commit 8b73085e82)

Change-source: pulse-maintainer
This commit is contained in:
rcourtman
2026-09-07 08:29:42 +01:00
committed by pulse-triage[bot]
parent 57f3b6401a
commit 148df87f06
3 changed files with 105 additions and 16 deletions
@@ -970,6 +970,14 @@ artifact-selection behaviour.
Worker startup must compare the complete `go`-prefixed toolchain identity
from `go.mod` with `go env GOVERSION` so a formatting mismatch cannot reject
an otherwise exact toolchain or conceal a real version drift.
Browser smoke must preserve private workspace ownership across the Docker
user-namespace boundary. On rootless Docker, container `0:0` maps to the
daemon owner; on rootful Docker, the browser uses the worker's host UID/GID.
The worker must read the daemon's security options, reject unreadable or
malformed identity information, and probe the mounted, lockfile-installed
Playwright CLI before the expensive suites and image build. It must not
download a replacement CLI or broaden checkout permissions to hide a mount
identity failure.
Race-instrumented Go builds must place `GOTMPDIR` under the worker's
persistent run directory and remove that bounded scratch directory on exit;
a small WSL `/tmp` tmpfs must not turn release qualification into a false
+36 -16
View File
@@ -67,6 +67,23 @@ if [ "$(node -p "process.versions.node.split('.')[0]")" != "24" ]; then
exit 3
fi
playwright_container_user() {
local security_options
security_options="$(docker info --format '{{json .SecurityOptions}}')" || return
python3 - "$security_options" "$(id -u):$(id -g)" <<'PY'
import json
import sys
options = json.loads(sys.argv[1])
if not isinstance(options, list) or not all(isinstance(value, str) for value in options):
raise SystemExit("Docker security options must be a JSON list of strings.")
# Container root maps to the daemon owner on rootless Docker. Reusing that
# owner's host UID instead selects an unrelated subordinate host identity.
print("0:0" if "name=rootless" in options else sys.argv[2])
PY
}
PLAYWRIGHT_CONTAINER_USER="$(playwright_container_user)"
mkdir -p \
"$CACHE_DIR/go-build" \
"$CACHE_DIR/go-mod" \
@@ -175,11 +192,28 @@ run_backend() {
fi
}
run_playwright() {
docker run --rm \
--network host \
--ipc host \
--user "$PLAYWRIGHT_CONTAINER_USER" \
--env CI=true \
--env HOME=/tmp \
--env "PULSE_E2E_DIAGNOSTIC=${PULSE_E2E_DIAGNOSTIC:-}" \
--volume "$REPOSITORY_DIR/tests/integration:/work" \
--workdir /work \
"$PLAYWRIGHT_IMAGE" \
node /work/node_modules/@playwright/test/cli.js "$@"
}
run_integration_prep() {
phase integration-dependencies npm --prefix tests/integration ci
PLAYWRIGHT_VERSION="$(node -p "require('./tests/integration/node_modules/@playwright/test/package.json').version")"
PLAYWRIGHT_IMAGE="mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble"
phase playwright-image docker pull "$PLAYWRIGHT_IMAGE"
# Check the real mount and locked CLI before the expensive test suites and
# image build. Never let npx download a different browser test version.
phase playwright-runtime run_playwright --version
phase mock-github-image docker build --tag pulse-mock-github:test tests/integration/mock-github-server
}
@@ -228,20 +262,6 @@ else
--tag pulse:test \
.
fi
run_playwright() {
docker run --rm \
--network host \
--ipc host \
--user "$(id -u):$(id -g)" \
--env CI=true \
--env HOME=/tmp \
--env "PULSE_E2E_DIAGNOSTIC=${PULSE_E2E_DIAGNOSTIC:-}" \
--volume "$REPOSITORY_DIR/tests/integration:/work" \
--workdir /work \
"$PLAYWRIGHT_IMAGE" \
npx playwright test "$@"
}
run_rehearsal_smoke() {
cd tests/integration
export MOCK_CHECKSUM_ERROR=false
@@ -251,7 +271,7 @@ run_rehearsal_smoke() {
export PULSE_E2E_DIAGNOSTIC=1
docker compose -f docker-compose.test.yml up -d --wait
timeout 60 sh -c 'until curl -fsS http://localhost:7655/api/health >/dev/null; do sleep 2; done'
run_playwright tests/00-diagnostic.spec.ts --project=chromium --reporter=list
run_playwright test tests/00-diagnostic.spec.ts --project=chromium --reporter=list
local status
status="$(curl -s -o "$RUN_DIR/update-status.json" -w '%{http_code}' http://localhost:7655/api/updates/status || true)"
case "$status" in
@@ -276,7 +296,7 @@ run_release_smoke() {
timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-mock-github | grep -q healthy; do sleep 2; done'
timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-test-server | grep -q healthy; do sleep 2; done'
timeout 60 sh -c 'until curl -fsS http://localhost:7655/api/health >/dev/null; do sleep 2; done'
run_playwright tests/95-release-smoke.spec.ts --project=chromium --reporter=list
run_playwright test tests/95-release-smoke.spec.ts --project=chromium --reporter=list
docker compose -f docker-compose.test.yml down -v
}
@@ -2,6 +2,7 @@
from __future__ import annotations
import os
import json
import pathlib
import re
import subprocess
@@ -265,6 +266,66 @@ class ReleasePreflightTest(unittest.TestCase):
self.assertLess(block.index("done\n"), block.index("run_frontend_tests\n"))
self.assertLess(block.index("run_frontend_tests\n"), block.index("run_backend\n"))
def test_browser_mount_identity_and_locked_cli_follow_the_docker_daemon(self) -> None:
worker = (ROOT / "scripts/release-preflight-worker.sh").read_text()
functions = []
for name in ("playwright_container_user", "run_playwright"):
match = re.search(rf"^{name}\(\) \{{\n.*?^\}}", worker, re.MULTILINE | re.DOTALL)
self.assertIsNotNone(match, name)
functions.append(match.group(0))
for options, info_rc, valid, expected_user in (
(["name=seccomp,profile=builtin", "name=rootless"], 0, True, "0:0"),
(["name=seccomp,profile=builtin"], 0, True, f"{os.getuid()}:{os.getgid()}"),
([], 42, False, None),
({"rootless": True}, 0, False, None),
(None, 0, False, None),
):
with self.subTest(options=options, info_rc=info_rc), tempfile.TemporaryDirectory() as raw:
temp = pathlib.Path(raw)
docker = temp / "docker"
docker.write_text(
f"#!{sys.executable}\n"
"import json,os,pathlib,sys\n"
"if sys.argv[1] == 'info':\n"
" print(os.environ['TEST_SECURITY_OPTIONS'])\n"
" raise SystemExit(int(os.environ['TEST_INFO_RC']))\n"
"pathlib.Path(os.environ['TEST_CONTAINER_STARTED']).touch()\n"
"print(json.dumps(sys.argv[1:]))\n"
)
docker.chmod(0o755)
started = temp / "started"
script = "set -euo pipefail\n" + "\n".join(functions) + "\n"
script += 'PLAYWRIGHT_CONTAINER_USER="$(playwright_container_user)"\n'
script += "run_playwright --version\n"
script += "run_playwright test tests/00-diagnostic.spec.ts --project=chromium\n"
result = subprocess.run(
["bash", "-c", script], capture_output=True, text=True, check=False,
env={**os.environ, "PATH": f"{temp}:{os.environ['PATH']}",
"TEST_SECURITY_OPTIONS": json.dumps(options), "TEST_INFO_RC": str(info_rc),
"TEST_CONTAINER_STARTED": str(started),
"REPOSITORY_DIR": str(temp / "private workspace"),
"PLAYWRIGHT_IMAGE": "locked-playwright-image",
"PULSE_E2E_BASE_URL": "http://127.0.0.1:27655"},
)
self.assertEqual(valid, result.returncode == 0, result.stderr)
self.assertEqual(valid, started.exists())
if not valid:
continue
calls = [json.loads(line) for line in result.stdout.splitlines()]
self.assertEqual(2, len(calls))
for args in calls:
self.assertEqual(expected_user, args[args.index("--user") + 1])
self.assertEqual(f"{temp}/private workspace/tests/integration:/work",
args[args.index("--volume") + 1])
self.assertIn("node", args)
self.assertIn("/work/node_modules/@playwright/test/cli.js", args)
self.assertNotIn("npx", args)
self.assertEqual("--version", calls[0][-1])
self.assertEqual(["test", "tests/00-diagnostic.spec.ts", "--project=chromium"], calls[1][-3:])
prep = worker[worker.index("run_integration_prep() {"):worker.index("# Static frontend checks")]
self.assertLess(prep.index("phase playwright-image"), prep.index("phase playwright-runtime"))
self.assertIn("phase playwright-runtime run_playwright --version", prep)
def test_api_shard_plan_is_deterministic_complete_and_disjoint(self) -> None:
test_names = [
f"TestReleaseCase{index:04d}"