Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ jobs:
- name: Verify browser harness build cleanup
run: node --test scripts/fixtures/replay-process.test.mjs

- name: Verify browser runner mirror preparation
run: node --test scripts/fixtures/browser-mirrors.test.mjs

- name: Verify Docker cloud runner offline
run: |
pnpm exec tsx --test scripts/cloud/*.test.ts
Expand Down Expand Up @@ -193,7 +196,14 @@ jobs:
with:
name: package-dist
path: dist/packages/
# Prefer the runner's existing Ubuntu archive mirrors over its slow Azure mirror.
# Preserve package sources, signatures and Azure as a final fallback.
- name: Prepare browser dependency mirrors
run: |
node scripts/fixtures/browser-mirrors.mjs /etc/apt/apt-mirrors.txt "${RUNNER_TEMP}/domscribe-apt-mirrors.txt"
sudo install -m 644 "${RUNNER_TEMP}/domscribe-apt-mirrors.txt" /etc/apt/apt-mirrors.txt
- name: Install Chromium
timeout-minutes: 5
run: pnpm --filter @domscribe/test-fixtures exec playwright install --with-deps chromium
- name: Replay persisted captures in a fresh browser session
run: >-
Expand Down Expand Up @@ -297,7 +307,12 @@ jobs:

- run: pnpm install --frozen-lockfile

- name: Prepare browser dependency mirrors
run: |
node scripts/fixtures/browser-mirrors.mjs /etc/apt/apt-mirrors.txt "${RUNNER_TEMP}/domscribe-apt-mirrors.txt"
sudo install -m 644 "${RUNNER_TEMP}/domscribe-apt-mirrors.txt" /etc/apt/apt-mirrors.txt
- name: Install Playwright browsers
timeout-minutes: 5
run: pnpm --filter @domscribe/test-fixtures exec playwright install --with-deps chromium

- name: Download registry storage
Expand Down
76 changes: 76 additions & 0 deletions scripts/fixtures/browser-mirrors.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';

export function preferUbuntuMirrors(content) {
const order = [
'archive.ubuntu.com',
'security.ubuntu.com',
'azure.archive.ubuntu.com',
];
const comments = [],
rows = [];
const unsupported = () => {
throw new Error('Unsupported Ubuntu runner mirror list');
};
for (const line of content.split(/\r?\n/)) {
if (!line.trim() || line.trimStart().startsWith('#')) {
comments.push(line);
continue;
}
const match = line.match(/^(https?:\/\/[^\s]+)\t(.+)$/);
if (!match) unsupported();
let uri;
try {
uri = new URL(match[1]);
} catch {
// Native URL errors retain input; do not echo unfamiliar runner data.
unsupported();
}
const metadata = match[2].trim().split(/\s+/);
const priorities = metadata.filter((value) =>
value.startsWith('priority:'),
);
if (
!order.includes(uri.hostname) ||
uri.username ||
uri.password ||
uri.search ||
uri.hash ||
uri.port ||
!/^\/ubuntu\/?$/.test(uri.pathname) ||
priorities.length !== 1 ||
!/^priority:\d+$/.test(priorities[0])
)
unsupported();
rows.push({ uri: match[1], host: uri.hostname, metadata });
}
if (
rows.length !== order.length ||
new Set(rows.map((row) => row.host)).size !== order.length
)
unsupported();
rows.sort((a, b) => order.indexOf(a.host) - order.indexOf(b.host));
return (
[
...comments.filter((line) => line.trim()),
...rows.map(
(row, i) =>
`${row.uri}\t${row.metadata.map((value) => (/^priority:\d+$/.test(value) ? `priority:${i + 1}` : value)).join(' ')}`,
),
].join('\n') + '\n'
);
}

if (
process.argv[1] &&
pathToFileURL(resolve(process.argv[1])).href === import.meta.url
) {
const [input, output] = process.argv.slice(2);
if (!input || !output || resolve(input) === resolve(output)) {
throw new Error(
'Usage: browser-mirrors.mjs <runner-mirror-list> <prepared-list>',
);
}
writeFileSync(output, preferUbuntuMirrors(readFileSync(input, 'utf8')));
}
127 changes: 127 additions & 0 deletions scripts/fixtures/browser-mirrors.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { preferUbuntuMirrors } from './browser-mirrors.mjs';

const runnerList =
[
'http://azure.archive.ubuntu.com/ubuntu\tpriority:1',
'http://archive.ubuntu.com/ubuntu\tpriority:2',
'http://security.ubuntu.com/ubuntu\tpriority:3',
].join('\n') + '\n';

test('prefer existing official Ubuntu mirrors while retaining Azure fallback', () => {
const rows = preferUbuntuMirrors(runnerList).trim().split('\n');
assert.equal(rows[0], 'http://archive.ubuntu.com/ubuntu\tpriority:1');
assert.equal(rows[1], 'http://security.ubuntu.com/ubuntu\tpriority:2');
assert.equal(rows[2], 'http://azure.archive.ubuntu.com/ubuntu\tpriority:3');
});

test('preserve transport, metadata and comments without adding package sources', () => {
const source =
'# Hosted runner mirrors\n\n' +
runnerList
.replaceAll('http:', 'https:')
.replace('priority:2', 'priority:2 arch:amd64');
const prepared = preferUbuntuMirrors(source);
assert.match(
prepared,
/https:\/\/archive\.ubuntu\.com\/ubuntu\tpriority:1 arch:amd64/,
);
assert.match(prepared, /^# Hosted runner mirrors/m);
assert.equal((prepared.match(/https:/g) ?? []).length, 3);
});

test('refuse unfamiliar or incomplete runner lists', () => {
for (const source of [
runnerList.replace(
'archive.ubuntu.com/ubuntu\tpriority:2',
'mirror.example/ubuntu\tpriority:2',
),
runnerList.replace(/^http:\/\/azure[^\n]+\n/m, ''),
runnerList + 'http://archive.ubuntu.com/ubuntu\tpriority:4\n',
runnerList.replace('priority:2', ''),
])
assert.throws(
() => preferUbuntuMirrors(source),
/Unsupported Ubuntu runner mirror list/,
);
});

test('reject a malformed duplicate priority instead of preserving it', () => {
assert.throws(
() =>
preferUbuntuMirrors(
runnerList.replace('priority:2', 'priority:2 priority:invalid'),
),
/Unsupported Ubuntu runner mirror list/,
);
});

test('malformed URLs use a generic error without retaining raw input', () => {
assert.throws(
() =>
preferUbuntuMirrors(
runnerList.replace(
'http://archive.ubuntu.com/ubuntu',
'http://[invalid]/ubuntu',
),
),
(error) =>
error.message === 'Unsupported Ubuntu runner mirror list' &&
!('input' in error),
);
});

test('CLI writes a prepared copy and leaves the runner input unchanged', () => {
const root = mkdtempSync(join(tmpdir(), 'domscribe-browser-mirrors-'));
try {
const input = join(root, 'input'),
output = join(root, 'output');
writeFileSync(input, runnerList);
const result = spawnSync(
process.execPath,
[
new URL('./browser-mirrors.mjs', import.meta.url).pathname,
input,
output,
],
{ encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
assert.equal(readFileSync(input, 'utf8'), runnerList);
assert.match(
readFileSync(output, 'utf8'),
/^http:\/\/archive\.ubuntu\.com\/ubuntu\tpriority:1/m,
);
} finally {
rmSync(root, { recursive: true });
}
});

test('invalid CLI input fails without replacing an existing prepared copy', () => {
const root = mkdtempSync(join(tmpdir(), 'domscribe-browser-mirrors-'));
try {
const input = join(root, 'input'),
output = join(root, 'output');
writeFileSync(input, 'http://mirror.example/ubuntu\tpriority:1\n');
writeFileSync(output, 'unchanged');
const result = spawnSync(
process.execPath,
[
new URL('./browser-mirrors.mjs', import.meta.url).pathname,
input,
output,
],
{ encoding: 'utf8' },
);
assert.notEqual(result.status, 0);
assert.equal(readFileSync(output, 'utf8'), 'unchanged');
assert.doesNotMatch(result.stderr, /mirror\.example/);
} finally {
rmSync(root, { recursive: true });
}
});
Loading