diff --git a/.release-please/main-manifest.json b/.release-please/main-manifest.json index 78e7f27..ddfa3e3 100644 --- a/.release-please/main-manifest.json +++ b/.release-please/main-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.0" + ".": "0.11.1" } diff --git a/.release-please/next-manifest.json b/.release-please/next-manifest.json index 78e7f27..ddfa3e3 100644 --- a/.release-please/next-manifest.json +++ b/.release-please/next-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.0" + ".": "0.11.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index f26c4fa..76b724b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project are documented here, generated by [release-please](https://github.com/googleapis/release-please) from [Conventional Commits](https://www.conventionalcommits.org). +## [0.11.1](https://github.com/taraxvoid/voidflow/compare/v0.11.0...v0.11.1) (2026-10-02) + + +### Bug Fixes + +* **unlighthouse:** launch Chrome with --no-sandbox in CI ([#114](https://github.com/taraxvoid/voidflow/issues/114)) ([67daebb](https://github.com/taraxvoid/voidflow/commit/67daebbc0fe4b18163dabd392b38a85653871e3e)) + ## [0.11.0](https://github.com/taraxvoid/voidflow/compare/v0.10.0...v0.11.0) (2026-10-02) diff --git a/jsr.json b/jsr.json index 39bbbe5..085437a 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@taraxvoid/voidflow", - "version": "0.11.0", + "version": "0.11.1", "license": "MIT", "exports": { "./playwright": "./playwright/index.mjs" diff --git a/package.json b/package.json index 1c157f1..be4a64d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@taraxvoid/voidflow", - "version": "0.11.0", + "version": "0.11.1", "description": "Shared workflows, actions and tooling for @taraxvoid sites", "license": "MIT", "keywords": [ diff --git a/scripts/unlighthouse/chrome.ts b/scripts/unlighthouse/chrome.ts new file mode 100644 index 0000000..7407eb3 --- /dev/null +++ b/scripts/unlighthouse/chrome.ts @@ -0,0 +1,20 @@ +import { chmodSync, mkdtempSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +/** + * GitHub's ubuntu-24.04 runners disable unprivileged user namespaces via + * AppArmor, so Chrome aborts at launch ("No usable sandbox!"). Returns a shell + * wrapper that runs `chrome` with --no-sandbox, for use as CHROME_PATH / + * PUPPETEER_EXECUTABLE_PATH. Only used in CI, where the browser loads our own + * static build and nothing untrusted. + */ +export function noSandboxWrapper(chrome: string): string { + const dir = mkdtempSync(join(tmpdir(), 'voidflow-chrome-')) + const wrapper = join(dir, 'chrome-no-sandbox') + // Single-quote the path for sh; escape any embedded single quotes. + const quoted = `'${chrome.replaceAll("'", `'\\''`)}'` + writeFileSync(wrapper, `#!/bin/sh\nexec ${quoted} --no-sandbox "$@"\n`) + chmodSync(wrapper, 0o755) + return wrapper +} diff --git a/scripts/unlighthouse/run.ts b/scripts/unlighthouse/run.ts index d20a03c..678cba2 100755 --- a/scripts/unlighthouse/run.ts +++ b/scripts/unlighthouse/run.ts @@ -17,6 +17,7 @@ import { existsSync, readdirSync, statSync } from 'node:fs' import { homedir } from 'node:os' import { join, resolve } from 'node:path' import { parseArgs } from 'node:util' +import { noSandboxWrapper } from './chrome.ts' const { values } = parseArgs({ options: { @@ -94,9 +95,21 @@ const site = `http://localhost:${server.port}` const chrome = findPlaywrightChromium() const env: Record = { ...process.env } if (chrome) { - env.CHROME_PATH = chrome - env.PUPPETEER_EXECUTABLE_PATH = chrome - console.log(`Using Chromium: ${chrome}`) + // CI runners (ubuntu-24.04) can't create Chrome's sandbox; see chrome.ts. + // Set VOIDFLOW_CHROME_SANDBOX=1 to keep the sandbox on. + const launcher = + process.platform === 'linux' && + process.env.CI && + !process.env.VOIDFLOW_CHROME_SANDBOX + ? noSandboxWrapper(chrome) + : chrome + env.CHROME_PATH = launcher + env.PUPPETEER_EXECUTABLE_PATH = launcher + console.log( + launcher === chrome + ? `Using Chromium: ${chrome}` + : `Using Chromium: ${chrome} (--no-sandbox for CI)`, + ) } else { console.log('No Playwright Chromium found; relying on system Chrome lookup.') } diff --git a/test/unlighthouse-chrome.test.ts b/test/unlighthouse-chrome.test.ts new file mode 100644 index 0000000..7fb67e4 --- /dev/null +++ b/test/unlighthouse-chrome.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from 'bun:test' +import { chmodSync, mkdtempSync, statSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { noSandboxWrapper } from '../scripts/unlighthouse/chrome.ts' + +// A stand-in "chrome" that prints the arguments it receives. +function fakeChrome(name = 'chrome') { + const dir = mkdtempSync(join(tmpdir(), 'voidflow-fake-')) + const bin = join(dir, name) + writeFileSync(bin, '#!/bin/sh\nfor a in "$@"; do echo "arg:$a"; done\n') + chmodSync(bin, 0o755) + return bin +} + +async function run(bin: string, ...args: string[]) { + const proc = Bun.spawn([bin, ...args], { stdout: 'pipe' }) + return (await new Response(proc.stdout).text()).trim().split('\n') +} + +describe('noSandboxWrapper', () => { + test('is executable and runs chrome with --no-sandbox first', async () => { + const wrapper = noSandboxWrapper(fakeChrome()) + expect(statSync(wrapper).mode & 0o111).not.toBe(0) + expect(await run(wrapper, '--headless', 'about:blank')).toEqual([ + 'arg:--no-sandbox', + 'arg:--headless', + 'arg:about:blank', + ]) + }) + + test('handles chrome paths with spaces and quotes', async () => { + const wrapper = noSandboxWrapper(fakeChrome("Google Chrome 'x'")) + expect(await run(wrapper, '--flag')).toEqual(['arg:--no-sandbox', 'arg:--flag']) + }) +})