@hominis/fireforge 0.44.1 → 0.44.3
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 +42 -0
- package/README.md +18 -110
- package/dist/build-info.json +4 -4
- package/dist/src/commands/build.js +17 -0
- package/dist/src/commands/furnace/apply.js +2 -2
- package/dist/src/commands/furnace/deploy.js +2 -2
- package/dist/src/commands/furnace/index.js +12 -6
- package/dist/src/commands/furnace/init.js +5 -2
- package/dist/src/commands/furnace/sync.js +2 -2
- package/dist/src/commands/re-export-files.js +63 -23
- package/dist/src/commands/re-export-scan.js +18 -2
- package/dist/src/commands/re-export-single.js +19 -7
- package/dist/src/commands/test-register.js +5 -1
- package/dist/src/commands/test-run.d.ts +5 -0
- package/dist/src/commands/test-run.js +11 -4
- package/dist/src/commands/test-verdict.js +10 -1
- package/dist/src/commands/test.js +9 -0
- package/dist/src/commands/token.js +36 -12
- package/dist/src/core/build-audit.js +6 -2
- package/dist/src/core/furnace-jsconfig.js +5 -3
- package/dist/src/core/furnace-operation.d.ts +25 -2
- package/dist/src/core/furnace-operation.js +27 -2
- package/dist/src/core/mach-known-noise-filter.d.ts +17 -0
- package/dist/src/core/mach-known-noise-filter.js +19 -0
- package/dist/src/core/mach.d.ts +8 -3
- package/dist/src/core/mach.js +47 -3
- package/dist/src/core/run-log.d.ts +60 -0
- package/dist/src/core/run-log.js +183 -0
- package/dist/src/core/test-harness-output.d.ts +24 -1
- package/dist/src/core/test-harness-output.js +46 -15
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -2
- package/dist/src/core/test-xpcshell-retry.js +6 -1
- package/dist/src/core/token-manager.d.ts +7 -2
- package/dist/src/core/token-manager.js +74 -20
- package/dist/src/core/token-variant.d.ts +46 -2
- package/dist/src/core/token-variant.js +190 -33
- package/dist/src/types/commands/options.d.ts +30 -1
- package/dist/src/utils/child-env.d.ts +30 -0
- package/dist/src/utils/child-env.js +30 -0
- package/dist/src/utils/options.d.ts +28 -0
- package/dist/src/utils/options.js +32 -1
- package/dist/src/utils/process.d.ts +22 -0
- package/dist/src/utils/process.js +8 -7
- package/docs/README.md +35 -0
- package/docs/configuration.md +116 -0
- package/docs/exit-codes.md +63 -0
- package/docs/furnace.md +57 -0
- package/docs/lifecycle-invariants.md +309 -0
- package/docs/machine-output.md +94 -0
- package/docs/patch-workflow.md +119 -0
- package/docs/run-logs.md +60 -0
- package/docs/testing.md +92 -0
- package/docs/verification-trees.md +48 -0
- package/package.json +2 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Environment construction for spawned children.
|
|
4
|
+
*
|
|
5
|
+
* Its own module rather than a helper inside `process.ts` only because that
|
|
6
|
+
* file sits on a 500-line budget; the logic belongs to the exec layer.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The child's environment: `process.env`, with `env` merged over it, then
|
|
10
|
+
* every `envUnset` key removed.
|
|
11
|
+
*
|
|
12
|
+
* One helper rather than five inline spreads — the removal step is easy to
|
|
13
|
+
* add at four of the five spawn sites and forget at the fifth, which would
|
|
14
|
+
* make the option silently inert exactly where it was not tested.
|
|
15
|
+
*
|
|
16
|
+
* Removal is a real removal, not an assignment to `''`: mozbuild's
|
|
17
|
+
* `is_running_under_coding_agent()` reads `CLAUDECODE` for its PRESENCE, so
|
|
18
|
+
* an empty value would not unset it. `Reflect.deleteProperty` rather than
|
|
19
|
+
* `delete obj[key]` because the key is computed.
|
|
20
|
+
*
|
|
21
|
+
* @param options - Options carrying `env` / `envUnset`
|
|
22
|
+
* @returns The environment to hand `spawn`
|
|
23
|
+
*/
|
|
24
|
+
export function buildChildEnv(options) {
|
|
25
|
+
const merged = { ...process.env, ...options.env };
|
|
26
|
+
for (const key of options.envUnset ?? []) {
|
|
27
|
+
Reflect.deleteProperty(merged, key);
|
|
28
|
+
}
|
|
29
|
+
return merged;
|
|
30
|
+
}
|
|
@@ -66,6 +66,28 @@ export declare const WAIT_LOCK_ENV_VAR = "FIREFORGE_WAIT_LOCK";
|
|
|
66
66
|
* ABSENCE, which is where it belongs.
|
|
67
67
|
*/
|
|
68
68
|
export declare function resolveWaitLockSeconds(value: string | number | boolean | undefined): number | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The session-wide wait budget in MILLISECONDS, or undefined when none is
|
|
71
|
+
* set. Sourced from {@link WAIT_LOCK_ENV_VAR} only.
|
|
72
|
+
*
|
|
73
|
+
* Exists for the SECONDARY locks a command takes after the one its
|
|
74
|
+
* `--wait-lock` flag was threaded to. A furnace mutation takes the engine
|
|
75
|
+
* session lock and then `.fireforge/furnace.lock`; only the first had a
|
|
76
|
+
* budget, so a contended deploy died on the file lock's fixed 30 s while
|
|
77
|
+
* its operator believed a much larger budget was in force. Threading the
|
|
78
|
+
* flag reaches the three furnace commands that declare it — this reaches
|
|
79
|
+
* the twelve that do not (`create`, `remove`, `rename`, `refresh`, `scan`,
|
|
80
|
+
* `override`, `chrome-doc`, …), which have no flag to thread but are just
|
|
81
|
+
* as contended.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately env-only: a flag belongs to one invocation and is passed
|
|
84
|
+
* explicitly where it applies, while the environment is exactly the place a
|
|
85
|
+
* session-wide default is expressed. An explicit budget at a call site
|
|
86
|
+
* always wins over this.
|
|
87
|
+
*
|
|
88
|
+
* @returns The session budget in milliseconds, or undefined
|
|
89
|
+
*/
|
|
90
|
+
export declare function sessionWaitLockMs(): number | undefined;
|
|
69
91
|
/**
|
|
70
92
|
* True when `command` already declares `--wait-lock` itself. Used by the
|
|
71
93
|
* CLI wiring to add the accept-and-ignore variant only where the honoring
|
|
@@ -85,5 +107,11 @@ export declare function ensureWaitLockOptionEverywhere(command: Command): void;
|
|
|
85
107
|
* for the bare flag or a validated integer; feed it through
|
|
86
108
|
* {@link resolveWaitLockSeconds} at the call site to obtain the
|
|
87
109
|
* `waitLockSeconds` for `withEngineSessionLock`/`withPatchDirectoryLock`.
|
|
110
|
+
*
|
|
111
|
+
* A command can take MORE than one lock — a furnace mutation takes the
|
|
112
|
+
* engine session lock and then `.fireforge/furnace.lock` — and the budget
|
|
113
|
+
* has to reach all of them or the flag is a half-truth. The secondary locks
|
|
114
|
+
* read the session budget through {@link sessionWaitLockMs} rather than
|
|
115
|
+
* having it threaded to every call site.
|
|
88
116
|
*/
|
|
89
117
|
export declare function addWaitLockOption(command: Command): Command;
|
|
@@ -144,6 +144,31 @@ export function resolveWaitLockSeconds(value) {
|
|
|
144
144
|
}
|
|
145
145
|
return parsed;
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* The session-wide wait budget in MILLISECONDS, or undefined when none is
|
|
149
|
+
* set. Sourced from {@link WAIT_LOCK_ENV_VAR} only.
|
|
150
|
+
*
|
|
151
|
+
* Exists for the SECONDARY locks a command takes after the one its
|
|
152
|
+
* `--wait-lock` flag was threaded to. A furnace mutation takes the engine
|
|
153
|
+
* session lock and then `.fireforge/furnace.lock`; only the first had a
|
|
154
|
+
* budget, so a contended deploy died on the file lock's fixed 30 s while
|
|
155
|
+
* its operator believed a much larger budget was in force. Threading the
|
|
156
|
+
* flag reaches the three furnace commands that declare it — this reaches
|
|
157
|
+
* the twelve that do not (`create`, `remove`, `rename`, `refresh`, `scan`,
|
|
158
|
+
* `override`, `chrome-doc`, …), which have no flag to thread but are just
|
|
159
|
+
* as contended.
|
|
160
|
+
*
|
|
161
|
+
* Deliberately env-only: a flag belongs to one invocation and is passed
|
|
162
|
+
* explicitly where it applies, while the environment is exactly the place a
|
|
163
|
+
* session-wide default is expressed. An explicit budget at a call site
|
|
164
|
+
* always wins over this.
|
|
165
|
+
*
|
|
166
|
+
* @returns The session budget in milliseconds, or undefined
|
|
167
|
+
*/
|
|
168
|
+
export function sessionWaitLockMs() {
|
|
169
|
+
const seconds = waitLockSecondsFromEnv();
|
|
170
|
+
return seconds === undefined ? undefined : seconds * 1000;
|
|
171
|
+
}
|
|
147
172
|
/**
|
|
148
173
|
* Registers `--wait-lock [seconds]` on a command that takes NO lock, so a
|
|
149
174
|
* scripted sequence can blanket-append the flag without hitting a usage
|
|
@@ -188,7 +213,13 @@ export function ensureWaitLockOptionEverywhere(command) {
|
|
|
188
213
|
* for the bare flag or a validated integer; feed it through
|
|
189
214
|
* {@link resolveWaitLockSeconds} at the call site to obtain the
|
|
190
215
|
* `waitLockSeconds` for `withEngineSessionLock`/`withPatchDirectoryLock`.
|
|
216
|
+
*
|
|
217
|
+
* A command can take MORE than one lock — a furnace mutation takes the
|
|
218
|
+
* engine session lock and then `.fireforge/furnace.lock` — and the budget
|
|
219
|
+
* has to reach all of them or the flag is a half-truth. The secondary locks
|
|
220
|
+
* read the session budget through {@link sessionWaitLockMs} rather than
|
|
221
|
+
* having it threaded to every call site.
|
|
191
222
|
*/
|
|
192
223
|
export function addWaitLockOption(command) {
|
|
193
|
-
return command.option('--wait-lock [seconds]', `Wait up to this many seconds (default 60) for another FireForge command to release
|
|
224
|
+
return command.option('--wait-lock [seconds]', `Wait up to this many seconds (default 60) for another FireForge command to release EVERY lock this command takes, instead of failing on the default budget. Set ${WAIT_LOCK_ENV_VAR} to apply a budget to every command in a session`, commanderArgParser((raw) => resolveWaitLockSeconds(raw)));
|
|
194
225
|
}
|
|
@@ -28,6 +28,28 @@ export interface ExecOptions {
|
|
|
28
28
|
cwd?: string;
|
|
29
29
|
/** Environment variables */
|
|
30
30
|
env?: Record<string, string>;
|
|
31
|
+
/**
|
|
32
|
+
* Environment variables to REMOVE from the child's inherited environment,
|
|
33
|
+
* applied after {@link ExecOptions.env} is merged over `process.env`.
|
|
34
|
+
*
|
|
35
|
+
* Needed because `env` can only add or overwrite, and some variables are
|
|
36
|
+
* read for their PRESENCE — mozbuild's `is_running_under_coding_agent()`
|
|
37
|
+
* keys on `CLAUDECODE`, so setting it empty would not unset it. An
|
|
38
|
+
* explicit delete says what it means; relying on Node dropping
|
|
39
|
+
* `undefined`-valued env entries would be a semantics gamble at a layer
|
|
40
|
+
* that must not have any.
|
|
41
|
+
*/
|
|
42
|
+
envUnset?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Streams to mirror the child's output into, replacing the default
|
|
45
|
+
* `process.stdout` / `process.stderr` on the stdio-inheriting capture
|
|
46
|
+
* path. Lets a caller tee the live output somewhere else (a run log)
|
|
47
|
+
* without a second per-chunk hook.
|
|
48
|
+
*/
|
|
49
|
+
mirror?: {
|
|
50
|
+
stdout?: NodeJS.WritableStream;
|
|
51
|
+
stderr?: NodeJS.WritableStream;
|
|
52
|
+
};
|
|
31
53
|
/** Timeout in milliseconds */
|
|
32
54
|
timeout?: number;
|
|
33
55
|
/**
|
|
@@ -3,6 +3,7 @@ import { spawn } from 'node:child_process';
|
|
|
3
3
|
import { constants as osConstants } from 'node:os';
|
|
4
4
|
import { StringDecoder } from 'node:string_decoder';
|
|
5
5
|
import { ExecTimeoutError } from '../errors/base.js';
|
|
6
|
+
import { buildChildEnv } from './child-env.js';
|
|
6
7
|
import { killProcessTree, sweepProcessGroup } from './process-group.js';
|
|
7
8
|
// 50 MB cap per stream to prevent OOM — large toolchain builds (e.g. Firefox, Chromium)
|
|
8
9
|
// can easily blow past this, so we truncate rather than let the buffer grow unbounded.
|
|
@@ -85,7 +86,7 @@ export async function exec(command, args, options = {}) {
|
|
|
85
86
|
return new Promise((resolve, reject) => {
|
|
86
87
|
const child = spawn(command, args, {
|
|
87
88
|
cwd: options.cwd,
|
|
88
|
-
env:
|
|
89
|
+
env: buildChildEnv(options),
|
|
89
90
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
91
|
signal: buildSignalFromTimeout(options.timeout),
|
|
91
92
|
});
|
|
@@ -119,7 +120,7 @@ export async function execStream(command, args, options = {}) {
|
|
|
119
120
|
const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
|
|
120
121
|
const child = spawn(command, args, {
|
|
121
122
|
cwd: options.cwd,
|
|
122
|
-
env:
|
|
123
|
+
env: buildChildEnv(options),
|
|
123
124
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
124
125
|
signal: buildSignalFromTimeout(options.timeout),
|
|
125
126
|
detached: usesProcessGroup,
|
|
@@ -256,7 +257,7 @@ export async function execInherit(command, args, options = {}) {
|
|
|
256
257
|
const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
|
|
257
258
|
const child = spawn(command, args, {
|
|
258
259
|
cwd: options.cwd,
|
|
259
|
-
env:
|
|
260
|
+
env: buildChildEnv(options),
|
|
260
261
|
stdio: 'inherit',
|
|
261
262
|
signal: buildSignalFromTimeout(options.timeout),
|
|
262
263
|
detached: usesProcessGroup,
|
|
@@ -368,14 +369,14 @@ export async function execInheritCapture(command, args, options = {}) {
|
|
|
368
369
|
const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
|
|
369
370
|
const child = spawn(command, args, {
|
|
370
371
|
cwd: options.cwd,
|
|
371
|
-
env:
|
|
372
|
+
env: buildChildEnv(options),
|
|
372
373
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
373
374
|
signal: buildSignalFromTimeout(options.timeout),
|
|
374
375
|
detached: usesProcessGroup,
|
|
375
376
|
});
|
|
376
377
|
const groupPid = usesProcessGroup ? child.pid : undefined;
|
|
377
|
-
const out = createStreamCollector(process.stdout);
|
|
378
|
-
const err = createStreamCollector(process.stderr);
|
|
378
|
+
const out = createStreamCollector(options.mirror?.stdout ?? process.stdout);
|
|
379
|
+
const err = createStreamCollector(options.mirror?.stderr ?? process.stderr);
|
|
379
380
|
child.stdout.on('data', out.onData);
|
|
380
381
|
child.stderr.on('data', err.onData);
|
|
381
382
|
const graceMs = options.shutdownGraceMs ?? 1500;
|
|
@@ -440,7 +441,7 @@ export async function execSmokeRun(command, args, options) {
|
|
|
440
441
|
const usesProcessGroup = process.platform !== 'win32';
|
|
441
442
|
const child = spawn(command, args, {
|
|
442
443
|
cwd: options.cwd,
|
|
443
|
-
env:
|
|
444
|
+
env: buildChildEnv(options),
|
|
444
445
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
445
446
|
// A new process group on POSIX lets us signal the whole descendant
|
|
446
447
|
// tree at once, which is essential for mach → python → firefox →
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
|
2
|
+
|
|
3
|
+
# FireForge documentation
|
|
4
|
+
|
|
5
|
+
[`../README.md`](../README.md) is the getting-started page. These files are
|
|
6
|
+
the reference material behind it. `fireforge --help` (and
|
|
7
|
+
`fireforge <command> --help`) stays the authority on flags; the docs cover
|
|
8
|
+
what a flag list cannot state.
|
|
9
|
+
|
|
10
|
+
## Using FireForge
|
|
11
|
+
|
|
12
|
+
| File | Covers |
|
|
13
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
14
|
+
| [patch-workflow.md](patch-workflow.md) | Export, re-export, deletions, queue maintenance, per-patch lint, CI gating, rebasing |
|
|
15
|
+
| [testing.md](testing.md) | `fireforge test`: scope, modes, build freshness, the verdict line, verbosity |
|
|
16
|
+
| [furnace.md](furnace.md) | Components: overrides, shared CSS fragments, typed cross-module imports |
|
|
17
|
+
| [verification-trees.md](verification-trees.md) | CoW snapshots for concurrent read-mostly verification |
|
|
18
|
+
| [configuration.md](configuration.md) | `fireforge.json` settings, design tokens, lock waits, environment variables |
|
|
19
|
+
|
|
20
|
+
## Contracts for scripts and CI
|
|
21
|
+
|
|
22
|
+
| File | Covers |
|
|
23
|
+
| -------------------------------------- | --------------------------------------------------------------------------- |
|
|
24
|
+
| [exit-codes.md](exit-codes.md) | Every exit code, which error class produces it, and how CI should branch |
|
|
25
|
+
| [machine-output.md](machine-output.md) | The `--json` / `--raw` / verdict-line output contract and its versioning |
|
|
26
|
+
| [run-logs.md](run-logs.md) | `.fireforge/logs/`: what `test` and `build` write, and what survives a pipe |
|
|
27
|
+
|
|
28
|
+
## Internals
|
|
29
|
+
|
|
30
|
+
| File | Covers |
|
|
31
|
+
| -------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
32
|
+
| [lifecycle-invariants.md](lifecycle-invariants.md) | Locking, rollback, signal handling, and the decision table for new mutations |
|
|
33
|
+
|
|
34
|
+
Release history and the reasoning behind individual changes live in
|
|
35
|
+
[`../CHANGELOG.md`](../CHANGELOG.md).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
|
2
|
+
|
|
3
|
+
# Configuration
|
|
4
|
+
|
|
5
|
+
`fireforge.json` at the project root is the project's configuration;
|
|
6
|
+
`furnace.json` holds component configuration. This file covers the settings
|
|
7
|
+
whose behaviour needs more than a schema line, plus the environment variables
|
|
8
|
+
FireForge reads.
|
|
9
|
+
|
|
10
|
+
## External toolchains
|
|
11
|
+
|
|
12
|
+
Projects with generated asset prerequisites can declare opt-in
|
|
13
|
+
`externalToolchains`. `fireforge doctor` reports missing **required** tools as
|
|
14
|
+
errors and missing **optional** tools as warnings.
|
|
15
|
+
|
|
16
|
+
Seasonal branding on macOS, for example, can declare Apple's Icon Composer
|
|
17
|
+
tool at
|
|
18
|
+
`/Applications/Xcode.app/Contents/Applications/Icon Composer.app/Contents/Executables/ictool`,
|
|
19
|
+
plus `actool` via `xcrun`, `sips`, and `iconutil`.
|
|
20
|
+
|
|
21
|
+
## `patchLint`
|
|
22
|
+
|
|
23
|
+
Per-patch lint type-checks plain Firefox JS against a built-in Firefox-globals
|
|
24
|
+
shim that tracks upstream WebIDL additions per release.
|
|
25
|
+
|
|
26
|
+
- `patchLint.checkJsExtraShim` adds members to the structured shim globals via
|
|
27
|
+
TypeScript interface merging, e.g.
|
|
28
|
+
`interface ChromeUtilsShim { newApi(): any }`.
|
|
29
|
+
- `patchLint.checkJsTestFiles: true` extends the pass to patch-owned test
|
|
30
|
+
`.js` files (`browser_*` / `test_*` / `xpcshell_*` and `/test/` paths). Each
|
|
31
|
+
is checked as its own small script-scope program against a loose built-in
|
|
32
|
+
harness shim.
|
|
33
|
+
- `patchLint.checkJsTestShim` points at a project `.d.ts` whose typed
|
|
34
|
+
declarations (e.g. a real `TestUtils` interface) override the loose
|
|
35
|
+
baseline, so calls to nonexistent harness members fail at export time.
|
|
36
|
+
|
|
37
|
+
## `patchPolicy`
|
|
38
|
+
|
|
39
|
+
`patchPolicy.ranges` makes `patch compact` range-aware, so closing ordinal
|
|
40
|
+
gaps does not move a patch out of the band its category owns.
|
|
41
|
+
|
|
42
|
+
## `test`
|
|
43
|
+
|
|
44
|
+
`test.canaryPath` and `test.canaryTimeoutSeconds` supply the defaults for
|
|
45
|
+
`fireforge test --canary`. See [`testing.md`](testing.md).
|
|
46
|
+
|
|
47
|
+
## Design tokens
|
|
48
|
+
|
|
49
|
+
Tokens are managed with `fireforge token add`.
|
|
50
|
+
|
|
51
|
+
- `--create-category` declares a new category banner and inserts the token in
|
|
52
|
+
one step. It is incompatible with `--variant`; author the category with the
|
|
53
|
+
base token first, then add each variant.
|
|
54
|
+
- `--category` is required for a base declaration and optional under
|
|
55
|
+
`--variant`, where the declaration is routed into a `:root<selector>` block
|
|
56
|
+
and never into a category section. It is still validated when supplied.
|
|
57
|
+
- `--variant` takes the same selector grammar the block matcher understands:
|
|
58
|
+
one attribute fragment (`[data-private]`, `[data-skin=precision]`), a run of
|
|
59
|
+
them (`[data-skin=precision][data-theme=dark]`), and an optional pseudo-class
|
|
60
|
+
tail (`[data-skin=precision]:not([data-private])`). Names and values must be
|
|
61
|
+
identifier-safe, and `=value` is normalized to `="value"`. A variant naming a
|
|
62
|
+
qualifier matches only a block carrying that qualifier; one without a
|
|
63
|
+
qualifier still matches a qualified block, and the write says so.
|
|
64
|
+
- `--mode override` mirrors into both `:root[data-theme="dark"]` and
|
|
65
|
+
`:root[data-theme="light"]`. A qualified block such as
|
|
66
|
+
`:root[data-theme="light"]:not([data-private])` is matched and written
|
|
67
|
+
through, with a warning naming the selector — the qualifier is
|
|
68
|
+
semantically load-bearing, but silently skipping half the mirror is the
|
|
69
|
+
worse failure.
|
|
70
|
+
- A token already present in the target block is reported with its location
|
|
71
|
+
(`already exists in :root[…] (line N), unchanged`) rather than a bare
|
|
72
|
+
no-op.
|
|
73
|
+
|
|
74
|
+
## Smoke runs
|
|
75
|
+
|
|
76
|
+
For unattended smoke checks, `fireforge run --smoke-exit <s> --headless`
|
|
77
|
+
launches the browser headless. A headed smoke window on a shared desktop
|
|
78
|
+
absorbs live input, which contaminates the console capture — headed non-CI
|
|
79
|
+
launches print a warning saying so. Exit codes 12 and 13 separate a console
|
|
80
|
+
regression from a launch failure; see [`exit-codes.md`](exit-codes.md).
|
|
81
|
+
|
|
82
|
+
## Lock waits
|
|
83
|
+
|
|
84
|
+
Every FireForge command that mutates the engine takes the engine-session lock,
|
|
85
|
+
and some take a second subsystem lock as well. Contention fails fast after
|
|
86
|
+
about a second by default.
|
|
87
|
+
|
|
88
|
+
`--wait-lock [seconds]` (bare flag = 60, valid 1–3600) replaces that with a
|
|
89
|
+
bounded wait, and the budget covers **every** lock the command takes — for
|
|
90
|
+
`furnace apply`/`deploy`/`sync` that means the furnace lock as well as the
|
|
91
|
+
engine-session lock. While waiting, a progress line roughly every 5 seconds
|
|
92
|
+
identifies the holder by PID, command and start time.
|
|
93
|
+
|
|
94
|
+
Commands that take no lock accept `--wait-lock` and ignore it, so a scripted
|
|
95
|
+
sequence can blanket-append the flag without a usage error killing the run.
|
|
96
|
+
|
|
97
|
+
`FIREFORGE_WAIT_LOCK=<seconds>` applies the same budget to every command in a
|
|
98
|
+
session, using the same bounds as the flag. It is the only way to give a
|
|
99
|
+
budget to the furnace mutators that declare no flag (`create`, `remove`,
|
|
100
|
+
`rename`, `refresh`, `scan`, `override`, `chrome-doc`, …). An explicit
|
|
101
|
+
`--wait-lock` always wins.
|
|
102
|
+
|
|
103
|
+
`fireforge status --lock` reports the current holder and queue depth.
|
|
104
|
+
|
|
105
|
+
## Environment variables
|
|
106
|
+
|
|
107
|
+
| Variable | Effect |
|
|
108
|
+
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| `FIREFORGE_WAIT_LOCK` | Session-wide lock wait budget in seconds (1–3600). See above. |
|
|
110
|
+
| `FIREFORGE_MAX_UNTRACKED_FILES` | Per-directory cap when expanding collapsed untracked directories in `status` (default 5000). A non-positive value warns and is ignored. |
|
|
111
|
+
| `FIREFORGE_GIT_ADD_TIMEOUT_MS` | Timeout for the monolithic `git add -A` baseline pass (default 10 min). For slow or loaded filesystems. |
|
|
112
|
+
| `FIREFORGE_GIT_ADD_CHUNK_TIMEOUT_MS` | Timeout for the chunked `git add -- <dir>` fallback (default 30 min). Paired with the above. |
|
|
113
|
+
|
|
114
|
+
`CLAUDECODE` is read by mozbuild, not by FireForge, and `fireforge test
|
|
115
|
+
--full-output` unsets it for test dispatches only — see
|
|
116
|
+
[`testing.md`](testing.md#output-verbosity).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
|
2
|
+
|
|
3
|
+
# Exit codes (internal)
|
|
4
|
+
|
|
5
|
+
The exit code is the machine-readable half of the CLI's contract: a script
|
|
6
|
+
branches on it, and CI decides whether to retry or escalate. The values live
|
|
7
|
+
in `src/errors/codes.ts`; this file is the operator-facing statement of what
|
|
8
|
+
they mean and which class produces each.
|
|
9
|
+
|
|
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. | — |
|
|
13
|
+
| 1 | `GENERAL_ERROR` | Unspecified failure the operator must act on. | `GeneralError`, `ParserFallbackError` |
|
|
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 |
|
|
17
|
+
| 5 | `BUILD_ERROR` | `mach build` failed. | `BuildError` and subclasses |
|
|
18
|
+
| 6 | `PATCH_ERROR` | A patch failed to apply, or the patch queue could not be mutated. | `PatchError` |
|
|
19
|
+
| 7 | `MISSING_DEPENDENCY` | A required tool (python3, git, tar) was not found. | `PythonNotFoundError`, `GitNotFoundError`, `MachNotFoundError` |
|
|
20
|
+
| 8 | `INVALID_ARGUMENT` | The flags were wrong. Usage problem, not an environment one. | `InvalidArgumentError` |
|
|
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. **A bug in FireForge**, not something the operator can fix. | `InternalInvariantError` |
|
|
24
|
+
| 12 | `SMOKE_EXIT_FAILURE` | `run --smoke-exit` saw unallowed console errors in the smoke window. | `SmokeRunError` |
|
|
25
|
+
| 13 | `SMOKE_LAUNCH_FAILURE` | `run --smoke-exit` saw a non-clean browser exit before the window elapsed. | `SmokeRunError` |
|
|
26
|
+
| 14 | `INCONCLUSIVE` | A test run's verdict was thrown away: `engine/` moved while the harness ran. **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 / Ctrl+C). 128 + SIGINT. | `CancellationError` |
|
|
29
|
+
|
|
30
|
+
## The three distinctions that matter for CI
|
|
31
|
+
|
|
32
|
+
**Retry vs escalate.** `GENERAL_ERROR` usually means the operator must change
|
|
33
|
+
something and a retry may help. `INTERNAL_ERROR` means nothing they could
|
|
34
|
+
change would have helped — escalate it, do not retry.
|
|
35
|
+
|
|
36
|
+
**Red vs never-ran vs thrown-away.** These are three different facts and,
|
|
37
|
+
before 0.44.0, two of them shared exit 1. `BUILD_ERROR` (5) is a suite that
|
|
38
|
+
FAILED. `LOCK_TIMEOUT` (15) is a run that never started, because another
|
|
39
|
+
FireForge process held the lock for the whole budget — nothing about the
|
|
40
|
+
request was wrong, so re-queue it. `INCONCLUSIVE` (14) is a suite whose
|
|
41
|
+
result was DISCARDED, because `engine/` moved underneath it — the code may
|
|
42
|
+
be perfectly green, and reporting it as red is wrong. A summary line reading
|
|
43
|
+
`FAIL — exit 1` beside `FAIL — exit 5` invited treating opposites as the same
|
|
44
|
+
kind of fact; the codes now separate them mechanically. The
|
|
45
|
+
`FIREFORGE-VERDICT:` line's `reason=` key carries the same distinction for
|
|
46
|
+
humans.
|
|
47
|
+
|
|
48
|
+
Note that `LockContentionError` backs every lock in FireForge — engine
|
|
49
|
+
session, patch directory, build, furnace — so all four now exit 15. That is
|
|
50
|
+
deliberate: the fact is the same whichever lock was contended.
|
|
51
|
+
|
|
52
|
+
**Interrupted vs declined.** Before 0.44.0 both a Ctrl+C and a deliberate
|
|
53
|
+
"no" at a confirmation returned the same value and exited 0, so a script
|
|
54
|
+
could not tell them apart. They are now distinct: an interrupt raises
|
|
55
|
+
`CancellationError` and exits **130**; a declined confirmation returns
|
|
56
|
+
normally and exits **0**, because the run did what it was asked and the
|
|
57
|
+
answer was no.
|
|
58
|
+
|
|
59
|
+
## Adding a code
|
|
60
|
+
|
|
61
|
+
New codes go in `src/errors/codes.ts` with a comment saying what a consumer
|
|
62
|
+
should DO differently on seeing it — if the answer is "nothing different from
|
|
63
|
+
an existing code", it does not need one. Add a row here in the same change.
|
package/docs/furnace.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
|
2
|
+
|
|
3
|
+
# Furnace components
|
|
4
|
+
|
|
5
|
+
Furnace is FireForge's answer to UI components. It tracks stock widgets,
|
|
6
|
+
creates new fork-owned widgets, and overrides existing ones. The files still
|
|
7
|
+
land inside the Firefox source directory and end up as part of the regular
|
|
8
|
+
patches to export.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx fireforge furnace scan
|
|
12
|
+
npx fireforge furnace override moz-button -t css-only
|
|
13
|
+
npx fireforge furnace create moz-my-widget
|
|
14
|
+
npx fireforge furnace deploy
|
|
15
|
+
npx fireforge furnace status
|
|
16
|
+
npx fireforge furnace preview
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`fireforge furnace --help` lists every subcommand.
|
|
20
|
+
|
|
21
|
+
## Shared CSS fragments
|
|
22
|
+
|
|
23
|
+
Cross-widget CSS can be single-sourced. Place the fragment in
|
|
24
|
+
`components/shared/` and reference it from a widget stylesheet with a
|
|
25
|
+
comment:
|
|
26
|
+
|
|
27
|
+
```css
|
|
28
|
+
/* @fireforge-include base-button.css */
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`deploy` expands it into the deployed copy only, so editing the fragment
|
|
32
|
+
surfaces as component drift until the next deploy.
|
|
33
|
+
|
|
34
|
+
## Typed cross-module imports
|
|
35
|
+
|
|
36
|
+
For multi-file components, set `furnace.json#typecheckJsconfig` to a
|
|
37
|
+
consumer-owned jsconfig. `deploy` then maintains `compilerOptions.paths`
|
|
38
|
+
entries mapping each deployed
|
|
39
|
+
`chrome://global/content/elements/<file>.mjs` URL to its workspace source.
|
|
40
|
+
|
|
41
|
+
Entries are recognized as Furnace-managed by containment in the components
|
|
42
|
+
workspace, so a hand-written entry is preserved and a managed entry whose
|
|
43
|
+
helper was deleted is pruned.
|
|
44
|
+
|
|
45
|
+
## Safety
|
|
46
|
+
|
|
47
|
+
Every furnace mutation runs through `runFurnaceMutation`: a per-root lock, a
|
|
48
|
+
rollback journal restored on both a signal and a thrown error, and a
|
|
49
|
+
`pendingRepair` marker that blocks further mutations when a rollback was
|
|
50
|
+
incomplete — cleared with `fireforge doctor --repair-furnace`. The full
|
|
51
|
+
contract, including which commands take which locks, is in
|
|
52
|
+
[`lifecycle-invariants.md`](lifecycle-invariants.md).
|
|
53
|
+
|
|
54
|
+
`apply`, `deploy` and `sync` accept `--wait-lock [seconds]`, whose budget
|
|
55
|
+
covers **both** locks they take. The twelve mutators without the flag pick up
|
|
56
|
+
a session budget from `FIREFORGE_WAIT_LOCK`; see
|
|
57
|
+
[`configuration.md`](configuration.md#lock-waits).
|