@hominis/fireforge 0.44.3 → 0.45.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/build.js +9 -4
  4. package/dist/src/commands/doctor-source-pin.d.ts +29 -0
  5. package/dist/src/commands/doctor-source-pin.js +81 -0
  6. package/dist/src/commands/doctor.js +2 -0
  7. package/dist/src/commands/export-flow.js +3 -18
  8. package/dist/src/commands/furnace/create-browser-test.d.ts +9 -0
  9. package/dist/src/commands/furnace/create-browser-test.js +6 -6
  10. package/dist/src/commands/furnace/create-dry-run.d.ts +10 -0
  11. package/dist/src/commands/furnace/create-dry-run.js +16 -11
  12. package/dist/src/commands/furnace/create-xpcshell.js +2 -2
  13. package/dist/src/commands/furnace/create.js +5 -0
  14. package/dist/src/commands/patch/move-files-into.js +3 -6
  15. package/dist/src/commands/patch/split-plan.js +3 -6
  16. package/dist/src/commands/re-export-adjacent.d.ts +8 -0
  17. package/dist/src/commands/re-export-adjacent.js +26 -1
  18. package/dist/src/commands/re-export-drift.d.ts +8 -0
  19. package/dist/src/commands/re-export-drift.js +61 -0
  20. package/dist/src/commands/re-export-files.js +3 -6
  21. package/dist/src/commands/re-export-options.js +19 -5
  22. package/dist/src/commands/re-export-register.js +5 -1
  23. package/dist/src/commands/re-export-scan.js +3 -6
  24. package/dist/src/commands/re-export.js +16 -16
  25. package/dist/src/commands/test-build-phase.js +3 -2
  26. package/dist/src/commands/test-diagnose.js +14 -5
  27. package/dist/src/commands/test-register.js +1 -0
  28. package/dist/src/commands/test-run.js +4 -3
  29. package/dist/src/commands/test-stale-gate.js +5 -2
  30. package/dist/src/commands/test.js +9 -2
  31. package/dist/src/core/build-audit.d.ts +20 -0
  32. package/dist/src/core/build-audit.js +69 -3
  33. package/dist/src/core/build-baseline-types.d.ts +28 -0
  34. package/dist/src/core/build-baseline.d.ts +16 -1
  35. package/dist/src/core/build-baseline.js +38 -2
  36. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  37. package/dist/src/core/build-jar-escalation.js +127 -0
  38. package/dist/src/core/build-prepare.d.ts +12 -1
  39. package/dist/src/core/build-prepare.js +68 -6
  40. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  41. package/dist/src/core/config-validate-file-size.js +65 -0
  42. package/dist/src/core/config-validate.js +6 -0
  43. package/dist/src/core/engine-changes.d.ts +18 -0
  44. package/dist/src/core/engine-changes.js +34 -0
  45. package/dist/src/core/furnace-constants.d.ts +21 -0
  46. package/dist/src/core/furnace-constants.js +26 -1
  47. package/dist/src/core/harness-orphans.d.ts +73 -0
  48. package/dist/src/core/harness-orphans.js +186 -0
  49. package/dist/src/core/license-headers.d.ts +9 -5
  50. package/dist/src/core/license-headers.js +33 -7
  51. package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
  52. package/dist/src/core/mach-known-noise-filter.js +24 -1
  53. package/dist/src/core/mach.d.ts +7 -0
  54. package/dist/src/core/mach.js +18 -2
  55. package/dist/src/core/patch-apply.js +49 -20
  56. package/dist/src/core/patch-lint-checkjs.js +6 -1
  57. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  58. package/dist/src/core/patch-lint-file-size.js +53 -0
  59. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  60. package/dist/src/core/patch-lint-prettier.js +126 -0
  61. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  62. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  63. package/dist/src/core/patch-lint.js +23 -24
  64. package/dist/src/core/patch-transform.d.ts +21 -0
  65. package/dist/src/core/patch-transform.js +38 -0
  66. package/dist/src/core/smoke-patterns.js +12 -0
  67. package/dist/src/core/test-harness-crash.js +61 -48
  68. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  69. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  70. package/dist/src/core/test-stale-check.d.ts +23 -6
  71. package/dist/src/core/test-stale-check.js +77 -15
  72. package/dist/src/core/test-stall-triage.js +37 -19
  73. package/dist/src/errors/build.d.ts +28 -0
  74. package/dist/src/errors/build.js +49 -0
  75. package/dist/src/types/commands/options.d.ts +19 -0
  76. package/dist/src/types/commands/patches.d.ts +17 -6
  77. package/dist/src/types/config.d.ts +41 -0
  78. package/dist/src/utils/fs.d.ts +4 -2
  79. package/dist/src/utils/fs.js +3 -1
  80. package/docs/configuration.md +47 -0
  81. package/docs/exit-codes.md +4 -2
  82. package/docs/testing.md +83 -0
  83. package/package.json +1 -1
