Skip to content

🐞 macOS channel installs drop the .app bundle from the cached path #658

Description

@maximilianschmid

What is happening?

On macOS, installing by channel name (chrome-version: stable) caches the contents of Google Chrome for Testing.app into the tool cache, so the resulting chrome-path has no .app component:

$RUNNER_TOOL_CACHE/setup-chrome/chrome/stable/arm64/Contents/MacOS/Google Chrome for Testing

Installing by explicit version (or a snapshot) keeps the bundle:

$RUNNER_TOOL_CACHE/setup-chrome/chrome/152.0.7977.75/arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing

The browser starts either way, but launchers that need a real app bundle break on the first layout. In our case Cypress 16 launches it, connects to CDP, and then never finds the initial page, because the page target's url stays empty:

Cypress failed to make a connection to the Chrome DevTools Protocol after retrying for 50 seconds.
Error: Could not find url target in browser about:blank.
Targets were [{"type":"page","title":"","url":"", …}, {"type":"browser_ui", …}, {"type":"browser_ui", …}]

I isolated it to the path alone — same bytes, only the .app component differs:

Executable path Cypress run
…/chrome/stable/arm64/Contents/MacOS/Google Chrome for Testing (channel install) fails, 2/2
…/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/… (straight from the zip) passes, 1/1
…/Google Chrome for Testing.app/Contents/MacOS/… where the .app is a symlink to the cached folder above passes, 1/1

The third row is the interesting one: it is the exact same directory as the failing case, reached through a .app-named symlink, and that is enough to make it work. So it is not the copy, the code signature or the download — only the absence of .app in the invoked path.

I could not reduce this to a Cypress-free repro: launching either layout manually with --headless --remote-debugging-port=… about:blank and reading /json/list gives a normal about:blank page target in both cases. So Cypress's specific launch flags are involved too, and I don't want to overstate the mechanism — but the .app component is reliably the deciding factor.

Version where you found the bug

v2 (v2.2.0)

Runner specifics

Self-hosted macOS 26.6.2, arm64 (Apple silicon). Also reproduced locally on the same OS with a hand-made copy of the two layouts, so it is not runner-specific.

Action workflow configuration

      - name: Install chrome
        id: install-chrome
        uses: browser-actions/setup-chrome@v2
        with:
          chrome-version: stable

      - uses: cypress-io/github-action@v7
        with:
          browser: ${{ steps.install-chrome.outputs.chrome-path }}

Relevant log output

Browser:        Custom Chrome for Testing 152 (headless)

Still waiting to connect to Custom chrome for testing, retrying in 1 second (attempt 62/62)
Cypress failed to make a connection to the Chrome DevTools Protocol after retrying for 50 seconds.

This usually indicates there was a problem opening the Custom chrome for testing browser.

Error: Could not find url target in browser about:blank. Targets were [{"targetId":"","type":"page","title":"","url":"","attached":true,"canAccessOpener":false,"browserContextId":""},{"targetId":"","type":"browser_ui","title":"","url":"","attached":true,"canAccessOpener":false,"browserContextId":""},{"targetId":"","type":"browser_ui","title":"","url":"","attached":true,"canAccessOpener":false,"browserContextId":""}]

Where it comes from

MacOSChannelInstaller caches the .app directory itself, so its contents land directly in <cache>/chrome/<channel>/<arch>/:

// src/channel_macos.ts
const extAppRoot = path.join(extPath, `chrome-${this.versionResolver.platformString}`, "Google Chrome for Testing.app");
const root = await cache.cacheDir(extAppRoot, "chrome", version);
return { root, bin: "Contents/MacOS/Google Chrome for Testing" };

The other two installers cache the parent directory and keep the bundle in bin:

// src/version_installer.ts
const extAppRoot = path.join(extPath, `chrome-${this.versionResolver.platformString}`);
const root = await cache.cacheDir(extAppRoot, "chrome", resolved.version);
// bin, DARWIN: "Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"

// src/snapshot_installer.ts
// bin, DARWIN: "Chromium.app/Contents/MacOS/Chromium"

So aligning channel_macos.ts with them — cache chrome-${platformString} and return bin: "Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" — would make channel installs consistent with version and snapshot installs. It changes the cached layout, so existing chrome/<channel>/<arch> cache entries on long-lived self-hosted runners would need to be invalidated.

The same applies to the chromedriver path in that file if it has the equivalent shape.

Workaround

For anyone hitting this: pin an explicit version instead of a channel name, which routes through VersionInstaller and keeps the .app:

        with:
          chrome-version: 152.0.7977.75

Or symlink a .app name over the cached folder and launch through that:

bin_name="$(basename "$CHROME_PATH")"
app_root="$(cd "$(dirname "$CHROME_PATH")/../.." && pwd)"
bundle="$RUNNER_TEMP/browser/$bin_name.app"
mkdir -p "$RUNNER_TEMP/browser"; rm -rf "$bundle"; ln -s "$app_root" "$bundle"
echo "path=$bundle/Contents/MacOS/$bin_name" >> "$GITHUB_OUTPUT"

Thanks for the action — happy to test a patch if that helps.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions