@mertushka/webrtc-node 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ const root = path.resolve(__dirname, "..");
7
+ const args = process.argv.slice(2);
8
+ const failOnRetries =
9
+ (args.includes("--fail-on-retries") || process.env.WPT_FAIL_ON_RETRIES === "1") &&
10
+ !args.includes("--allow-retries") &&
11
+ process.env.WPT_ALLOW_RETRIES !== "1";
12
+ const explicitPathIndex = args.indexOf("--results");
13
+ const expectedTotalIndex = args.indexOf("--expected-total");
14
+ const resultsPath =
15
+ explicitPathIndex === -1
16
+ ? process.env.WPT_RESULTS || path.join(root, "wpt-results.json")
17
+ : args[explicitPathIndex + 1];
18
+ const manifestPath = path.join(root, "wpt-manifest.json");
19
+ const manifest = fs.existsSync(manifestPath)
20
+ ? JSON.parse(fs.readFileSync(manifestPath, "utf8"))
21
+ : {};
22
+ const expectedTotal = process.env.WPT_EXPECTED_TOTAL
23
+ ? Number(process.env.WPT_EXPECTED_TOTAL)
24
+ : expectedTotalIndex === -1
25
+ ? (manifest.expectedSelectedSubtests ?? null)
26
+ : Number(args[expectedTotalIndex + 1]);
27
+
28
+ function fail(message) {
29
+ console.error(`WPT result check failed: ${message}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ if (explicitPathIndex !== -1 && !resultsPath) {
34
+ fail("--results requires a path");
35
+ }
36
+ if (expectedTotalIndex !== -1 && !args[expectedTotalIndex + 1]) {
37
+ fail("--expected-total requires a positive integer");
38
+ }
39
+
40
+ if (!fs.existsSync(resultsPath)) {
41
+ fail(`${resultsPath} does not exist`);
42
+ }
43
+
44
+ let summary;
45
+ try {
46
+ summary = JSON.parse(fs.readFileSync(resultsPath, "utf8"));
47
+ } catch (error) {
48
+ fail(`could not parse ${resultsPath}: ${error.message}`);
49
+ }
50
+
51
+ if (!Array.isArray(summary.results)) {
52
+ fail(`${resultsPath} is not a WPT run result artifact`);
53
+ }
54
+
55
+ if (!Number.isInteger(summary.total) || summary.total < 1) {
56
+ fail("total must be a positive integer");
57
+ }
58
+
59
+ if (!Number.isInteger(summary.pass) || summary.pass < 0) {
60
+ fail("pass must be a non-negative integer");
61
+ }
62
+
63
+ if (!Number.isInteger(summary.fail) || summary.fail < 0) {
64
+ fail("fail must be a non-negative integer");
65
+ }
66
+
67
+ if (summary.results.length !== summary.total) {
68
+ fail(`results length ${summary.results.length} does not match total ${summary.total}`);
69
+ }
70
+
71
+ const passCount = summary.results.filter((result) => result.status === "PASS").length;
72
+ const failCount = summary.results.filter((result) => result.status === "FAIL").length;
73
+ const unexpectedStatuses = summary.results.filter(
74
+ (result) => result.status !== "PASS" && result.status !== "FAIL",
75
+ );
76
+ const retried = summary.results.filter((result) => Number(result.retries) > 0);
77
+
78
+ if (passCount !== summary.pass) {
79
+ fail(`pass count ${summary.pass} does not match ${passCount} PASS results`);
80
+ }
81
+
82
+ if (failCount !== summary.fail) {
83
+ fail(`fail count ${summary.fail} does not match ${failCount} FAIL results`);
84
+ }
85
+
86
+ if (unexpectedStatuses.length) {
87
+ fail(`unexpected result status ${unexpectedStatuses[0].status}`);
88
+ }
89
+
90
+ if (summary.fail !== 0 || passCount !== summary.total) {
91
+ const failures = summary.results
92
+ .filter((result) => result.status !== "PASS")
93
+ .slice(0, 5)
94
+ .map((result) => `${result.file} :: ${result.name}`)
95
+ .join("; ");
96
+ fail(`selected WPT suite did not pass: ${failures}`);
97
+ }
98
+
99
+ if (expectedTotal !== null) {
100
+ if (!Number.isInteger(expectedTotal) || expectedTotal < 1) {
101
+ fail("WPT_EXPECTED_TOTAL must be a positive integer");
102
+ }
103
+ if (summary.total !== expectedTotal) {
104
+ fail(`total ${summary.total} does not match expected selected subtests ${expectedTotal}`);
105
+ }
106
+ }
107
+
108
+ if (retried.length && failOnRetries) {
109
+ const details = retried
110
+ .slice(0, 5)
111
+ .map((result) => `${result.file} :: ${result.name} (${result.retries})`)
112
+ .join("; ");
113
+ fail(`worker retries were recorded: ${details}`);
114
+ }
115
+
116
+ const retrySuffix = retried.length ? `, retries=${retried.length}` : ", retries=0";
117
+ console.log(`WPT results verified: ${summary.pass}/${summary.total} passed${retrySuffix}`);
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { spawnSync } = require("node:child_process");
7
+
8
+ const root = path.resolve(__dirname, "..");
9
+ const manifestPath = path.join(root, "wpt-manifest.json");
10
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
11
+ const expectedTotal = manifest.expectedSelectedSubtests;
12
+
13
+ function fail(message) {
14
+ console.error(`WPT selection check failed: ${message}`);
15
+ process.exit(1);
16
+ }
17
+
18
+ if (!Number.isInteger(expectedTotal) || expectedTotal < 1) {
19
+ fail("wpt-manifest.json expectedSelectedSubtests must be a positive integer");
20
+ }
21
+
22
+ const resultsPath = path.join(
23
+ os.tmpdir(),
24
+ `webrtc-node-wpt-selection-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
25
+ );
26
+
27
+ try {
28
+ const child = spawnSync(process.execPath, [path.join("scripts", "run-wpt-subset.js")], {
29
+ cwd: root,
30
+ env: {
31
+ ...process.env,
32
+ WPT_LIST_TESTS: "1",
33
+ WPT_WORKER_RESULTS: resultsPath,
34
+ },
35
+ encoding: "utf8",
36
+ maxBuffer: 20 * 1024 * 1024,
37
+ timeout: Number(process.env.WPT_SELECTION_TIMEOUT_MS || 300000),
38
+ });
39
+
40
+ if (child.error) fail(child.error.message);
41
+ if (child.status !== 0 || child.signal) {
42
+ const output = [child.stderr, child.stdout].filter(Boolean).join("\n").trim();
43
+ fail(
44
+ output ||
45
+ (child.signal
46
+ ? `list worker terminated by ${child.signal}`
47
+ : `list worker exited with status ${child.status}`),
48
+ );
49
+ }
50
+ if (!fs.existsSync(resultsPath)) {
51
+ fail("list mode did not write a result artifact");
52
+ }
53
+
54
+ const payload = JSON.parse(fs.readFileSync(resultsPath, "utf8"));
55
+ if (!Array.isArray(payload.tests)) {
56
+ fail("list mode artifact does not contain a tests array");
57
+ }
58
+ if (!payload.tests.every((test) => typeof test === "string" && test.length > 0)) {
59
+ fail("list mode artifact contains an invalid test name");
60
+ }
61
+ if (payload.tests.length !== expectedTotal) {
62
+ fail(`selected ${payload.tests.length} subtests, expected ${expectedTotal}`);
63
+ }
64
+
65
+ console.log(`WPT selection verified: ${payload.tests.length} selected subtests`);
66
+ } finally {
67
+ try {
68
+ fs.unlinkSync(resultsPath);
69
+ } catch {
70
+ // Best-effort temp cleanup.
71
+ }
72
+ }
@@ -0,0 +1,118 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { spawnSync } = require("node:child_process");
4
+
5
+ const root = path.resolve(__dirname, "..");
6
+ const manifest = require("../wpt-manifest.json");
7
+
8
+ const wptDir = path.resolve(process.env.WPT_DIR || path.join(root, "wpt"));
9
+ const wptRepository = process.env.WPT_REPOSITORY || "https://github.com/web-platform-tests/wpt.git";
10
+ const wptCommit = process.env.WPT_COMMIT || manifest.wptCommit;
11
+ const sparsePaths = ["common", "resources", "webrtc"];
12
+
13
+ function runGit(args, options = {}) {
14
+ const result = spawnSync("git", args, {
15
+ cwd: options.cwd || root,
16
+ encoding: "utf8",
17
+ stdio: options.stdio || "pipe",
18
+ });
19
+ if (result.status !== 0) {
20
+ const stderr = result.stderr ? result.stderr.trim() : "";
21
+ const stdout = result.stdout ? result.stdout.trim() : "";
22
+ const detail = stderr || stdout || `git exited with status ${result.status}`;
23
+ throw new Error(`git ${args.join(" ")} failed: ${detail}`);
24
+ }
25
+ return (result.stdout || "").trim();
26
+ }
27
+
28
+ function hasWptFiles() {
29
+ return (
30
+ fs.existsSync(path.join(wptDir, "resources", "testharness.js")) &&
31
+ fs.existsSync(path.join(wptDir, "common", "gc.js")) &&
32
+ fs.existsSync(path.join(wptDir, "webrtc"))
33
+ );
34
+ }
35
+
36
+ function currentWptCommit() {
37
+ if (!fs.existsSync(path.join(wptDir, ".git"))) return null;
38
+ try {
39
+ return runGit(["rev-parse", "HEAD"], { cwd: wptDir });
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ function ensureCleanCheckout() {
46
+ const status = runGit(["status", "--porcelain"], { cwd: wptDir });
47
+ if (status) {
48
+ throw new Error(
49
+ `wpt checkout at ${wptDir} has local changes. ` +
50
+ "Commit, stash, or remove them before changing the pinned WPT checkout.",
51
+ );
52
+ }
53
+ }
54
+
55
+ function checkoutPinnedCommit() {
56
+ runGit(["sparse-checkout", "init", "--cone"], { cwd: wptDir });
57
+ runGit(["sparse-checkout", "set", ...sparsePaths], { cwd: wptDir });
58
+ runGit(["fetch", "--depth", "1", "origin", wptCommit], { cwd: wptDir, stdio: "inherit" });
59
+ runGit(["checkout", "--detach", "FETCH_HEAD"], { cwd: wptDir, stdio: "inherit" });
60
+ }
61
+
62
+ function clonePinnedWpt() {
63
+ fs.mkdirSync(wptDir, { recursive: true });
64
+ runGit(["init"], { cwd: wptDir });
65
+ runGit(["remote", "add", "origin", wptRepository], { cwd: wptDir });
66
+ checkoutPinnedCommit();
67
+ }
68
+
69
+ function ensureWpt(options = {}) {
70
+ const quiet = Boolean(options.quiet);
71
+ if (!wptCommit) throw new Error("wptCommit is missing from wpt-manifest.json");
72
+
73
+ if (!fs.existsSync(wptDir)) {
74
+ if (!quiet) console.log(`Fetching WPT ${wptCommit} into ${wptDir}`);
75
+ clonePinnedWpt();
76
+ } else {
77
+ const actualCommit = currentWptCommit();
78
+ if (actualCommit === wptCommit && hasWptFiles()) {
79
+ if (!quiet) console.log(`WPT checkout is pinned at ${wptCommit}`);
80
+ return;
81
+ }
82
+
83
+ if (actualCommit === null) {
84
+ if (hasWptFiles()) {
85
+ if (!quiet) {
86
+ console.warn(
87
+ `Using existing non-git WPT tree at ${wptDir}; commit pin cannot be verified.`,
88
+ );
89
+ }
90
+ return;
91
+ }
92
+ throw new Error(`WPT directory exists at ${wptDir}, but it is not a usable git checkout.`);
93
+ }
94
+
95
+ ensureCleanCheckout();
96
+ if (!quiet) console.log(`Updating WPT from ${actualCommit} to ${wptCommit}`);
97
+ checkoutPinnedCommit();
98
+ }
99
+
100
+ const actualCommit = currentWptCommit();
101
+ if (actualCommit !== wptCommit) {
102
+ throw new Error(`WPT checkout is ${actualCommit || "unknown"}, expected ${wptCommit}`);
103
+ }
104
+ if (!hasWptFiles()) {
105
+ throw new Error(`WPT checkout at ${wptDir} is missing required common/resources/webrtc files.`);
106
+ }
107
+ }
108
+
109
+ if (require.main === module) {
110
+ try {
111
+ ensureWpt();
112
+ } catch (error) {
113
+ console.error(error.message);
114
+ process.exitCode = 1;
115
+ }
116
+ }
117
+
118
+ module.exports = { ensureWpt };
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { spawnSync } = require("node:child_process");
6
+ const libc = require("detect-libc");
7
+ const tar = require("tar");
8
+
9
+ const root = path.resolve(__dirname, "..");
10
+ const packageJson = require("../package.json");
11
+ const moduleName = "webrtc_node.node";
12
+ const releaseBaseUrl = "https://github.com/mertushka/webrtc-node/releases/download";
13
+
14
+ function envFlag(name) {
15
+ return /^(1|true|yes)$/i.test(String(process.env[name] || ""));
16
+ }
17
+
18
+ function isSourceCheckout() {
19
+ return fs.existsSync(path.join(root, ".git")) && !root.split(path.sep).includes("node_modules");
20
+ }
21
+
22
+ function hasNativeAddon() {
23
+ try {
24
+ require("../lib/load-native");
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ function linuxLibcTag() {
32
+ if (process.platform !== "linux") return null;
33
+ const family = libc.familySync();
34
+ if (family === libc.MUSL) return "musl";
35
+ if (family === libc.GLIBC) return "glibc";
36
+ return null;
37
+ }
38
+
39
+ function targetTuple() {
40
+ return [process.platform, process.arch, linuxLibcTag()].filter(Boolean).join("-");
41
+ }
42
+
43
+ function releaseTag() {
44
+ return process.env.WEBRTC_NODE_PREBUILD_TAG || `v${packageJson.version}`;
45
+ }
46
+
47
+ function prebuildAssetName() {
48
+ return `webrtc-node-${releaseTag()}-napi-v8-${targetTuple()}.tar.gz`;
49
+ }
50
+
51
+ function prebuildUrl() {
52
+ return `${releaseBaseUrl}/${releaseTag()}/${prebuildAssetName()}`;
53
+ }
54
+
55
+ async function downloadPrebuild() {
56
+ const response = await fetch(prebuildUrl(), {
57
+ headers: { "user-agent": "webrtc-node-install" },
58
+ });
59
+ if (!response.ok) {
60
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
61
+ }
62
+
63
+ const outputDir = path.join(root, "build", "Release");
64
+ const archivePath = path.join(outputDir, prebuildAssetName());
65
+ fs.mkdirSync(outputDir, { recursive: true });
66
+ const buffer = Buffer.from(await response.arrayBuffer());
67
+ fs.writeFileSync(archivePath, buffer);
68
+ await tar.x({ file: archivePath, cwd: outputDir });
69
+ fs.unlinkSync(archivePath);
70
+ }
71
+
72
+ function runBuild() {
73
+ const npm = process.env.npm_execpath
74
+ ? process.execPath
75
+ : process.platform === "win32"
76
+ ? "npm.cmd"
77
+ : "npm";
78
+ const args = process.env.npm_execpath
79
+ ? [process.env.npm_execpath, "run", "build"]
80
+ : ["run", "build"];
81
+ const result = spawnSync(npm, args, {
82
+ cwd: root,
83
+ stdio: "inherit",
84
+ });
85
+ return result.status === 0 && !result.signal;
86
+ }
87
+
88
+ async function main() {
89
+ const buildFromSource = envFlag("npm_config_build_from_source");
90
+ if (!buildFromSource && hasNativeAddon()) return;
91
+
92
+ if (isSourceCheckout() && !buildFromSource) {
93
+ console.log("Skipping native install in source checkout. Run npm run build explicitly.");
94
+ return;
95
+ }
96
+
97
+ if (!buildFromSource) {
98
+ try {
99
+ await downloadPrebuild();
100
+ if (hasNativeAddon()) return;
101
+ throw new Error(`downloaded archive did not provide ${moduleName}`);
102
+ } catch (error) {
103
+ console.warn(`Prebuilt binary unavailable for ${targetTuple()}: ${error.message}`);
104
+ }
105
+ }
106
+
107
+ if (!runBuild()) {
108
+ console.error("No compatible prebuilt binary was found and the cmake-js source build failed.");
109
+ process.exit(1);
110
+ }
111
+ }
112
+
113
+ main().catch((error) => {
114
+ console.error(error);
115
+ process.exit(1);
116
+ });
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const libc = require("detect-libc");
6
+ const tar = require("tar");
7
+
8
+ const root = path.resolve(__dirname, "..");
9
+ const packageJson = require("../package.json");
10
+ const moduleName = "webrtc_node.node";
11
+
12
+ function option(name, defaultValue = undefined) {
13
+ const prefix = `--${name}=`;
14
+ const arg = process.argv.slice(2).find((value) => value.startsWith(prefix));
15
+ return arg
16
+ ? arg.slice(prefix.length)
17
+ : process.env[`PREBUILD_${name.toUpperCase()}`] || defaultValue;
18
+ }
19
+
20
+ function fail(message) {
21
+ console.error(`Prebuild package failed: ${message}`);
22
+ process.exit(1);
23
+ }
24
+
25
+ function detectLibcTag(platform) {
26
+ if (platform !== "linux") return null;
27
+ const family = libc.familySync();
28
+ if (family === libc.MUSL) return "musl";
29
+ if (family === libc.GLIBC) return "glibc";
30
+ return option("libc", null);
31
+ }
32
+
33
+ const platform = option("platform", process.platform);
34
+ const arch = option("arch", process.arch);
35
+ const libcTag = option("libc", detectLibcTag(platform));
36
+ const source = path.resolve(option("source", path.join(root, "build", "Release", moduleName)));
37
+ const releaseTag = option("tag", `v${packageJson.version}`);
38
+
39
+ async function main() {
40
+ if (!fs.existsSync(source)) fail(`native addon not found at ${source}`);
41
+
42
+ const tuple = [platform, arch, libcTag].filter(Boolean).join("-");
43
+ const artifactDir = path.join(root, "prebuild-artifacts");
44
+ const stagingDir = path.join(artifactDir, tuple);
45
+ const archiveName = `webrtc-node-${releaseTag}-napi-v8-${tuple}.tar.gz`;
46
+ const archivePath = path.join(artifactDir, archiveName);
47
+
48
+ fs.rmSync(stagingDir, { recursive: true, force: true });
49
+ fs.mkdirSync(stagingDir, { recursive: true });
50
+ fs.copyFileSync(source, path.join(stagingDir, moduleName));
51
+
52
+ const extraFiles = String(option("extra", process.env.PREBUILD_EXTRA_FILES || "") || "")
53
+ .split(";")
54
+ .map((file) => file.trim())
55
+ .filter(Boolean);
56
+
57
+ for (const extraFile of extraFiles) {
58
+ const resolved = path.resolve(extraFile);
59
+ if (!fs.existsSync(resolved)) fail(`extra file not found at ${resolved}`);
60
+ fs.copyFileSync(resolved, path.join(stagingDir, path.basename(resolved)));
61
+ }
62
+
63
+ await tar.c({ gzip: true, file: archivePath, cwd: stagingDir }, fs.readdirSync(stagingDir));
64
+ fs.rmSync(stagingDir, { recursive: true, force: true });
65
+
66
+ console.log(`Packaged ${path.relative(root, archivePath).replace(/\\/g, "/")}`);
67
+ }
68
+
69
+ main().catch((error) => fail(error.message || String(error)));
@@ -0,0 +1,7 @@
1
+ const manifest = require("../wpt-manifest.json");
2
+
3
+ for (const [group, tests] of Object.entries(manifest)) {
4
+ if (!Array.isArray(tests)) continue;
5
+ console.log(`${group}:`);
6
+ for (const test of tests) console.log(` ${test}`);
7
+ }
@@ -0,0 +1,73 @@
1
+ param(
2
+ [string]$NodeImage = "node:20-bookworm",
3
+ [string]$ArtifactsDir = "ci-artifacts/docker-linux-node20",
4
+ [switch]$SkipWpt,
5
+ [string[]]$WptSelector = @(),
6
+ [int]$WptExpectedTotal = 0
7
+ )
8
+
9
+ $ErrorActionPreference = "Stop"
10
+
11
+ $root = Resolve-Path (Join-Path $PSScriptRoot "..")
12
+ $artifactPath = Join-Path $root $ArtifactsDir
13
+ New-Item -ItemType Directory -Force $artifactPath | Out-Null
14
+
15
+ $rootForDocker = $root.Path -replace "\\", "/"
16
+ $artifactForDocker = (Resolve-Path $artifactPath).Path -replace "\\", "/"
17
+ $wptSelectorArgs = ($WptSelector | ForEach-Object {
18
+ if ($_.Contains("'")) {
19
+ throw "WPT selector cannot contain a single quote: $_"
20
+ }
21
+ "'$_'"
22
+ }) -join " "
23
+ $wptTestCommand = if ($WptSelector.Count -gt 0) {
24
+ "npm run wpt:test -- $wptSelectorArgs"
25
+ } else {
26
+ "npm run wpt:test"
27
+ }
28
+ $wptCheckCommand = if ($WptSelector.Count -gt 0) {
29
+ $expectedTotal = if ($WptExpectedTotal -gt 0) { $WptExpectedTotal } else { $WptSelector.Count }
30
+ "WPT_EXPECTED_TOTAL=$expectedTotal npm run wpt:check:strict"
31
+ } else {
32
+ "npm run wpt:check:strict"
33
+ }
34
+ $wptReportCommand = if ($WptSelector.Count -gt 0) {
35
+ "true"
36
+ } else {
37
+ "npm run wpt:report -- --output /out/wpt-report.md && RUNNER_OS=Linux RUNNER_ARCH=X64 node scripts/write-ci-evidence.js --results /out/wpt-results.json --output /out/ci-evidence.json"
38
+ }
39
+ $wptCommand = if ($SkipWpt) {
40
+ "npm run wpt:selection:check"
41
+ } else {
42
+ "npm run wpt:selection:check && WPT_TEST_TIMEOUT_MS=180000 WPT_WORKER_TIMEOUT_MS=600000 WPT_WORKER_DELAY_MS=2000 WPT_CLEANUP_DELAY_MS=3000 $wptTestCommand 2>&1 | tee /out/wpt-output.txt && cp wpt-results.json /out/wpt-results.json && $wptCheckCommand && $wptReportCommand"
43
+ }
44
+
45
+ docker run --rm `
46
+ -v "${rootForDocker}:/src:ro" `
47
+ -v "${artifactForDocker}:/out" `
48
+ $NodeImage `
49
+ bash -lc "set -euo pipefail; mkdir -p /tmp/webrtc-node; tar -C /src --exclude='./build' --exclude='./node_modules' --exclude='./.git' --exclude='./wpt-results.json' --exclude='./wpt-report.md' --exclude='./ci-artifacts' -cf - . | tar -C /tmp/webrtc-node -xf -; cd /tmp/webrtc-node; if [ -f /etc/apt/sources.list.d/debian.sources ]; then sed -i '0,/URIs: http:\/\/deb.debian.org\/debian$/s//URIs: http:\/\/snapshot.debian.org\/archive\/debian\/20260421T000000Z/' /etc/apt/sources.list.d/debian.sources; sed -i '0,/URIs: http:\/\/deb.debian.org\/debian-security$/s//URIs: http:\/\/snapshot.debian.org\/archive\/debian-security\/20260421T000000Z/' /etc/apt/sources.list.d/debian.sources; fi; for attempt in 1 2 3; do if apt-get -o Acquire::Check-Valid-Until=false update >/out/apt-update.txt 2>&1 && apt-get install -y cmake ninja-build libssl-dev >/out/apt-install.txt 2>&1; then break; fi; if [ ""`$attempt"" = 3 ]; then exit 1; fi; sleep `$((attempt * 10)); done; npm ci 2>&1 | tee /out/npm-ci.txt; npm run check; npm run native:check; npm run build 2>&1 | tee /out/build-output.txt; npm test; npm run api:check; npm run types:check; npm run wpt:ensure; set +e; ${wptCommand}; wpt_status=`$?; set -e; cp wpt-results.json /out/wpt-results.json 2>/dev/null || true; cp wpt-manifest.json /out/wpt-manifest.json; npm run wpt:manifest > /out/wpt-manifest.txt; exit `$wpt_status"
50
+
51
+ $dockerExitCode = $LASTEXITCODE
52
+
53
+ if (-not $SkipWpt) {
54
+ $resultsPath = Join-Path $artifactPath "wpt-results.json"
55
+ if (-not (Test-Path $resultsPath)) {
56
+ throw "Docker CI did not produce $resultsPath"
57
+ }
58
+
59
+ $results = Get-Content -Raw -Path $resultsPath | ConvertFrom-Json
60
+ if ([int]$results.fail -gt 0) {
61
+ throw "Docker CI WPT subset failed: $($results.pass)/$($results.total) passed"
62
+ }
63
+ $retried = @($results.results | Where-Object {
64
+ ($_.PSObject.Properties.Name -contains "retries") -and [int]$_.retries -gt 0
65
+ }).Count
66
+ if ($retried -gt 0) {
67
+ throw "Docker CI WPT subset required retries: $retried"
68
+ }
69
+ }
70
+
71
+ if ($dockerExitCode -ne 0) {
72
+ throw "Docker CI failed with exit code $dockerExitCode"
73
+ }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ node_image="node:20-bookworm"
5
+ artifacts_dir="ci-artifacts/docker-linux-node20"
6
+ skip_wpt=0
7
+ wpt_expected_total=0
8
+ wpt_selectors=()
9
+
10
+ usage() {
11
+ cat <<'EOF'
12
+ Usage: bash scripts/run-docker-linux-ci.sh [options]
13
+
14
+ Options:
15
+ --node-image IMAGE Node Docker image to use (default: node:20-bookworm)
16
+ --artifacts-dir DIR Output directory for logs/artifacts
17
+ --skip-wpt Run build/unit/API/type/WPT-selection checks only
18
+ --wpt-selector SELECTOR Run one WPT file or file#subtest selector; repeatable
19
+ --wpt-expected-total N Expected selected subtest count for targeted WPT
20
+ -h, --help Show this help
21
+ EOF
22
+ }
23
+
24
+ while [[ $# -gt 0 ]]; do
25
+ case "$1" in
26
+ --node-image)
27
+ node_image="${2:?--node-image requires a value}"
28
+ shift 2
29
+ ;;
30
+ --artifacts-dir)
31
+ artifacts_dir="${2:?--artifacts-dir requires a value}"
32
+ shift 2
33
+ ;;
34
+ --skip-wpt)
35
+ skip_wpt=1
36
+ shift
37
+ ;;
38
+ --wpt-selector)
39
+ wpt_selectors+=("${2:?--wpt-selector requires a value}")
40
+ shift 2
41
+ ;;
42
+ --wpt-expected-total)
43
+ wpt_expected_total="${2:?--wpt-expected-total requires a value}"
44
+ shift 2
45
+ ;;
46
+ -h | --help)
47
+ usage
48
+ exit 0
49
+ ;;
50
+ *)
51
+ echo "Unknown option: $1" >&2
52
+ usage >&2
53
+ exit 2
54
+ ;;
55
+ esac
56
+ done
57
+
58
+ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
59
+ root="$(cd -- "$script_dir/.." && pwd)"
60
+ artifact_path="$root/$artifacts_dir"
61
+ mkdir -p "$artifact_path"
62
+
63
+ wpt_selector_args=""
64
+ for selector in "${wpt_selectors[@]}"; do
65
+ if [[ "$selector" == *"'"* ]]; then
66
+ echo "WPT selector cannot contain a single quote: $selector" >&2
67
+ exit 2
68
+ fi
69
+ wpt_selector_args+=" '$selector'"
70
+ done
71
+
72
+ if [[ ${#wpt_selectors[@]} -gt 0 ]]; then
73
+ wpt_test_command="npm run wpt:test --$wpt_selector_args"
74
+ if [[ "$wpt_expected_total" -gt 0 ]]; then
75
+ expected_total="$wpt_expected_total"
76
+ else
77
+ expected_total="${#wpt_selectors[@]}"
78
+ fi
79
+ wpt_check_command="WPT_EXPECTED_TOTAL=$expected_total npm run wpt:check:strict"
80
+ wpt_report_command="true"
81
+ else
82
+ wpt_test_command="npm run wpt:test"
83
+ wpt_check_command="npm run wpt:check:strict"
84
+ wpt_report_command="npm run wpt:report -- --output /out/wpt-report.md && RUNNER_OS=Linux RUNNER_ARCH=X64 node scripts/write-ci-evidence.js --results /out/wpt-results.json --output /out/ci-evidence.json"
85
+ fi
86
+
87
+ if [[ "$skip_wpt" -eq 1 ]]; then
88
+ wpt_command="npm run wpt:selection:check"
89
+ else
90
+ wpt_command="npm run wpt:selection:check && WPT_TEST_TIMEOUT_MS=180000 WPT_WORKER_TIMEOUT_MS=600000 WPT_WORKER_DELAY_MS=2000 WPT_CLEANUP_DELAY_MS=3000 $wpt_test_command 2>&1 | tee /out/wpt-output.txt && cp wpt-results.json /out/wpt-results.json && $wpt_check_command && $wpt_report_command"
91
+ fi
92
+
93
+ docker run --rm \
94
+ -v "$root:/src:ro" \
95
+ -v "$artifact_path:/out" \
96
+ "$node_image" \
97
+ bash -lc "set -euo pipefail; mkdir -p /tmp/webrtc-node; tar -C /src --exclude='./build' --exclude='./node_modules' --exclude='./.git' --exclude='./wpt-results.json' --exclude='./wpt-report.md' --exclude='./ci-artifacts' -cf - . | tar -C /tmp/webrtc-node -xf -; cd /tmp/webrtc-node; if [ -f /etc/apt/sources.list.d/debian.sources ]; then sed -i '0,/URIs: http:\/\/deb.debian.org\/debian$/s//URIs: http:\/\/snapshot.debian.org\/archive\/debian\/20260421T000000Z/' /etc/apt/sources.list.d/debian.sources; sed -i '0,/URIs: http:\/\/deb.debian.org\/debian-security$/s//URIs: http:\/\/snapshot.debian.org\/archive\/debian-security\/20260421T000000Z/' /etc/apt/sources.list.d/debian.sources; fi; for attempt in 1 2 3; do if apt-get -o Acquire::Check-Valid-Until=false update >/out/apt-update.txt 2>&1 && apt-get install -y cmake ninja-build libssl-dev >/out/apt-install.txt 2>&1; then break; fi; if [ \"\$attempt\" = 3 ]; then exit 1; fi; sleep \$((attempt * 10)); done; npm ci 2>&1 | tee /out/npm-ci.txt; npm run check; npm run native:check; npm run build 2>&1 | tee /out/build-output.txt; npm test; npm run api:check; npm run types:check; npm run wpt:ensure; set +e; $wpt_command; wpt_status=\$?; set -e; cp wpt-results.json /out/wpt-results.json 2>/dev/null || true; cp wpt-manifest.json /out/wpt-manifest.json; npm run wpt:manifest > /out/wpt-manifest.txt; exit \$wpt_status"