@@ -12,12 +12,33 @@ export declare const JAR_MN = "toolkit/content/jar.mn";
12
12
  export declare const WIDGETS_DIR = "toolkit/content/widgets";
13
13
  /** Default Fluent localization directory for toolkit global components, relative to engine root */
14
14
  export declare const FTL_DIR = "toolkit/locales/en-US/toolkit/global";
15
+ /**
16
+ * Engine-relative root every browser-chrome / xpcshell test scaffold lives
17
+ * under, trailing slash included. A `--test-dir` override must stay below
18
+ * it so the `browser/base/moz.build` manifest registration keeps working.
19
+ */
20
+ export declare const BROWSER_TEST_SCAFFOLD_ROOT = "browser/base/content/test/";
15
21
  /**
16
22
  * Returns the engine-relative directory that holds xpcshell scaffolds for
17
23
  * a given binary. Matches the form `create-xpcshell.ts` writes and the
18
24
  * path `remove.ts` / `rename.ts` / `validate.ts` must clean up.
19
25
  */
20
26
  export declare function xpcshellTestParentDir(binaryName: string): string;
27
+ /**
28
+ * Resolves the engine-relative directory a browser-chrome scaffold is
29
+ * written to: the `--test-dir` override when given, else
30
+ * `browser/base/content/test/<binaryName>`. The scaffolder AND the
31
+ * dry-run / success formatters resolve through this one function, so the
32
+ * printed plan cannot disagree with the files that land on disk.
33
+ */
34
+ export declare function resolveBrowserChromeTestDir(binaryName: string, override?: string): string;
35
+ /**
36
+ * Resolves the engine-relative directory an xpcshell scaffold is written
37
+ * to. A `--test-dir` override names the FINAL directory (no per-component
38
+ * segment is appended); the default is
39
+ * `browser/base/content/test/<binaryName>-xpcshell/<componentName>`.
40
+ */
41
+ export declare function resolveXpcshellTestDir(binaryName: string, componentName: string, override?: string): string;
21
42
  /** Returns true when `fileName` has one of the standard component file extensions. */
22
43
  export declare function isComponentSourceFile(fileName: string): boolean;
23
44
  /**
@@ -13,6 +13,12 @@ export const JAR_MN = 'toolkit/content/jar.mn';
13
13
  export const WIDGETS_DIR = 'toolkit/content/widgets';
14
14
  /** Default Fluent localization directory for toolkit global components, relative to engine root */
15
15
  export const FTL_DIR = 'toolkit/locales/en-US/toolkit/global';
16
+ /**
17
+ * Engine-relative root every browser-chrome / xpcshell test scaffold lives
18
+ * under, trailing slash included. A `--test-dir` override must stay below
19
+ * it so the `browser/base/moz.build` manifest registration keeps working.
20
+ */
21
+ export const BROWSER_TEST_SCAFFOLD_ROOT = 'browser/base/content/test/';
16
22
  /**
17
23
  * Suffix for the per-binary xpcshell scaffold parent directory. Components
18
24
  * created with `furnace create --with-tests --xpcshell` land at
@@ -27,7 +33,26 @@ const XPCSHELL_TEST_DIR_SUFFIX = '-xpcshell';
27
33
  * path `remove.ts` / `rename.ts` / `validate.ts` must clean up.
28
34
  */
29
35
  export function xpcshellTestParentDir(binaryName) {
30
- return `browser/base/content/test/${binaryName}${XPCSHELL_TEST_DIR_SUFFIX}`;
36
+ return `${BROWSER_TEST_SCAFFOLD_ROOT}${binaryName}${XPCSHELL_TEST_DIR_SUFFIX}`;
37
+ }
38
+ /**
39
+ * Resolves the engine-relative directory a browser-chrome scaffold is
40
+ * written to: the `--test-dir` override when given, else
41
+ * `browser/base/content/test/<binaryName>`. The scaffolder AND the
42
+ * dry-run / success formatters resolve through this one function, so the
43
+ * printed plan cannot disagree with the files that land on disk.
44
+ */
45
+ export function resolveBrowserChromeTestDir(binaryName, override) {
46
+ return override ?? `${BROWSER_TEST_SCAFFOLD_ROOT}${binaryName}`;
47
+ }
48
+ /**
49
+ * Resolves the engine-relative directory an xpcshell scaffold is written
50
+ * to. A `--test-dir` override names the FINAL directory (no per-component
51
+ * segment is appended); the default is
52
+ * `browser/base/content/test/<binaryName>-xpcshell/<componentName>`.
53
+ */
54
+ export function resolveXpcshellTestDir(binaryName, componentName, override) {
55
+ return override ?? `${xpcshellTestParentDir(binaryName)}/${componentName}`;
31
56
  }
