Description
playwrightBrowsersPath (pkg/workflow/playwright_cli.go) is compiled as the literal string ${RUNNER_TEMP}/gh-aw/playwright-browsers and set via env: on both the "Install Playwright browsers" step and the agent-execution step. GitHub Actions env: map values are never shell-expanded, so ${RUNNER_TEMP} reaches each process as a literal, unexpanded string — but the two steps end up disagreeing on where the browser is:
- Install step (plain
run: bash install_playwright_browsers.sh chromium, no shell macro-expansion available): the browser is written under a directory literally named ${RUNNER_TEMP} (a relative path, since it doesn't start with /), rooted at the job's cwd. Observed in the real log:
Chrome for Testing 152.0.7977.8 (playwright chromium v1237) downloaded to
/home/runner/work/<repo>/<repo>/${RUNNER_TEMP}/gh-aw/playwright-browsers/chromium-1237
- Agent step (Claude +
playwright-cli open): playwright-core's own throwIfExecutableMissing resolves the same nominal path to the real, expanded runner temp dir:
Error: Browser "chrome-for-testing" is not installed; expected executable at
/home/runner/work/_temp/gh-aw/playwright-browsers/chromium-1237/chrome-linux64/chrome
Because the browser was actually installed at the first (bogus, unexpanded) path and looked for at the second (real) path, playwright-cli open always fails with "Browser ... is not installed", even though the install step reported success. The agent correctly refuses to self-heal (per its own "never install at runtime" instruction) and reports a noop instead — so the failure is silent from a CI-green/red perspective, just quietly produces nothing.
Reproduced on agent-pr-diagram.md (Playwright CLI mode, tools.playwright: with no mode:, i.e. default) pinned to v0.88.7. Confirmed playwrightBrowsersPath is unchanged in HEAD (checked through v0.89.13) — not fixed by a version bump.
I believe this is why ${{ runner.temp }} isn't used here (per #54295, that trips a CodeQL code-injection alert on the interpolated ${{ }}), but the literal-string fallback isn't actually expanded anywhere in the plain-bash install step, only in whatever resolves paths inside playwright-core at launch time. The two need to agree.
Repro
- Workflow with
tools.playwright: (CLI mode) and a step later in the same job that runs playwright-cli open ....
- Compile + run on a GitHub-hosted runner.
- Install step log shows the browser downloaded under a directory literally named
${RUNNER_TEMP} (relative to cwd).
- The later
playwright-cli open fails with Browser "chrome-for-testing" is not installed; expected executable at <real runner.temp>/gh-aw/playwright-browsers/....
Suggested fix
Don't rely on the literal ${RUNNER_TEMP} string being expanded consistently by whatever happens to read PLAYWRIGHT_BROWSERS_PATH. Either:
- have
install_playwright_browsers.sh (and any other consumer) explicitly export PLAYWRIGHT_BROWSERS_PATH="${RUNNER_TEMP}/gh-aw/playwright-browsers" itself at the top of the script (real bash expansion, since run: script bodies do get $VAR-expanded, unlike env: map values), rather than trusting the value handed in via env:; or
- resolve
playwrightBrowsersPath to a real absolute path at compile time some other CodeQL-safe way (e.g. a small composite/js step that reads process.env.RUNNER_TEMP and re-exports the resolved value via $GITHUB_ENV, rather than a literal ${VAR} string in a YAML env: map).
Evidence
Description
playwrightBrowsersPath(pkg/workflow/playwright_cli.go) is compiled as the literal string${RUNNER_TEMP}/gh-aw/playwright-browsersand set viaenv:on both the "Install Playwright browsers" step and the agent-execution step. GitHub Actionsenv:map values are never shell-expanded, so${RUNNER_TEMP}reaches each process as a literal, unexpanded string — but the two steps end up disagreeing on where the browser is:run: bash install_playwright_browsers.sh chromium, no shell macro-expansion available): the browser is written under a directory literally named${RUNNER_TEMP}(a relative path, since it doesn't start with/), rooted at the job's cwd. Observed in the real log:playwright-cli open):playwright-core's ownthrowIfExecutableMissingresolves the same nominal path to the real, expanded runner temp dir:Because the browser was actually installed at the first (bogus, unexpanded) path and looked for at the second (real) path,
playwright-cli openalways fails with "Browser ... is not installed", even though the install step reported success. The agent correctly refuses to self-heal (per its own "never install at runtime" instruction) and reports anoopinstead — so the failure is silent from a CI-green/red perspective, just quietly produces nothing.Reproduced on
agent-pr-diagram.md(Playwright CLI mode,tools.playwright:with nomode:, i.e. default) pinned tov0.88.7. ConfirmedplaywrightBrowsersPathis unchanged in HEAD (checked throughv0.89.13) — not fixed by a version bump.I believe this is why
${{ runner.temp }}isn't used here (per #54295, that trips a CodeQL code-injection alert on the interpolated${{ }}), but the literal-string fallback isn't actually expanded anywhere in the plain-bash install step, only in whatever resolves paths insideplaywright-coreat launch time. The two need to agree.Repro
tools.playwright:(CLI mode) and a step later in the same job that runsplaywright-cli open ....${RUNNER_TEMP}(relative to cwd).playwright-cli openfails withBrowser "chrome-for-testing" is not installed; expected executable at <real runner.temp>/gh-aw/playwright-browsers/....Suggested fix
Don't rely on the literal
${RUNNER_TEMP}string being expanded consistently by whatever happens to readPLAYWRIGHT_BROWSERS_PATH. Either:install_playwright_browsers.sh(and any other consumer) explicitlyexport PLAYWRIGHT_BROWSERS_PATH="${RUNNER_TEMP}/gh-aw/playwright-browsers"itself at the top of the script (real bash expansion, sincerun:script bodies do get$VAR-expanded, unlikeenv:map values), rather than trusting the value handed in viaenv:; orplaywrightBrowsersPathto a real absolute path at compile time some other CodeQL-safe way (e.g. a small composite/js step that readsprocess.env.RUNNER_TEMPand re-exports the resolved value via$GITHUB_ENV, rather than a literal${VAR}string in a YAMLenv:map).Evidence
pkg/workflow/playwright_cli.go:const playwrightBrowsersPath = "${RUNNER_TEMP}/gh-aw/playwright-browsers"pkg/workflow/engine_helpers.go:env["PLAYWRIGHT_BROWSERS_PATH"] = playwrightBrowsersPath(same literal reused for both the install step and the agent-execution step)