From 040978c54c9b710be8d09732e44ba6bf00d195e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Nerl=C3=B8e?= Date: Thu, 3 Sep 2026 00:21:17 +0200 Subject: [PATCH 1/3] fix(build): playwright extension parses the 1.58+ install --dry-run output Playwright 1.58 changed the per-browser header printed by `playwright install --dry-run` from `browser: version ` to ` (playwright v)`, so the extension's grep found nothing and the image build failed at that step. The header match now accepts both formats. The context window after the header is also narrowed to the two lines the extension reads (install location and download url): the new blocks are shorter than the old five-line ones, so the previous window ran into the next browser's install location and would have unpacked the archive into the wrong directory. Fixes #3089 Co-Authored-By: Claude Fable 5.1 --- .changeset/playwright-extension-dry-run-1-58.md | 5 +++++ packages/build/src/extensions/playwright.ts | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/playwright-extension-dry-run-1-58.md diff --git a/.changeset/playwright-extension-dry-run-1-58.md b/.changeset/playwright-extension-dry-run-1-58.md new file mode 100644 index 00000000000..32e42b2a24d --- /dev/null +++ b/.changeset/playwright-extension-dry-run-1-58.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/build": patch +--- + +Fix the `playwright` build extension for Playwright 1.58+. The extension reads `playwright install --dry-run` to find each browser's download URL, and 1.58 changed the per-browser header from `browser: chromium-headless-shell version …` to `Chrome Headless Shell … (playwright chromium-headless-shell v…)`, so the image build failed at the `grep` step with exit code 1. The header match now accepts both formats, and the context window after the header is narrowed to the two lines actually used (install location and download url): 1.58+ blocks are shorter than before, so the old window ran into the next browser's install location and would have extracted the archive into the wrong directory. diff --git a/packages/build/src/extensions/playwright.ts b/packages/build/src/extensions/playwright.ts index 0931a4855c7..2c142e1b0e7 100644 --- a/packages/build/src/extensions/playwright.ts +++ b/packages/build/src/extensions/playwright.ts @@ -317,7 +317,13 @@ class PlaywrightExtension implements BuildExtension { Array.from(browsersToInstall).forEach((browser) => { instructions.push( - `RUN grep -A5 -m1 "browser: ${browser}" /tmp/browser-info.txt > /tmp/${browser}-info.txt`, + // Playwright < 1.58 prints `browser: version `; 1.58+ prints + // ` (playwright v)`. The trailing space / `v` + // keep `chromium` from matching the `chromium-headless-shell` block. + // Only the two lines after the header are needed (install location and + // download url); 1.58+ blocks are that short, so a longer window would + // bleed into the next browser's install location. + `RUN grep -A2 -m1 -E "browser: ${browser} |\\(playwright ${browser} v" /tmp/browser-info.txt > /tmp/${browser}-info.txt`, `RUN INSTALL_DIR=$(grep "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs) && \ DIR_NAME=$(basename "$INSTALL_DIR") && \ From b9dd0f09ceca62126a6ee0f1cabf7c51d1d83b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Nerl=C3=B8e?= Date: Thu, 3 Sep 2026 00:43:56 +0200 Subject: [PATCH 2/3] fix(build): accurate comment, release-note changeset, header-pattern test Only the Chrome-for-Testing blocks shrank in 1.58+; Firefox and WebKit still print the fallback urls. The window is two lines because that is all the extension reads, not because every block is that short. Co-Authored-By: Claude Fable 5.1 --- .../playwright-extension-dry-run-1-58.md | 2 +- .../build/src/extensions/playwright.test.ts | 74 +++++++++++++++++++ packages/build/src/extensions/playwright.ts | 7 +- 3 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 packages/build/src/extensions/playwright.test.ts diff --git a/.changeset/playwright-extension-dry-run-1-58.md b/.changeset/playwright-extension-dry-run-1-58.md index 32e42b2a24d..4ea983116b9 100644 --- a/.changeset/playwright-extension-dry-run-1-58.md +++ b/.changeset/playwright-extension-dry-run-1-58.md @@ -2,4 +2,4 @@ "@trigger.dev/build": patch --- -Fix the `playwright` build extension for Playwright 1.58+. The extension reads `playwright install --dry-run` to find each browser's download URL, and 1.58 changed the per-browser header from `browser: chromium-headless-shell version …` to `Chrome Headless Shell … (playwright chromium-headless-shell v…)`, so the image build failed at the `grep` step with exit code 1. The header match now accepts both formats, and the context window after the header is narrowed to the two lines actually used (install location and download url): 1.58+ blocks are shorter than before, so the old window ran into the next browser's install location and would have extracted the archive into the wrong directory. +The `playwright` build extension now works with Playwright 1.58 and later. 1.58 changed the `playwright install --dry-run` output, which made deploy image builds fail while downloading the browsers. diff --git a/packages/build/src/extensions/playwright.test.ts b/packages/build/src/extensions/playwright.test.ts new file mode 100644 index 00000000000..f6d9bb7610d --- /dev/null +++ b/packages/build/src/extensions/playwright.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import type { BuildContext, BuildLayer } from "@trigger.dev/core/v3/build"; +import { playwright } from "./playwright.js"; + +// Real `playwright install --dry-run` headers, before and after the 1.58 format change. +const HEADERS = { + "1.57": { + chromium: "browser: chromium version 143.0.7499.4", + "chromium-headless-shell": "browser: chromium-headless-shell version 143.0.7499.4", + firefox: "browser: firefox version 144.0.2", + webkit: "browser: webkit version 26.0", + }, + "1.62": { + chromium: "Chrome for Testing 151.0.7922.34 (playwright chromium v1234)", + "chromium-headless-shell": + "Chrome Headless Shell 151.0.7922.34 (playwright chromium-headless-shell v1234)", + firefox: "Firefox 153.0 (playwright firefox v1538)", + webkit: "WebKit 26.5 (playwright webkit v2336)", + }, +} as const; + +type BrowserKey = keyof (typeof HEADERS)["1.57"]; + +function generatedInstructions(options: Parameters[0]): string[] { + let captured: BuildLayer | undefined; + + const context = { + target: "deploy", + logger: { debug: () => {} }, + addLayer: (layer: BuildLayer) => { + captured = layer; + }, + } as unknown as BuildContext; + + const manifest = { + externals: [{ name: "playwright", version: "1.62.0" }], + } as any; + + playwright(options).onBuildComplete!(context, manifest); + + return captured?.image?.instructions ?? []; +} + +/** The ERE the generated `grep -E ""` step selects a browser's block with. */ +function headerPattern(instructions: string[], browser: BrowserKey): RegExp { + const step = instructions.find((line) => line.endsWith(`> /tmp/${browser}-info.txt`)); + const match = step?.match(/grep [^"]*"(.+)" \/tmp\/browser-info\.txt/); + if (!match?.[1]) throw new Error(`no header grep generated for ${browser}`); + return new RegExp(match[1]); +} + +describe("playwright extension dry-run header parsing", () => { + const instructions = generatedInstructions({ + browsers: ["chromium", "firefox", "webkit"], + headless: false, + }); + const browsers = Object.keys(HEADERS["1.57"]) as BrowserKey[]; + + it.each(browsers)("selects the %s block in both output formats", (browser) => { + const pattern = headerPattern(instructions, browser); + + expect(pattern.test(HEADERS["1.57"][browser])).toBe(true); + expect(pattern.test(HEADERS["1.62"][browser])).toBe(true); + }); + + it.each(browsers)("does not select another browser's block for %s", (browser) => { + const pattern = headerPattern(instructions, browser); + + for (const other of browsers.filter((b) => b !== browser)) { + expect(pattern.test(HEADERS["1.57"][other])).toBe(false); + expect(pattern.test(HEADERS["1.62"][other])).toBe(false); + } + }); +}); diff --git a/packages/build/src/extensions/playwright.ts b/packages/build/src/extensions/playwright.ts index 2c142e1b0e7..49160df9383 100644 --- a/packages/build/src/extensions/playwright.ts +++ b/packages/build/src/extensions/playwright.ts @@ -319,10 +319,9 @@ class PlaywrightExtension implements BuildExtension { instructions.push( // Playwright < 1.58 prints `browser: version `; 1.58+ prints // ` (playwright v)`. The trailing space / `v` - // keep `chromium` from matching the `chromium-headless-shell` block. - // Only the two lines after the header are needed (install location and - // download url); 1.58+ blocks are that short, so a longer window would - // bleed into the next browser's install location. + // keep `chromium` from matching the `chromium-headless-shell` block, and + // only the two lines after the header (install location, download url) + // are read, so the window stops there. `RUN grep -A2 -m1 -E "browser: ${browser} |\\(playwright ${browser} v" /tmp/browser-info.txt > /tmp/${browser}-info.txt`, `RUN INSTALL_DIR=$(grep "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs) && \ From 3720e4b1e9769380db87783cc3c5e3238696fb1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Nerl=C3=B8e?= Date: Thu, 3 Sep 2026 00:55:48 +0200 Subject: [PATCH 3/3] fix(build): test the dry-run header pattern directly Lift the pattern into dryRunHeaderPattern() so the test exercises it without a hand-built BuildContext. Co-Authored-By: Claude Fable 5.1 --- .../build/src/extensions/playwright.test.ts | 45 +++---------------- packages/build/src/extensions/playwright.ts | 20 ++++++--- 2 files changed, 20 insertions(+), 45 deletions(-) diff --git a/packages/build/src/extensions/playwright.test.ts b/packages/build/src/extensions/playwright.test.ts index f6d9bb7610d..73716883e76 100644 --- a/packages/build/src/extensions/playwright.test.ts +++ b/packages/build/src/extensions/playwright.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { BuildContext, BuildLayer } from "@trigger.dev/core/v3/build"; -import { playwright } from "./playwright.js"; +import { dryRunHeaderPattern } from "./playwright.js"; // Real `playwright install --dry-run` headers, before and after the 1.58 format change. const HEADERS = { @@ -19,52 +18,18 @@ const HEADERS = { }, } as const; -type BrowserKey = keyof (typeof HEADERS)["1.57"]; - -function generatedInstructions(options: Parameters[0]): string[] { - let captured: BuildLayer | undefined; - - const context = { - target: "deploy", - logger: { debug: () => {} }, - addLayer: (layer: BuildLayer) => { - captured = layer; - }, - } as unknown as BuildContext; - - const manifest = { - externals: [{ name: "playwright", version: "1.62.0" }], - } as any; - - playwright(options).onBuildComplete!(context, manifest); - - return captured?.image?.instructions ?? []; -} - -/** The ERE the generated `grep -E ""` step selects a browser's block with. */ -function headerPattern(instructions: string[], browser: BrowserKey): RegExp { - const step = instructions.find((line) => line.endsWith(`> /tmp/${browser}-info.txt`)); - const match = step?.match(/grep [^"]*"(.+)" \/tmp\/browser-info\.txt/); - if (!match?.[1]) throw new Error(`no header grep generated for ${browser}`); - return new RegExp(match[1]); -} - -describe("playwright extension dry-run header parsing", () => { - const instructions = generatedInstructions({ - browsers: ["chromium", "firefox", "webkit"], - headless: false, - }); - const browsers = Object.keys(HEADERS["1.57"]) as BrowserKey[]; +const browsers = Object.keys(HEADERS["1.57"]) as Array; +describe("playwright extension dry-run header pattern", () => { it.each(browsers)("selects the %s block in both output formats", (browser) => { - const pattern = headerPattern(instructions, browser); + const pattern = new RegExp(dryRunHeaderPattern(browser)); expect(pattern.test(HEADERS["1.57"][browser])).toBe(true); expect(pattern.test(HEADERS["1.62"][browser])).toBe(true); }); it.each(browsers)("does not select another browser's block for %s", (browser) => { - const pattern = headerPattern(instructions, browser); + const pattern = new RegExp(dryRunHeaderPattern(browser)); for (const other of browsers.filter((b) => b !== browser)) { expect(pattern.test(HEADERS["1.57"][other])).toBe(false); diff --git a/packages/build/src/extensions/playwright.ts b/packages/build/src/extensions/playwright.ts index 49160df9383..8a390fcd8d5 100644 --- a/packages/build/src/extensions/playwright.ts +++ b/packages/build/src/extensions/playwright.ts @@ -196,6 +196,19 @@ export function playwright(options: PlaywrightExtensionOptions = {}) { return new PlaywrightExtension(options); } +/** + * Extended regex selecting a browser's block header in `playwright install --dry-run` output. + * + * Playwright < 1.58 prints `browser: version `; 1.58+ prints + * ` (playwright v)`. The trailing space / `v` keep + * `chromium` from matching the `chromium-headless-shell` block. + * + * @internal + */ +export function dryRunHeaderPattern(browser: string): string { + return `browser: ${browser} |\\(playwright ${browser} v`; +} + /** * Background: * @@ -317,12 +330,9 @@ class PlaywrightExtension implements BuildExtension { Array.from(browsersToInstall).forEach((browser) => { instructions.push( - // Playwright < 1.58 prints `browser: version `; 1.58+ prints - // ` (playwright v)`. The trailing space / `v` - // keep `chromium` from matching the `chromium-headless-shell` block, and - // only the two lines after the header (install location, download url) + // Only the two lines after the header (install location, download url) // are read, so the window stops there. - `RUN grep -A2 -m1 -E "browser: ${browser} |\\(playwright ${browser} v" /tmp/browser-info.txt > /tmp/${browser}-info.txt`, + `RUN grep -A2 -m1 -E "${dryRunHeaderPattern(browser)}" /tmp/browser-info.txt > /tmp/${browser}-info.txt`, `RUN INSTALL_DIR=$(grep "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs) && \ DIR_NAME=$(basename "$INSTALL_DIR") && \