32
57
  /** File extensions that constitute a Furnace component's source files. */
33
58
  const COMPONENT_FILE_EXTENSIONS = ['.mjs', '.css', '.ftl'];
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Preflight census of ORPHANED test-harness helper processes.
3
+ *
4
+ * Stopping a long mochitest run mid-flight does not necessarily take its
5
+ * helpers with it. A downstream incident left `xpcshell` (the harness
6
+ * httpd, pegged at 100% CPU), `pywebsocket`, `ssltunnel` and `moz-http2`
7
+ * alive for an hour. Every subsequent run crawled — a three-second suite
8
+ * took six minutes of wall clock and audio-start waits timed out — and
9
+ * nothing in FireForge's output connected the slowness to the survivors.
10
+ * The hour was spent mis-attributing it to the change under test.
11
+ *
12
+ * Visibility alone would have collapsed that hour, which is why the census
13
+ * runs by default and the kill does not. Two rules keep it honest:
14
+ *
15
+ * - The census runs at PREFLIGHT, before this run spawns anything, so
16
+ * every matching process is by construction a survivor of an earlier
17
+ * run. No age heuristic is needed, and none is used.
18
+ * - A match must be anchored to a Firefox OBJDIR. `xpcshell` and
19
+ * `server.js` are far too generic on their own; the same rule
20
+ * `mochitest-server-port.ts` applies to the httpd holder applies here,
21
+ * for the same reason — FireForge has no business reporting, let alone
22
+ * terminating, a process it cannot attribute to the harness.
23
+ *
24
+ * `--reap-orphans` opts into termination. Without it the census is
25
+ * report-only, exactly like the `Orphaned harness workers` doctor check
26
+ * (which covers a DIFFERENT shape: reparented Python multiprocessing
27
+ * workers, matched on PPID 1 and accumulated CPU time).
28
+ */
29
+ /** One surviving harness helper process. */
30
+ export interface OrphanedHarnessProcess {
31
+ pid: number;
32
+ ppid: number;
33
+ /** Raw `ps` ELAPSED column. */
34
+ elapsed: string;
35
+ /** Parsed elapsed seconds since the process started. */
36
+ elapsedSeconds: number;
37
+ command: string;
38
+ }
39
+ /**
40
+ * Scans `ps -axo pid=,ppid=,etime=,command=` output for surviving harness
41
+ * helpers. Pure — fixture-testable without spawning anything.
42
+ *
43
+ * @param psOutput - Raw `ps` output
44
+ * @param objDir - Absolute objdir of this project, when known; widens the
45
+ * provenance test beyond the generic `obj-…` path segment
46
+ * @param selfPid - This process's pid, excluded so FireForge cannot report
47
+ * itself
48
+ */
49
+ export declare function findOrphanedHarnessProcesses(psOutput: string, objDir?: string, selfPid?: number): OrphanedHarnessProcess[];
50
+ /**
51
+ * Renders the census. Names every process with its elapsed time, because
52
+ * "a leftover harness process exists" and "a leftover harness process has
53
+ * been burning a core for 58 minutes" are different findings.
54
+ */
55
+ export declare function formatOrphanReport(orphans: readonly OrphanedHarnessProcess[]): string;
56
+ /**
57
+ * Preflight census of surviving harness helpers, run before a test
58
+ * dispatch.
59
+ *
60
+ * Best-effort by construction: a host without a usable `ps` (Windows, a
61
+ * locked-down container) logs at verbose and runs exactly as before. It
62
+ * never refuses a run — unlike the server-port preflight, a survivor here
63
+ * degrades performance rather than making the run dispatch against the
64
+ * wrong server, so the correct response is to say so loudly, not to stop.
65
+ *
66
+ * @param objDir - Absolute objdir of this project, when known
67
+ * @param options - `reap` terminates each recognized survivor (SIGTERM,
68
+ * then SIGKILL for anything that stays)
69
+ * @returns The census, empty when nothing was found or nothing could be probed
70
+ */
71
+ export declare function reportOrphanedHarnessProcesses(objDir: string | undefined, options?: {
72
+ reap?: boolean;
73
+ }): Promise<OrphanedHarnessProcess[]>;
@@ -0,0 +1,186 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Preflight census of ORPHANED test-harness helper processes.
4
+ *
5
+ * Stopping a long mochitest run mid-flight does not necessarily take its
6
+ * helpers with it. A downstream incident left `xpcshell` (the harness
7
+ * httpd, pegged at 100% CPU), `pywebsocket`, `ssltunnel` and `moz-http2`
8
+ * alive for an hour. Every subsequent run crawled — a three-second suite
9
+ * took six minutes of wall clock and audio-start waits timed out — and
10
+ * nothing in FireForge's output connected the slowness to the survivors.
11
+ * The hour was spent mis-attributing it to the change under test.
12
+ *
13
+ * Visibility alone would have collapsed that hour, which is why the census
14
+ * runs by default and the kill does not. Two rules keep it honest:
15
+ *
16
+ * - The census runs at PREFLIGHT, before this run spawns anything, so
17
+ * every matching process is by construction a survivor of an earlier
18
+ * run. No age heuristic is needed, and none is used.
19
+ * - A match must be anchored to a Firefox OBJDIR. `xpcshell` and
20
+ * `server.js` are far too generic on their own; the same rule
21
+ * `mochitest-server-port.ts` applies to the httpd holder applies here,
22
+ * for the same reason — FireForge has no business reporting, let alone
23
+ * terminating, a process it cannot attribute to the harness.
24
+ *
25
+ * `--reap-orphans` opts into termination. Without it the census is
26
+ * report-only, exactly like the `Orphaned harness workers` doctor check
27
+ * (which covers a DIFFERENT shape: reparented Python multiprocessing
28
+ * workers, matched on PPID 1 and accumulated CPU time).
29
+ */
30
+ import { toError } from '../utils/errors.js';
31
+ import { info, verbose, warn } from '../utils/logger.js';
32
+ import { exec } from '../utils/process.js';
33
+ import { parsePsDuration } from '../utils/ps-duration.js';
34
+ /**
35
+ * Harness helper executables/scripts. Every one of these is started by the
36
+ * mochitest/xpcshell harness and is expected to die with it; none of them
37
+ * is a thing a developer runs by hand.
38
+ */
39
+ const HARNESS_HELPER_PATTERN = /\b(?:xpcshell|pywebsocket\w*|websocket_server\.py|ssltunnel|moz-http2|http2_server|httpd\.js|server\.js|runtests\.py|runxpcshelltests\.py)\b/;
40
+ /** Objdir provenance: an explicit objdir path, a `obj-…` path segment, or `_tests/`. */
41
+ function isObjdirAnchored(command, objDir) {
42
+ if (objDir !== undefined && objDir.length > 0 && command.includes(objDir))
43
+ return true;
44
+ if (/[/\\]obj-[^/\\\s]*[/\\]/.test(command))
45
+ return true;
46
+ return /[/\\]_tests[/\\]/.test(command);
47
+ }
48
+ /**
49
+ * Scans `ps -axo pid=,ppid=,etime=,command=` output for surviving harness
50
+ * helpers. Pure — fixture-testable without spawning anything.
51
+ *
52
+ * @param psOutput - Raw `ps` output
53
+ * @param objDir - Absolute objdir of this project, when known; widens the
54
+ * provenance test beyond the generic `obj-…` path segment
55
+ * @param selfPid - This process's pid, excluded so FireForge cannot report
56
+ * itself
57
+ */
58
+ export function findOrphanedHarnessProcesses(psOutput, objDir, selfPid = process.pid) {
59
+ const found = [];
60
+ for (const line of psOutput.split('\n')) {
61
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/.exec(line);
62
+ if (!match)
63
+ continue;
64
+ const pid = Number(match[1]);
65
+ const ppid = Number(match[2]);
66
+ const elapsed = match[3] ?? '';
67
+ const command = (match[4] ?? '').trim();
68
+ if (pid === selfPid || ppid === selfPid)
69
+ continue;
70
+ if (!HARNESS_HELPER_PATTERN.test(command))
71
+ continue;
72
+ if (!isObjdirAnchored(command, objDir))
73
+ continue;
74
+ const elapsedSeconds = parsePsDuration(elapsed);
75
+ found.push({
76
+ pid,
77
+ ppid,
78
+ elapsed,
79
+ elapsedSeconds: Number.isNaN(elapsedSeconds) ? 0 : elapsedSeconds,
80
+ command,
81
+ });
82
+ }
83
+ return found;
84
+ }
85
+ /** Grace period between SIGTERM and SIGKILL when reaping. */
86
+ const REAP_GRACE_MS = 500;
87
+ /** Longest command excerpt carried into the report line. */
88
+ const COMMAND_EXCERPT_LIMIT = 160;
89
+ /**
90
+ * Renders the census. Names every process with its elapsed time, because
91
+ * "a leftover harness process exists" and "a leftover harness process has
92
+ * been burning a core for 58 minutes" are different findings.
93
+ */
94
+ export function formatOrphanReport(orphans) {
95
+ const rows = orphans
96
+ .map((p) => ` PID ${String(p.pid)} (up ${p.elapsed}): ${p.command.slice(0, COMMAND_EXCERPT_LIMIT)}`)
97
+ .join('\n');
98
+ const pids = orphans.map((p) => String(p.pid)).join(' ');
99
+ return (`${String(orphans.length)} harness helper process(es) from an EARLIER run are still alive ` +
100
+ `(this preflight runs before the current run spawns anything, so none of these belong to ` +
101
+ `it):\n${rows}\n` +
102
+ `Survivors like these slow every later run without appearing anywhere in its output — a ` +
103
+ `three-second suite taking minutes of wall clock is the usual symptom. Terminate them with ` +
104
+ `"kill ${pids}", or re-run with --reap-orphans to have FireForge do it.`);
105
+ }
106
+ /** Thin exec wrapper for the process listing (tests mock `exec` instead). */
107
+ async function listSystemProcesses() {
108
+ const result = await exec('ps', ['-axo', 'pid=,ppid=,etime=,command='], { timeout: 10000 });
109
+ if (result.exitCode !== 0) {
110
+ throw new Error(`ps exited ${String(result.exitCode)}`);
111
+ }
112
+ return result.stdout;
113
+ }
114
+ /**
115
+ * Preflight census of surviving harness helpers, run before a test
116
+ * dispatch.
117
+ *
118
+ * Best-effort by construction: a host without a usable `ps` (Windows, a
119
+ * locked-down container) logs at verbose and runs exactly as before. It
120
+ * never refuses a run — unlike the server-port preflight, a survivor here
121
+ * degrades performance rather than making the run dispatch against the
122
+ * wrong server, so the correct response is to say so loudly, not to stop.
123
+ *
124
+ * @param objDir - Absolute objdir of this project, when known
125
+ * @param options - `reap` terminates each recognized survivor (SIGTERM,
126
+ * then SIGKILL for anything that stays)
127
+ * @returns The census, empty when nothing was found or nothing could be probed
128
+ */
129
+ export async function reportOrphanedHarnessProcesses(objDir, options = {}) {
130
+ if (process.platform === 'win32')
131
+ return [];
132
+ let psOutput;
133
+ try {
134
+ psOutput = await listSystemProcesses();
135
+ }
136
+ catch (error) {
137
+ verbose(`Orphan preflight: could not scan processes (${toError(error).message}); skipping.`);
138
+ return [];
139
+ }
140
+ const orphans = findOrphanedHarnessProcesses(psOutput, objDir);
141
+ if (orphans.length === 0) {
142
+ verbose('Orphan preflight: no surviving harness helper processes.');
143
+ return [];
144
+ }
145
+ warn(formatOrphanReport(orphans));
146
+ if (options.reap === true) {
147
+ await reapOrphanedHarnessProcesses(orphans);
148
+ }
149
+ return orphans;
150
+ }
151
+ /**
152
+ * Terminates each census entry: SIGTERM first, then SIGKILL for anything
153
+ * still alive. Failures are reported, never thrown — a process that exited
154
+ * between the census and the signal is the common case, not an error.
155
+ */
156
+ async function reapOrphanedHarnessProcesses(orphans) {
157
+ for (const orphan of orphans) {
158
+ try {
159
+ process.kill(orphan.pid, 'SIGTERM');
160
+ }
161
+ catch (error) {
162
+ verbose(`--reap-orphans: SIGTERM to ${String(orphan.pid)} failed (${toError(error).message}).`);
163
+ continue;
164
+ }
165
+ // Grace period before escalating: the httpd shape in the field
166
+ // incident ignored SIGTERM while spinning, but an ordinary helper exits
167
+ // promptly and must not be SIGKILLed for being slow by a millisecond.
168
+ await new Promise((resolve) => setTimeout(resolve, REAP_GRACE_MS));
169
+ let alive = true;
170
+ try {
171
+ process.kill(orphan.pid, 0);
172
+ }
173
+ catch {
174
+ alive = false;
175
+ }
176
+ if (alive) {
177
+ try {
178
+ process.kill(orphan.pid, 'SIGKILL');
179
+ }
180
+ catch (error) {
181
+ verbose(`--reap-orphans: SIGKILL to ${String(orphan.pid)} failed (${toError(error).message}).`);
182
+ }
183
+ }
184
+ info(`--reap-orphans: terminated PID ${String(orphan.pid)}.`);
185
+ }
186
+ }
@@ -69,7 +69,9 @@ export declare function hasUpstreamMplBlockHeader(content: string): boolean;
69
69
  export declare function hasAnyLicenseHeaderAnyStyle(content: string): boolean;
