@hominis/fireforge 0.46.2 → 0.47.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.
- package/CHANGELOG.md +28 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/config.js +1 -2
- package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +59 -0
- package/dist/src/commands/furnace/deploy-fragment-includers.js +120 -0
- package/dist/src/commands/furnace/deploy.js +36 -3
- package/dist/src/commands/furnace/validation-output.d.ts +5 -0
- package/dist/src/commands/furnace/validation-output.js +35 -9
- package/dist/src/commands/patch/delete-gap-notice.d.ts +24 -0
- package/dist/src/commands/patch/delete-gap-notice.js +46 -0
- package/dist/src/commands/patch/delete.js +11 -0
- package/dist/src/commands/re-export-lint.d.ts +26 -1
- package/dist/src/commands/re-export-lint.js +60 -20
- package/dist/src/commands/re-export-scan.d.ts +8 -0
- package/dist/src/commands/re-export-scan.js +12 -2
- package/dist/src/commands/re-export-single.js +24 -5
- package/dist/src/commands/run.js +42 -17
- package/dist/src/commands/test-harness-env.d.ts +24 -0
- package/dist/src/commands/test-harness-env.js +61 -0
- package/dist/src/commands/test-register.js +6 -2
- package/dist/src/commands/test-verdict.d.ts +3 -1
- package/dist/src/commands/test-verdict.js +17 -2
- package/dist/src/commands/test.js +9 -16
- package/dist/src/core/build-jar-escalation.d.ts +1 -4
- package/dist/src/core/build-jar-escalation.js +1 -4
- package/dist/src/core/build-prepare.js +72 -55
- package/dist/src/core/firefox-cache.js +1 -2
- package/dist/src/core/furnace-apply-output.d.ts +10 -0
- package/dist/src/core/furnace-apply-output.js +19 -4
- package/dist/src/core/furnace-css-fragments.d.ts +7 -0
- package/dist/src/core/furnace-css-fragments.js +12 -3
- package/dist/src/core/furnace-fragment-includers.d.ts +56 -0
- package/dist/src/core/furnace-fragment-includers.js +122 -0
- package/dist/src/core/git-diff-new-file.js +1 -2
- package/dist/src/core/git-file-ops.js +1 -2
- package/dist/src/core/git.js +1 -2
- package/dist/src/core/mach-build-artifacts.js +1 -2
- package/dist/src/core/mochitest-server-port.d.ts +43 -4
- package/dist/src/core/mochitest-server-port.js +64 -11
- package/dist/src/core/patch-lint-imports.js +4 -4
- package/dist/src/core/smoke-patterns.d.ts +24 -0
- package/dist/src/core/smoke-patterns.js +50 -5
- package/dist/src/types/commands/options.d.ts +6 -0
- package/docs/configuration.md +8 -0
- package/docs/exit-codes.md +19 -19
- package/docs/machine-output.md +22 -10
- package/docs/testing.md +13 -1
- package/package.json +5 -5
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* port-generic, rather than growing a second probe.
|
|
24
24
|
*/
|
|
25
25
|
import { GeneralError } from '../errors/base.js';
|
|
26
|
+
import { isExplicitAbsolutePath, isPathInsideRoot } from '../utils/paths.js';
|
|
26
27
|
import { exec } from '../utils/process.js';
|
|
27
28
|
import { probeMarionettePort } from './marionette-port.js';
|
|
28
29
|
/** Default port the mochitest harness binds its httpd to. */
|
|
@@ -43,6 +44,40 @@ export function isMochitestServerHolder(holder) {
|
|
|
43
44
|
return false;
|
|
44
45
|
return /\bxpcshell\b/.test(line) || /\/obj-[^/\s]*\//.test(line) || /\b_tests\b/.test(line);
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Classifies the holder against this project's engine directory. Every
|
|
49
|
+
* absolute path on the command line is tested with {@link isPathInsideRoot}
|
|
50
|
+
* (a token test, not a substring test, so `/a/hominis/engine` does not claim
|
|
51
|
+
* a holder living under `/a/hominis-2/engine`). On POSIX the command line is
|
|
52
|
+
* the full `ps -o args=` output, so the objdir path is always present for a
|
|
53
|
+
* harness httpd; a holder whose paths all lie elsewhere belongs to another
|
|
54
|
+
* checkout.
|
|
55
|
+
*
|
|
56
|
+
* @param holder - The listener found on the port
|
|
57
|
+
* @param engineDir - Absolute engine directory of THIS project
|
|
58
|
+
*/
|
|
59
|
+
export function classifyMochitestServerHolder(holder, engineDir) {
|
|
60
|
+
if (!isMochitestServerHolder(holder))
|
|
61
|
+
return 'unrecognized';
|
|
62
|
+
const absoluteTokens = holder.commandLine.split(/\s+/).filter(isExplicitAbsolutePath);
|
|
63
|
+
return absoluteTokens.some((token) => isPathInsideRoot(engineDir, token))
|
|
64
|
+
? 'this-checkout'
|
|
65
|
+
: 'foreign-checkout';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The objdir a foreign holder is serving from, for the refusal text: the
|
|
69
|
+
* first absolute command-line token with an `obj-…` segment, cut at that
|
|
70
|
+
* segment. Falls back to the first absolute token, then to a placeholder.
|
|
71
|
+
*/
|
|
72
|
+
export function describeHolderObjdir(commandLine) {
|
|
73
|
+
const tokens = commandLine.split(/\s+/).filter(isExplicitAbsolutePath);
|
|
74
|
+
for (const token of tokens) {
|
|
75
|
+
const match = /^(.*?[/\\]obj-[^/\\]*)(?:[/\\]|$)/.exec(token);
|
|
76
|
+
if (match?.[1])
|
|
77
|
+
return match[1];
|
|
78
|
+
}
|
|
79
|
+
return tokens[0] ?? '(no path visible on the command line)';
|
|
80
|
+
}
|
|
46
81
|
/**
|
|
47
82
|
* Refuses a mochitest dispatch when the harness's server port is held. It
|
|
48
83
|
* terminates the holder first only for a recognized stale harness httpd,
|
|
@@ -57,16 +92,24 @@ export function isMochitestServerHolder(holder) {
|
|
|
57
92
|
* Best effort: {@link probeMarionettePort} reports an
|
|
58
93
|
* unprobeable port as free, so a host without `lsof` runs exactly as before.
|
|
59
94
|
*
|
|
95
|
+
* A recognized httpd from ANOTHER checkout is a refusal and never a kill
|
|
96
|
+
* either, flag or no flag: it is a peer's live suite, not our debris. The
|
|
97
|
+
* browser-side probe draws the same line for a bare launch it cannot
|
|
98
|
+
* attribute; this one can attribute, because the objdir is on the command
|
|
99
|
+
* line, so it names the other checkout instead of offering `kill -9`.
|
|
100
|
+
*
|
|
60
101
|
* @param port - Port to probe (default {@link DEFAULT_MOCHITEST_SERVER_PORT})
|
|
61
|
-
* @param options - `
|
|
102
|
+
* @param options - `engineDir` is this project's absolute engine directory,
|
|
103
|
+
* the ownership boundary; `killStaleServer` opts into terminating a holder
|
|
104
|
+
* recognized as THIS checkout's debris
|
|
62
105
|
*/
|
|
63
|
-
export async function ensureMochitestServerPortAvailable(port = DEFAULT_MOCHITEST_SERVER_PORT, options
|
|
106
|
+
export async function ensureMochitestServerPortAvailable(port = DEFAULT_MOCHITEST_SERVER_PORT, options) {
|
|
64
107
|
const probe = await probeMarionettePort(port);
|
|
65
108
|
if (!probe.inUse || !probe.holder)
|
|
66
109
|
return;
|
|
67
110
|
const holder = probe.holder;
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
111
|
+
const holderClass = classifyMochitestServerHolder(holder, options.engineDir);
|
|
112
|
+
if (holderClass === 'this-checkout' && options.killStaleServer === true) {
|
|
70
113
|
try {
|
|
71
114
|
if (process.platform === 'win32') {
|
|
72
115
|
await exec('powershell.exe', [
|
|
@@ -87,16 +130,16 @@ export async function ensureMochitestServerPortAvailable(port = DEFAULT_MOCHITES
|
|
|
87
130
|
// pretending the port is now free.
|
|
88
131
|
}
|
|
89
132
|
}
|
|
90
|
-
throw new GeneralError(describeMochitestServerRefusal(port, holder,
|
|
133
|
+
throw new GeneralError(describeMochitestServerRefusal(port, holder, holderClass));
|
|
91
134
|
}
|
|
92
135
|
/**
|
|
93
136
|
* Builds the operator-facing refusal. Exported for direct unit testing.
|
|
94
137
|
*
|
|
95
138
|
* @param port - The probed port
|
|
96
139
|
* @param holder - The listener found on it
|
|
97
|
-
* @param
|
|
140
|
+
* @param holderClass - Whose harness `server.js` it is, if it is one at all
|
|
98
141
|
*/
|
|
99
|
-
export function describeMochitestServerRefusal(port, holder,
|
|
142
|
+
export function describeMochitestServerRefusal(port, holder, holderClass) {
|
|
100
143
|
const killHint = process.platform === 'win32'
|
|
101
144
|
? `Stop-Process -Id ${holder.pid} -Force`
|
|
102
145
|
: `kill -9 ${holder.pid}`;
|
|
@@ -106,11 +149,21 @@ export function describeMochitestServerRefusal(port, holder, recognized) {
|
|
|
106
149
|
" A browser launched now would connect to THAT server, which cannot serve this run's " +
|
|
107
150
|
'manifest — the run would stall before TEST_START and die on the no-output timeout with ' +
|
|
108
151
|
'"Ran 0 checks".\n';
|
|
109
|
-
if (
|
|
152
|
+
if (holderClass === 'this-checkout') {
|
|
153
|
+
return (evidence +
|
|
154
|
+
" This is the mochitest harness's own server.js from THIS checkout's objdir, so it is " +
|
|
155
|
+
'debris from an interrupted run. Retry with "--kill-stale-marionette" to have FireForge ' +
|
|
156
|
+
`stop it, or stop it yourself with "${killHint}" (a wedged httpd can ignore SIGTERM).`);
|
|
157
|
+
}
|
|
158
|
+
if (holderClass === 'foreign-checkout') {
|
|
159
|
+
// No kill hint on this branch, on purpose: the PID is a peer's live
|
|
160
|
+
// suite, and "kill -9 <pid>" is exactly the instruction that would have
|
|
161
|
+
// destroyed it.
|
|
110
162
|
return (evidence +
|
|
111
|
-
|
|
112
|
-
'
|
|
113
|
-
|
|
163
|
+
` This is a mochitest httpd from ANOTHER checkout: ${describeHolderObjdir(holder.commandLine)}. ` +
|
|
164
|
+
'It is serving a live run there, so FireForge will not stop it and "--kill-stale-marionette" ' +
|
|
165
|
+
'will not either. Wait for that run to finish or free the port yourself, then retry. ' +
|
|
166
|
+
`Diagnose with "lsof -nP -iTCP:${port}".`);
|
|
114
167
|
}
|
|
115
168
|
return (evidence +
|
|
116
169
|
" This is NOT the mochitest harness's server.js, so FireForge will not offer to kill it. " +
|
|
@@ -54,14 +54,14 @@ export function hasRelativeImport(content) {
|
|
|
54
54
|
try {
|
|
55
55
|
return astHasRelativeImport(content, 'module');
|
|
56
56
|
}
|
|
57
|
-
catch
|
|
58
|
-
|
|
57
|
+
catch {
|
|
58
|
+
// Not parseable as an ES module; try classic-script mode next.
|
|
59
59
|
}
|
|
60
60
|
try {
|
|
61
61
|
return astHasRelativeImport(content, 'script');
|
|
62
62
|
}
|
|
63
|
-
catch
|
|
64
|
-
|
|
63
|
+
catch {
|
|
64
|
+
// Not parseable at all; fall back to stripped-text matching below.
|
|
65
65
|
}
|
|
66
66
|
return RELATIVE_IMPORT_FALLBACK_PATTERN.test(stripJsComments(content));
|
|
67
67
|
}
|
|
@@ -29,6 +29,12 @@ export interface CompiledAllowlistEntry {
|
|
|
29
29
|
source: string;
|
|
30
30
|
/** Human-readable origin: `allow.txt:12` or `--console-allow #2`. */
|
|
31
31
|
origin: string;
|
|
32
|
+
/**
|
|
33
|
+
* Ceiling from a `# max-hits: N` directive above the entry: how often the
|
|
34
|
+
* suppressed cause can physically fire in one boot. More hits than this
|
|
35
|
+
* is a flood wearing an allowlisted shape, and fails the run.
|
|
36
|
+
*/
|
|
37
|
+
maxHits?: number;
|
|
32
38
|
}
|
|
33
39
|
/**
|
|
34
40
|
* Returns the index of the FIRST entry in `allow` matching `line`, or -1.
|
|
@@ -43,8 +49,26 @@ export declare function matchAllowlist(line: string, allow: readonly CompiledAll
|
|
|
43
49
|
* compiled as a RegExp with its `<file>:<line>` origin retained. A bad
|
|
44
50
|
* pattern throws immediately: better to fail fast at CLI parse time than
|
|
45
51
|
* to silently let a typo match nothing.
|
|
52
|
+
*
|
|
53
|
+
* A `# max-hits: N` comment sets the ceiling of the entry that follows it
|
|
54
|
+
* (other comment lines may sit between). A blank line or end of file with
|
|
55
|
+
* no entry after the directive is an error rather than a silently dropped
|
|
56
|
+
* cap, since a cap the operator wrote and the run ignored is the exact
|
|
57
|
+
* shape of failure this directive exists to prevent.
|
|
46
58
|
*/
|
|
47
59
|
export declare function compileAllowlistFromFile(body: string, sourcePath: string): CompiledAllowlistEntry[];
|
|
60
|
+
/** An allowlist entry that matched more lines than its `# max-hits` ceiling. */
|
|
61
|
+
export interface OverCapAllowlistEntry {
|
|
62
|
+
entry: CompiledAllowlistEntry;
|
|
63
|
+
hits: number;
|
|
64
|
+
maxHits: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Entries whose per-entry hit count (every matched line, error-class or
|
|
68
|
+
* not: the `N×` number the attribution block prints and the operator tunes
|
|
69
|
+
* against) exceeds their ceiling. Entries without a ceiling never appear.
|
|
70
|
+
*/
|
|
71
|
+
export declare function findOverCapAllowlistEntries(allow: readonly CompiledAllowlistEntry[], hits: readonly number[]): OverCapAllowlistEntry[];
|
|
48
72
|
/**
|
|
49
73
|
* Compiles an array of regex-string inputs (e.g. repeated `--console-allow`
|
|
50
74
|
* flag values). Same fail-fast semantics as {@link compileAllowlistFromFile}.
|
|
@@ -56,6 +56,14 @@ export function matchesSmokeError(line) {
|
|
|
56
56
|
}
|
|
57
57
|
return false;
|
|
58
58
|
}
|
|
59
|
+
/** `# max-hits: N` on its own comment line, case-insensitive. */
|
|
60
|
+
const MAX_HITS_DIRECTIVE = /^#\s*max-hits:\s*(\S+)\s*$/i;
|
|
61
|
+
function parseMaxHitsDirective(raw, at) {
|
|
62
|
+
if (!/^[1-9]\d*$/.test(raw)) {
|
|
63
|
+
throw new Error(`Invalid allowlist max-hits directive at ${at}: expected a positive integer (got "${raw}")`);
|
|
64
|
+
}
|
|
65
|
+
return Number.parseInt(raw, 10);
|
|
66
|
+
}
|
|
59
67
|
/**
|
|
60
68
|
* Returns the index of the FIRST entry in `allow` matching `line`, or -1.
|
|
61
69
|
* First-match attribution is deterministic and cheap. A line matching
|
|
@@ -71,30 +79,67 @@ export function matchAllowlist(line, allow) {
|
|
|
71
79
|
* compiled as a RegExp with its `<file>:<line>` origin retained. A bad
|
|
72
80
|
* pattern throws immediately: better to fail fast at CLI parse time than
|
|
73
81
|
* to silently let a typo match nothing.
|
|
82
|
+
*
|
|
83
|
+
* A `# max-hits: N` comment sets the ceiling of the entry that follows it
|
|
84
|
+
* (other comment lines may sit between). A blank line or end of file with
|
|
85
|
+
* no entry after the directive is an error rather than a silently dropped
|
|
86
|
+
* cap, since a cap the operator wrote and the run ignored is the exact
|
|
87
|
+
* shape of failure this directive exists to prevent.
|
|
74
88
|
*/
|
|
75
89
|
export function compileAllowlistFromFile(body, sourcePath) {
|
|
76
90
|
const lines = body.split(/\r?\n/);
|
|
77
91
|
const compiled = [];
|
|
92
|
+
let pending;
|
|
93
|
+
const assertNoOrphanDirective = () => {
|
|
94
|
+
if (pending) {
|
|
95
|
+
throw new Error(`Invalid allowlist max-hits directive at ${pending.at}: no entry follows it (the ceiling applies to the next non-comment line)`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
78
98
|
lines.forEach((raw, index) => {
|
|
79
99
|
const line = raw.trim();
|
|
80
|
-
|
|
100
|
+
const at = `${sourcePath}:${index + 1}`;
|
|
101
|
+
if (!line) {
|
|
102
|
+
assertNoOrphanDirective();
|
|
81
103
|
return;
|
|
104
|
+
}
|
|
105
|
+
if (line.startsWith('#')) {
|
|
106
|
+
const directive = MAX_HITS_DIRECTIVE.exec(line);
|
|
107
|
+
if (directive)
|
|
108
|
+
pending = { maxHits: parseMaxHitsDirective(directive[1] ?? '', at), at };
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
82
111
|
try {
|
|
83
112
|
compiled.push({
|
|
84
113
|
pattern: new RegExp(line),
|
|
85
114
|
source: line,
|
|
86
|
-
origin:
|
|
115
|
+
origin: at,
|
|
116
|
+
...(pending ? { maxHits: pending.maxHits } : {}),
|
|
87
117
|
});
|
|
88
118
|
}
|
|
89
119
|
catch (error) {
|
|
90
120
|
const message = toError(error).message;
|
|
91
|
-
throw new Error(`Invalid allowlist regex at ${
|
|
92
|
-
cause: error,
|
|
93
|
-
});
|
|
121
|
+
throw new Error(`Invalid allowlist regex at ${at}: ${message}`, { cause: error });
|
|
94
122
|
}
|
|
123
|
+
pending = undefined;
|
|
95
124
|
});
|
|
125
|
+
assertNoOrphanDirective();
|
|
96
126
|
return compiled;
|
|
97
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Entries whose per-entry hit count (every matched line, error-class or
|
|
130
|
+
* not: the `N×` number the attribution block prints and the operator tunes
|
|
131
|
+
* against) exceeds their ceiling. Entries without a ceiling never appear.
|
|
132
|
+
*/
|
|
133
|
+
export function findOverCapAllowlistEntries(allow, hits) {
|
|
134
|
+
const over = [];
|
|
135
|
+
allow.forEach((entry, index) => {
|
|
136
|
+
const count = hits[index] ?? 0;
|
|
137
|
+
if (entry.maxHits !== undefined && count > entry.maxHits) {
|
|
138
|
+
over.push({ entry, hits: count, maxHits: entry.maxHits });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return over;
|
|
142
|
+
}
|
|
98
143
|
/**
|
|
99
144
|
* Compiles an array of regex-string inputs (e.g. repeated `--console-allow`
|
|
100
145
|
* flag values). Same fail-fast semantics as {@link compileAllowlistFromFile}.
|
|
@@ -655,6 +655,12 @@ export interface TestOptions extends EngineLockWaitable {
|
|
|
655
655
|
* checkers that consume a sample artifact after the run.
|
|
656
656
|
*/
|
|
657
657
|
perfSamples?: string;
|
|
658
|
+
/**
|
|
659
|
+
* Seeded file-order shuffle for mochitest runs. `true` draws a fresh seed;
|
|
660
|
+
* a number replays one. Forwarded as mach `--shuffle` with the seed
|
|
661
|
+
* exported as `FIREFORGE_SHUFFLE_SEED` and stamped on the verdict line.
|
|
662
|
+
*/
|
|
663
|
+
shuffle?: number | boolean;
|
|
658
664
|
}
|
|
659
665
|
/**
|
|
660
666
|
* Options for the furnace apply command.
|
package/docs/configuration.md
CHANGED
|
@@ -219,6 +219,14 @@ non-CI launches print a warning saying so. Exit codes 12 and 13 separate a
|
|
|
219
219
|
console regression from a launch failure. See
|
|
220
220
|
[`exit-codes.md`](exit-codes.md).
|
|
221
221
|
|
|
222
|
+
The allowlist (`--console-allow-file`) is count-blind by default: a line that
|
|
223
|
+
matches an entry never counts, however often it fires. A `# max-hits: N`
|
|
224
|
+
comment directly above an entry (other comments may sit between) caps how
|
|
225
|
+
many lines that entry may match in one run. The attribution block prints
|
|
226
|
+
capped entries as `hits×/N`, and an entry that fires past its ceiling is
|
|
227
|
+
reported and fails the run with exit 12, the same as an unallowed error. A
|
|
228
|
+
directive with no entry after it is refused rather than silently ignored.
|
|
229
|
+
|
|
222
230
|
## Lock waits
|
|
223
231
|
|
|
224
232
|
Every FireForge command that mutates the engine takes the engine-session
|
package/docs/exit-codes.md
CHANGED
|
@@ -7,25 +7,25 @@ branches on it, and CI uses it to decide whether to retry or escalate. The
|
|
|
7
7
|
values live in `src/errors/codes.ts`. This file says what they mean and
|
|
8
8
|
which error class produces each one.
|
|
9
9
|
|
|
10
|
-
| Code | Name | Meaning
|
|
11
|
-
| ---: | ---------------------- |
|
|
12
|
-
| 0 | `SUCCESS` | Completed. Also a run that prompted and the operator declined: declining is a successful run that chose not to proceed.
|
|
13
|
-
| 1 | `GENERAL_ERROR` | Unspecified failure the operator has to act on.
|
|
14
|
-
| 2 | `CONFIG_ERROR` | `fireforge.json` missing or invalid.
|
|
15
|
-
| 3 | `DOWNLOAD_ERROR` | Could not download or extract Firefox source.
|
|
16
|
-
| 4 | `GIT_ERROR` | A git operation failed.
|
|
17
|
-
| 5 | `BUILD_ERROR` | `mach build` failed, or a test suite went red.
|
|
18
|
-
| 6 | `PATCH_ERROR` | A patch failed to apply, or the patch queue could not be mutated.
|
|
19
|
-
| 7 | `MISSING_DEPENDENCY` | A required tool (python3, git, tar) was not found.
|
|
20
|
-
| 8 | `INVALID_ARGUMENT` | The flags were wrong. A usage problem, not an environment one.
|
|
21
|
-
| 9 | `FURNACE_ERROR` | A Furnace component operation failed.
|
|
22
|
-
| 10 | `RESOLUTION_ERROR` | Conflict resolution failed.
|
|
23
|
-
| 11 | `INTERNAL_ERROR` | A FireForge invariant did not hold. This is a bug in FireForge, not something the operator can fix.
|
|
24
|
-
| 12 | `SMOKE_EXIT_FAILURE` | `run --smoke-exit` saw console errors that were not allowed in the smoke window.
|
|
25
|
-
| 13 | `SMOKE_LAUNCH_FAILURE` | `run --smoke-exit` saw the browser exit uncleanly before the window elapsed.
|
|
26
|
-
| 14 | `INCONCLUSIVE` | A test run's verdict was thrown away because `engine/` moved while the harness ran. This is not red. Re-run it.
|
|
27
|
-
| 15 | `LOCK_TIMEOUT` | A lock wait expired, so the run never started. Re-queue it with a larger `--wait-lock`.
|
|
28
|
-
| 130 | `USER_CANCELLED` | The operator interrupted a prompt (Esc or Ctrl+C). 128 + SIGINT.
|
|
10
|
+
| Code | Name | Meaning | Produced by |
|
|
11
|
+
| ---: | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
12
|
+
| 0 | `SUCCESS` | Completed. Also a run that prompted and the operator declined: declining is a successful run that chose not to proceed. | (none) |
|
|
13
|
+
| 1 | `GENERAL_ERROR` | Unspecified failure the operator has to act on. | `GeneralError` (and `PreflightRefusalError`), `ParserFallbackError`, `ExecTimeoutError` |
|
|
14
|
+
| 2 | `CONFIG_ERROR` | `fireforge.json` missing or invalid. | `ConfigError` and subclasses |
|
|
15
|
+
| 3 | `DOWNLOAD_ERROR` | Could not download or extract Firefox source. | `DownloadError` and subclasses |
|
|
16
|
+
| 4 | `GIT_ERROR` | A git operation failed. | `GitError` and subclasses, except `GitNotFoundError` |
|
|
17
|
+
| 5 | `BUILD_ERROR` | `mach build` failed, or a test suite went red. | `BuildError` and subclasses, `TestFailureError` |
|
|
18
|
+
| 6 | `PATCH_ERROR` | A patch failed to apply, or the patch queue could not be mutated. | `PatchError`, `PatchManifestCorruptError`, `RebaseError` and subclasses |
|
|
19
|
+
| 7 | `MISSING_DEPENDENCY` | A required tool (python3, git, tar) was not found. | `PythonNotFoundError`, `GitNotFoundError`, `MachNotFoundError` |
|
|
20
|
+
| 8 | `INVALID_ARGUMENT` | The flags were wrong. A usage problem, not an environment one. | `InvalidArgumentError`, plus commander usage errors (unknown command or option, missing argument) |
|
|
21
|
+
| 9 | `FURNACE_ERROR` | A Furnace component operation failed. | `FurnaceError` |
|
|
22
|
+
| 10 | `RESOLUTION_ERROR` | Conflict resolution failed. | `ResolutionError` |
|
|
23
|
+
| 11 | `INTERNAL_ERROR` | A FireForge invariant did not hold. This is a bug in FireForge, not something the operator can fix. | `InternalInvariantError` |
|
|
24
|
+
| 12 | `SMOKE_EXIT_FAILURE` | `run --smoke-exit` saw console errors that were not allowed in the smoke window, or an allowlisted shape fired past its `# max-hits` ceiling. | `SmokeRunError` |
|
|
25
|
+
| 13 | `SMOKE_LAUNCH_FAILURE` | `run --smoke-exit` saw the browser exit uncleanly before the window elapsed. | `SmokeRunError` |
|
|
26
|
+
| 14 | `INCONCLUSIVE` | A test run's verdict was thrown away because `engine/` moved while the harness ran. This is not red. Re-run it. | `InconclusiveVerdictError` |
|
|
27
|
+
| 15 | `LOCK_TIMEOUT` | A lock wait expired, so the run never started. Re-queue it with a larger `--wait-lock`. | `LockContentionError` |
|
|
28
|
+
| 130 | `USER_CANCELLED` | The operator interrupted a prompt (Esc or Ctrl+C). 128 + SIGINT. | `CancellationError` |
|
|
29
29
|
|
|
30
30
|
Usage errors that commander detects itself (an unknown command, an unknown
|
|
31
31
|
or malformed option, a missing required argument) never reach a command
|
package/docs/machine-output.md
CHANGED
|
@@ -53,16 +53,16 @@ if (options.json === true) {
|
|
|
53
53
|
|
|
54
54
|
## Current surfaces
|
|
55
55
|
|
|
56
|
-
| Surface | Shape
|
|
57
|
-
| ---------------------------------- |
|
|
58
|
-
| `status --json` | `{ schemaVersion: 1, … }`
|
|
59
|
-
| `tree list --json` | `{ schemaVersion: 1, trees }`
|
|
60
|
-
| `token list --json` | `{ schemaVersion: 1, tokensCssPath, categories }`
|
|
61
|
-
| `token show --json` | `{ schemaVersion: 1, name, category, declarations }`
|
|
62
|
-
| `test` verdict line | `FIREFORGE-VERDICT: PASS\|FAIL reason=… [note=<class>] [log=<path>]` | Not JSON. A single final stdout line, sealed by `setStdoutSealed` so nothing can displace it. `log=` names the run's own complete output under `.fireforge/logs/`, so a piped or truncated run still leaves a re-readable artifact |
|
|
63
|
-
| `lint --per-patch --report <path>` | `{ schemaVersion: 1, … }`
|
|
64
|
-
| `status --raw` | `git status --porcelain` lines
|
|
65
|
-
| `build` log announcement | `Full build output: <path>`
|
|
56
|
+
| Surface | Shape | Notes |
|
|
57
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `status --json` | `{ schemaVersion: 1, … }` | Error envelope on three engine preconditions |
|
|
59
|
+
| `tree list --json` | `{ schemaVersion: 1, trees }` | Error envelope added in 0.44.0 |
|
|
60
|
+
| `token list --json` | `{ schemaVersion: 1, tokensCssPath, categories }` | Categories in file order, each with its tokens (`name`, `line`, `value`) |
|
|
61
|
+
| `token show --json` | `{ schemaVersion: 1, name, category, declarations }` | One entry per declaring block, with its selector trail and line |
|
|
62
|
+
| `test` verdict line | `FIREFORGE-VERDICT: PASS\|FAIL reason=… [note=<class>] [shuffle=<seed>] [log=<path>]` | Not JSON. A single final stdout line, sealed by `setStdoutSealed` so nothing can displace it. `log=` names the run's own complete output under `.fireforge/logs/`, so a piped or truncated run still leaves a re-readable artifact |
|
|
63
|
+
| `lint --per-patch --report <path>` | `{ schemaVersion: 1, … }` | Written to a file, never read back by FireForge |
|
|
64
|
+
| `status --raw` | `git status --porcelain` lines | Not JSON, but still a machine surface: it engages the same stdout discipline, so rules 3 and 4 apply and rules 1 and 2 do not. A clean tree prints nothing |
|
|
65
|
+
| `build` log announcement | `Full build output: <path>` | Not JSON. `build` prints no verdict line, so it names its run log directly on the way out |
|
|
66
66
|
|
|
67
67
|
`--raw` is why `cli.ts` reads `process.argv` for `--json` or `--raw` before
|
|
68
68
|
dispatching. Rule 3 is about who owns stdout, and that question is settled
|
|
@@ -112,6 +112,18 @@ consumers should treat an unrecognised one as opaque. Current values:
|
|
|
112
112
|
`stale-browser`, `stale-browser-kill-failed`, `marionette-port-busy`,
|
|
113
113
|
`coverage-replaced`, `stale-build`, `stale-components`.
|
|
114
114
|
|
|
115
|
+
### The additive `shuffle=` key
|
|
116
|
+
|
|
117
|
+
A `fireforge test --shuffle [seed]` run forwards mach's `--shuffle` (an
|
|
118
|
+
unseeded shuffle of file order in the harness) and exports the seed FireForge
|
|
119
|
+
drew or was given as `FIREFORGE_SHUFFLE_SEED` in the harness environment. The
|
|
120
|
+
seed is appended to the verdict line as `shuffle=<seed>` on every emission
|
|
121
|
+
form, so a red found by shuffling carries its own reproduction
|
|
122
|
+
(`fireforge test --shuffle=<seed> …`) on the one line a pipe keeps. Reordering
|
|
123
|
+
tasks _inside_ a file is harness code that reads the exported seed; FireForge
|
|
124
|
+
owns the seed, the plumbing and the record. Absent when the run did not
|
|
125
|
+
shuffle.
|
|
126
|
+
|
|
115
127
|
A preflight refusal's own text is written to stdout before the verdict line,
|
|
116
128
|
and into the run log, so the verdict stays the last stdout write while the
|
|
117
129
|
reason survives both a redirect and a `tail`. See
|
package/docs/testing.md
CHANGED
|
@@ -101,7 +101,7 @@ a slow build for a silently wrong test.
|
|
|
101
101
|
Every test run ends with one machine-readable line:
|
|
102
102
|
|
|
103
103
|
```
|
|
104
|
-
FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [log=<path>]
|
|
104
|
+
FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [shuffle=<seed>] [log=<path>]
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
Automation should branch on this line rather than on the raw process code,
|
|
@@ -117,6 +117,18 @@ text is written to stdout before the verdict line and into the run log, so a
|
|
|
117
117
|
redirected run keeps the reason as well as the verdict. See
|
|
118
118
|
[`run-logs.md`](run-logs.md).
|
|
119
119
|
|
|
120
|
+
## Seeded shuffle
|
|
121
|
+
|
|
122
|
+
`fireforge test --shuffle` runs a mochitest selection in a seeded random file
|
|
123
|
+
order and prints the seed; `--shuffle=<seed>` replays it. FireForge forwards
|
|
124
|
+
mach's own `--shuffle`, exports `FIREFORGE_SHUFFLE_SEED=<seed>` to the harness
|
|
125
|
+
so in-file task reordering (harness `head.js` code, not FireForge) can key off
|
|
126
|
+
the same number, and stamps `shuffle=<seed>` on the verdict line. It is
|
|
127
|
+
mochitest-only: the xpcshell harness has no shuffle, and generic `mach test`
|
|
128
|
+
dispatch would hand the flag to every suite. Write the flag as
|
|
129
|
+
`--shuffle=<seed>` or after the paths, because a bare `--shuffle <path>` reads
|
|
130
|
+
the path as the seed.
|
|
131
|
+
|
|
120
132
|
Exit code 14 (`INCONCLUSIVE`) is not red: it means `engine/` moved while the
|
|
121
133
|
harness ran and the result was thrown away. Exit 15 (`LOCK_TIMEOUT`) means
|
|
122
134
|
the run never started. See [`exit-codes.md`](exit-codes.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hominis/fireforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "FireForge — a build tool for customizing Firefox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@eslint/js": "^10.0.0",
|
|
68
68
|
"@types/estree": "^1.0.8",
|
|
69
|
-
"@types/node": "^
|
|
70
|
-
"@vitest/coverage-v8": "^
|
|
69
|
+
"@types/node": "^26.4.1",
|
|
70
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
71
71
|
"dpdm": "4.3.0",
|
|
72
72
|
"eslint": "^10.0.0",
|
|
73
73
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -75,14 +75,14 @@
|
|
|
75
75
|
"eslint-plugin-simple-import-sort": "^14.0.0",
|
|
76
76
|
"fast-check": "^4.6.0",
|
|
77
77
|
"husky": "^9.1.7",
|
|
78
|
-
"knip": "6.
|
|
78
|
+
"knip": "6.35.1",
|
|
79
79
|
"lint-staged": "^17.0.4",
|
|
80
80
|
"prettier": "3.9.6",
|
|
81
81
|
"tsx": "^4.7.0",
|
|
82
82
|
"typescript": "~6.0.0",
|
|
83
83
|
"typescript-eslint": "^8.0.0",
|
|
84
84
|
"vite": "^8.0.7",
|
|
85
|
-
"vitest": "^
|
|
85
|
+
"vitest": "^5.0.0"
|
|
86
86
|
},
|
|
87
87
|
"engines": {
|
|
88
88
|
"node": ">=22.22.2"
|