mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 02:55:51 +00:00
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 proposedae1ac6ae95and0d0b105352stack; normal reviewed source landing and exact qualification remain required. (cherry picked from commit8b73085e82) Change-source: pulse-maintainer
This commit is contained in:
committed by
pulse-triage[bot]
parent
57f3b6401a
commit
148df87f06
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user