70
70
  /**
71
71
  * Returns true if the first few lines of `content` contain a recognized
72
- * upstream license identifier string.
72
+ * upstream license identifier string — Mozilla's MPL boilerplate, an SPDX
73
+ * tag, the common permissive/copyleft license names, or the CC0
74
+ * public-domain dedication upstream test files carry.
73
75
  *
74
76
  * @param content - File content to check
75
77
  * @param maxLines - Number of leading lines to inspect (default 10)
@@ -77,10 +79,12 @@ export declare function hasAnyLicenseHeaderAnyStyle(content: string): boolean;
77
79
  export declare function containsUpstreamLicenseText(content: string, maxLines?: number): boolean;
78
80
  /**
79
81
  * Returns true when the head of `content` carries a recognized THIRD-PARTY
80
- * permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0), in any
81
- * comment style. Used by `export` to treat such files as vendored: offering
82
- * to prepend the project's own license header onto a byte-identity-pinned
83
- * upstream bundle would mislicense third-party code.
82
+ * permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0) or the
83
+ * CC0 public-domain dedication, in any comment style. Used by `export` to
84
+ * treat such files as vendored: offering to prepend the project's own
85
+ * license header onto a byte-identity-pinned upstream bundle — or onto a
86
+ * test file Mozilla dedicated to the public domain — would mislicense code
87
+ * the project does not own.
84
88
  *
85
89
  * @param content - File content to check
86
90
  * @param maxLines - Number of leading lines to inspect (default 30 — full
@@ -227,9 +227,31 @@ function normalizeLicenseHeadForScan(head) {
227
227
  s = s.replace(/\n[ \t]*\*[ \t]*/g, ' ');
228
228
  return s.replace(/\s+/g, ' ').trim();
229
229
  }
230
+ /**
231
+ * Recognizes the CC0 public-domain dedication Mozilla puts on most of its
232
+ * test files, in every spelling the tree carries:
233
+ *
234
+ * - the prose (`Any copyright is dedicated to the Public Domain.`), which
235
+ * the block, `//` and `#` comment forms all reduce to after
236
+ * {@link normalizeLicenseHeadForScan};
237
+ * - the dedication URL (`creativecommons.org/publicdomain/zero/1.0/`, with
238
+ * either scheme);
239
+ * - the SPDX identifier `CC0-1.0`.
240
+ *
241
+ * Deliberately narrow: a bare "public domain" mention, a different Creative
242
+ * Commons license (`licenses/by/4.0/`, `CC-BY-4.0`) or a made-up
243
+ * `publicdomain/zero/2.0/` is NOT a CC0 dedication and must not be
244
+ * accepted as one. `scanText` must already be normalized (and may be
245
+ * lowercased — the match is case-insensitive).
246
+ */
247
+ function hasCc0Dedication(scanText) {
248
+ return /dedicated to the public domain|creativecommons\.org\/publicdomain\/zero\/1\.0|spdx-license-identifier:\s*cc0-1\.0\b/i.test(scanText);
249
+ }
230
250
  /**
231
251
  * Returns true if the first few lines of `content` contain a recognized
232
- * upstream license identifier string.
252
+ * upstream license identifier string — Mozilla's MPL boilerplate, an SPDX
253
+ * tag, the common permissive/copyleft license names, or the CC0
254
+ * public-domain dedication upstream test files carry.
233
255
  *
234
256
  * @param content - File content to check
235
257
  * @param maxLines - Number of leading lines to inspect (default 10)
@@ -244,14 +266,16 @@ export function containsUpstreamLicenseText(content, maxLines = 10) {
244
266
  'MIT License',
245
267
  'GNU General Public License',
246
268
  ];
247
- return markers.some((marker) => scanText.includes(marker));
269
+ return markers.some((marker) => scanText.includes(marker)) || hasCc0Dedication(scanText);
248
270
  }
249
271
  /**
250
272
  * Returns true when the head of `content` carries a recognized THIRD-PARTY
251
- * permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0), in any
252
- * comment style. Used by `export` to treat such files as vendored: offering
253
- * to prepend the project's own license header onto a byte-identity-pinned
254
- * upstream bundle would mislicense third-party code.
273
+ * permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0) or the
274
+ * CC0 public-domain dedication, in any comment style. Used by `export` to
275
+ * treat such files as vendored: offering to prepend the project's own
276
+ * license header onto a byte-identity-pinned upstream bundle — or onto a
277
+ * test file Mozilla dedicated to the public domain — would mislicense code
278
+ * the project does not own.
255
279
  *
256
280
  * @param content - File content to check
257
281
  * @param maxLines - Number of leading lines to inspect (default 30 — full
@@ -271,7 +295,9 @@ export function hasThirdPartyPermissiveBanner(content, maxLines = 30) {
271
295
  ];
272
296
  if (markers.some((marker) => scanText.includes(marker)))
273
297
  return true;
274
- return /spdx-license-identifier:\s*(mit|isc|bsd-2-clause|bsd-3-clause|apache-2\.0)\b/.test(scanText);
298
+ if (hasCc0Dedication(scanText))
299
+ return true;
300
+ return /spdx-license-identifier:\s*(mit|isc|bsd-2-clause|bsd-3-clause|apache-2\.0|cc0-1\.0)\b/.test(scanText);
275
301
  }
276
302
  /**
277
303
  * Prepends the license header to a file on disk if it is not already present.
@@ -25,6 +25,20 @@
25
25
  * echoed verbatim, always.
26
26
  * - The hold buffer is bounded; on overflow the block is flushed verbatim
27
27
  * and the filter returns to pass-through, so output is never lost.
28
+ *
29
+ * The COLLAPSE is test-only, deliberately, and not an oversight — the two
30
+ * reasons are worth stating because from outside it reads like one:
31
+ * 1. Recognition requires a previously-seen SUITE_END shutdown marker, and
32
+ * `mach build` never prints one. Wiring the option into the build
33
+ * dispatch would therefore be a silent no-op.
34
+ * 2. Relaxing that gate for builds would be wrong on its own terms. The
35
+ * marker is what separates teardown noise from a traceback raised while
36
+ * work was still happening; a build has no equivalent boundary, so
37
+ * FireForge cannot tell the known-cosmetic case from a real build-time
38
+ * failure and must not withhold the block.
39
+ * The build path instead RECOGNIZES the signature and says so beside the
40
+ * verbatim traceback — see {@link KNOWN_TEARDOWN_NOISE_BUILD_NOTE} and
41
+ * `runProtectedMachBuild`.
28
42
  */
29
43
  /**
30
44
  * True when CAPTURED output carries the documented mozsystemmonitor
@@ -76,6 +90,13 @@ export interface TeardownNoiseContext {
76
90
  }
77
91
  /** Fresh per-run context — hand the same instance to both stream filters. */
78
92
  export declare function createTeardownNoiseContext(): TeardownNoiseContext;
93
+ /**
94
+ * Build-phase note for the SAME recognized signature. The build path does
95
+ * not COLLAPSE the traceback (see the module doc's "test-only" note) — it
96
+ * prints verbatim and this line is added beside it, so one signature reads
97
+ * the same in both phases without a build losing output it may need.
98
+ */
99
+ export declare const KNOWN_TEARDOWN_NOISE_BUILD_NOTE: string;
79
100
  /** One line replaces the whole recognized traceback block in the echo. */
80
101
  export declare const KNOWN_TEARDOWN_NOISE_ANNOTATION: string;
81
102
  /** Chunk-safe, line-buffered echo filter. See module doc. */
@@ -26,6 +26,20 @@
26
26
  * echoed verbatim, always.
27
27
  * - The hold buffer is bounded; on overflow the block is flushed verbatim
28
28
  * and the filter returns to pass-through, so output is never lost.
29
+ *
30
+ * The COLLAPSE is test-only, deliberately, and not an oversight — the two
31
+ * reasons are worth stating because from outside it reads like one:
32
+ * 1. Recognition requires a previously-seen SUITE_END shutdown marker, and
33
+ * `mach build` never prints one. Wiring the option into the build
34
+ * dispatch would therefore be a silent no-op.
35
+ * 2. Relaxing that gate for builds would be wrong on its own terms. The
36
+ * marker is what separates teardown noise from a traceback raised while
37
+ * work was still happening; a build has no equivalent boundary, so
38
+ * FireForge cannot tell the known-cosmetic case from a real build-time
39
+ * failure and must not withhold the block.
40
+ * The build path instead RECOGNIZES the signature and says so beside the
41
+ * verbatim traceback — see {@link KNOWN_TEARDOWN_NOISE_BUILD_NOTE} and
42
+ * `runProtectedMachBuild`.
29
43
  */
30
44
  const TRACEBACK_HEADER_PATTERN = /^Traceback \(most recent call last\)/;
31
45
  const CHAINED_EXCEPTION_CONNECTOR_PATTERN = /^(?:During handling of the above exception, another exception occurred:|The above exception was the direct cause of the following exception:)\s*$/;
@@ -93,9 +107,18 @@ export function isKnownTeardownNoiseLine(line) {
93
107
  export function createTeardownNoiseContext() {
94
108
  return { shutdownSeen: false };
95
109
  }
110
+ /**
111
+ * Build-phase note for the SAME recognized signature. The build path does
112
+ * not COLLAPSE the traceback (see the module doc's "test-only" note) — it
113
+ * prints verbatim and this line is added beside it, so one signature reads
114
+ * the same in both phases without a build losing output it may need.
115
+ */
116
+ export const KNOWN_TEARDOWN_NOISE_BUILD_NOTE = '[FireForge] The mozsystemmonitor traceback above is the known upstream ' +
117
+ 'SystemResourceMonitor AttributeError — cosmetic, and not a build failure. It is left ' +
118
+ 'verbatim here (unlike a test run, which collapses it); see docs/testing.md.';
96
119
  /** One line replaces the whole recognized traceback block in the echo. */
97
120
  export const KNOWN_TEARDOWN_NOISE_ANNOTATION = '[FireForge] Known upstream mozsystemmonitor teardown noise (SystemResourceMonitor ' +
98
- 'AttributeError at harness shutdown) — not a test failure. See FireForge docs.\n';
121
+ 'AttributeError at harness shutdown) — not a test failure. See docs/testing.md.\n';
99
122
  /** Bounds on the held traceback block before flushing verbatim. */
100
123
  const HOLD_LINE_LIMIT = 100;
101
124
  const HOLD_BYTE_LIMIT = 16 * 1024;
@@ -23,6 +23,13 @@ export interface MachOptions {
23
23
  * stdout/stderr stay raw — the harness classifier depends on the raw
24
24
  * traceback. Unrecognized tracebacks always echo verbatim. Opted into by
25
25
  * the test dispatchers only.
26
+ *
27
+ * Consumed by {@link runMachCapture} ALONE. `runMachInheritCapture` accepts
28
+ * it in the type and ignores it — the inherit path has no per-chunk hook,
29
+ * only a mirror stream that feeds the terminal and the run log the same
30
+ * string, so filtering there would strip the traceback from the raw log
31
+ * too. The build path uses the recognition NOTE instead; see
32
+ * `mach-known-noise-filter.ts`.
26
33
  */
27
34
  annotateKnownTeardownNoise?: boolean;
28
35
  /**
@@ -2,12 +2,12 @@
2
2
  import { basename, join } from 'node:path';
3
3
  import { MachNotFoundError } from '../errors/build.js';
4
4
  import { pathExists } from '../utils/fs.js';
5
- import { warn } from '../utils/logger.js';
5
+ import { info, warn } from '../utils/logger.js';
6
6
  import { exec, execInherit, execInheritCapture, execSmokeRun, execStream, } from '../utils/process.js';
7
7
  import { createSiblingLockPath, withFileLock } from './file-lock.js';
8
8
  import { ensureFirefoxIgnorefileCompatibility } from './firefox-ignorefile.js';
9
9
  import { explainMachError } from './mach-error-hints.js';
10
- import { createKnownTeardownNoiseFilter, createTeardownNoiseContext, } from './mach-known-noise-filter.js';
10
+ import { createKnownTeardownNoiseFilter, createTeardownNoiseContext, hasKnownTeardownNoise, KNOWN_TEARDOWN_NOISE_BUILD_NOTE, } from './mach-known-noise-filter.js';
11
11
  import { getPython } from './mach-python.js';
12
12
  import { installMachResourceGuard } from './mach-resource-shim.js';
13
13
  import { teeToRunLog, writeToActiveRunLog } from './run-log.js';
@@ -193,6 +193,20 @@ function surfaceMachErrorHints(result) {
193
193
  * the same crash family the same number of times.
194
194
  */
195
195
  const DEFAULT_BUILD_CRASH_RETRIES = 2;
196
+ /**
197
+ * Names the known mozsystemmonitor teardown traceback when a BUILD carried
198
+ * it. The test path collapses this signature in the echo; a build cannot
199
+ * (see `mach-known-noise-filter.ts`), so it reached operators as an
200
+ * unexplained traceback in a build log — a recognized, documented upstream
201
+ * defect wearing the appearance of a new one. The output is left verbatim
202
+ * and this one line is added beside it, so the same signature reads the same
203
+ * in both phases.
204
+ */
205
+ function noteKnownTeardownNoiseInBuild(result) {
206
+ if (!hasKnownTeardownNoise(`${result.stdout}\n${result.stderr}`))
207
+ return;
208
+ info(KNOWN_TEARDOWN_NOISE_BUILD_NOTE);
209
+ }
196
210
  /**
197
211
  * The single protected path every FireForge mach build dispatch routes
198
212
  * through — `build`, `build --ui`, and the pre-test `--build` step all use
@@ -222,6 +236,7 @@ export async function runProtectedMachBuild(kind, engineDir, options = {}) {
222
236
  const { env } = await installMachResourceGuard(engineDir);
223
237
  const result = await runMachInheritCapture(args, engineDir, { env });
224
238
  if (result.exitCode === 0) {
239
+ noteKnownTeardownNoiseInBuild(result);
225
240
  return { ...result, attempts: attempt };
226
241
  }
227
242
  const signature = detectHarnessCrashSignature(`${result.stdout}\n${result.stderr}`);
@@ -233,6 +248,7 @@ export async function runProtectedMachBuild(kind, engineDir, options = {}) {
233
248
  continue;
234
249
  }
235
250
  surfaceMachErrorHints(result);
251
+ noteKnownTeardownNoiseInBuild(result);
236
252
  return { ...result, attempts: attempt, ...(signature ? { crashSignature: signature } : {}) };
237
253
  }
238
254
  }