@hominis/fireforge 0.46.2 → 0.47.1

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 (72) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/commands/config.js +1 -2
  5. package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +59 -0
  6. package/dist/src/commands/furnace/deploy-fragment-includers.js +120 -0
  7. package/dist/src/commands/furnace/deploy.js +36 -3
  8. package/dist/src/commands/furnace/validation-output.d.ts +5 -0
  9. package/dist/src/commands/furnace/validation-output.js +35 -9
  10. package/dist/src/commands/patch/delete-gap-notice.d.ts +24 -0
  11. package/dist/src/commands/patch/delete-gap-notice.js +46 -0
  12. package/dist/src/commands/patch/delete.js +11 -0
  13. package/dist/src/commands/re-export-lint.d.ts +26 -1
  14. package/dist/src/commands/re-export-lint.js +60 -20
  15. package/dist/src/commands/re-export-scan.d.ts +8 -0
  16. package/dist/src/commands/re-export-scan.js +12 -2
  17. package/dist/src/commands/re-export-single.js +24 -5
  18. package/dist/src/commands/run.js +42 -17
  19. package/dist/src/commands/test-harness-env.d.ts +24 -0
  20. package/dist/src/commands/test-harness-env.js +61 -0
  21. package/dist/src/commands/test-harness-teardown.d.ts +62 -0
  22. package/dist/src/commands/test-harness-teardown.js +128 -0
  23. package/dist/src/commands/test-register.js +8 -3
  24. package/dist/src/commands/test-run.d.ts +2 -0
  25. package/dist/src/commands/test-run.js +36 -7
  26. package/dist/src/commands/test-verdict.d.ts +21 -4
  27. package/dist/src/commands/test-verdict.js +37 -4
  28. package/dist/src/commands/test.js +27 -26
  29. package/dist/src/core/build-jar-escalation.d.ts +1 -4
  30. package/dist/src/core/build-jar-escalation.js +1 -4
  31. package/dist/src/core/build-prepare.js +72 -55
  32. package/dist/src/core/config-paths.d.ts +1 -1
  33. package/dist/src/core/config-paths.js +1 -0
  34. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  35. package/dist/src/core/file-lock.js +16 -9
  36. package/dist/src/core/firefox-cache.js +1 -2
  37. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  38. package/dist/src/core/furnace-apply-output.js +19 -4
  39. package/dist/src/core/furnace-css-fragments.d.ts +7 -0
  40. package/dist/src/core/furnace-css-fragments.js +12 -3
  41. package/dist/src/core/furnace-fragment-includers.d.ts +56 -0
  42. package/dist/src/core/furnace-fragment-includers.js +122 -0
  43. package/dist/src/core/git-diff-new-file.js +1 -2
  44. package/dist/src/core/git-file-ops.js +1 -2
  45. package/dist/src/core/git.js +1 -2
  46. package/dist/src/core/harness-helper-pids.d.ts +98 -0
  47. package/dist/src/core/harness-helper-pids.js +240 -0
  48. package/dist/src/core/harness-orphans.d.ts +53 -8
  49. package/dist/src/core/harness-orphans.js +125 -44
  50. package/dist/src/core/mach-build-artifacts.js +1 -2
  51. package/dist/src/core/mach.d.ts +16 -0
  52. package/dist/src/core/mach.js +6 -1
  53. package/dist/src/core/marionette-preflight.js +9 -3
  54. package/dist/src/core/mochitest-server-port.d.ts +43 -4
  55. package/dist/src/core/mochitest-server-port.js +64 -11
  56. package/dist/src/core/parent-exit-watchdog.d.ts +62 -0
  57. package/dist/src/core/parent-exit-watchdog.js +69 -0
  58. package/dist/src/core/patch-lint-imports.js +4 -4
  59. package/dist/src/core/smoke-patterns.d.ts +24 -0
  60. package/dist/src/core/smoke-patterns.js +50 -5
  61. package/dist/src/types/commands/options.d.ts +17 -1
  62. package/dist/src/types/config.d.ts +7 -0
  63. package/dist/src/utils/process-group.d.ts +5 -1
  64. package/dist/src/utils/process-group.js +7 -3
  65. package/dist/src/utils/process.d.ts +18 -0
  66. package/dist/src/utils/process.js +14 -2
  67. package/docs/configuration.md +15 -1
  68. package/docs/exit-codes.md +19 -19
  69. package/docs/lifecycle-invariants.md +17 -1
  70. package/docs/machine-output.md +44 -19
  71. package/docs/testing.md +80 -4
  72. package/package.json +5 -5
@@ -53,16 +53,16 @@ if (options.json === true) {
53
53
 
54
54
  ## Current surfaces
55
55
 
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>] [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 |
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
@@ -86,15 +86,15 @@ moved.
86
86
  `FIREFORGE-VERDICT: FAIL reason=<reason>` uses a closed set
87
87
  (`FireforgeVerdictReason`, `src/commands/test-verdict.ts`):
88
88
 
89
- | Reason | Meaning |
90
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
91
- | `crash` | The harness or the browser died, so there is no trustworthy suite result. |
92
- | `no-tests` | The run dispatched but nothing ran. |
93
- | `test-failures` | The suite ran and reported unexpected results. |
94
- | `preflight` | The run was refused before the harness was reached. |
95
- | `inconclusive` | A result exists, but `engine/` moved under it, so the result was discarded. |
96
- | `lock-timeout` | The run never started: the engine session lock stayed contended. |
97
- | `killed` | A signal terminated the run. Written from the signal handler so that a log tail always describes itself. A killed run must never be silent, or "killed", "still running" and "never started" all look alike. |
89
+ | Reason | Meaning |
90
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `crash` | The harness or the browser died, so there is no trustworthy suite result. |
92
+ | `no-tests` | The run dispatched but nothing ran. |
93
+ | `test-failures` | The suite ran and reported unexpected results. |
94
+ | `preflight` | The run was refused before the harness was reached. |
95
+ | `inconclusive` | A result exists, but `engine/` moved under it, so the result was discarded. |
96
+ | `lock-timeout` | The run never started: the engine session lock stayed contended. |
97
+ | `killed` | A signal terminated the run, or FireForge ended it because its parent process vanished. Written from the signal handler (or the parent-exit watchdog) so that a log tail always describes itself. A killed run must never be silent, or "killed", "still running" and "never started" all look alike. `signal=` names the cause: a signal name, or `parent-exit`. |
98
98
 
99
99
  ### The additive `note=` key
100
100
 
@@ -112,6 +112,31 @@ 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
+
127
+ ### The additive `orphans-reaped=` key
128
+
129
+ How many harness processes FireForge terminated itself during the run: at
130
+ preflight, survivors of an earlier run in this objdir under `--reap-orphans`
131
+ or `test.reapOrphans: "reap"`; at teardown, helpers this run's harness
132
+ launched (httpd, websocket server, ssltunnel, process bridge, the browser,
133
+ `moz-http2`) that were still alive after mach exited. Summed across the census, every
134
+ retry attempt and every shard. Absent when nothing was reaped, so a green on
135
+ a quiet machine prints the line it always did, and a green after a reap is
136
+ distinguishable from it. A run ended by SIGTERM or by the parent-exit
137
+ watchdog writes its verdict before the teardown reap runs, so that line never
138
+ carries the key; the reap is reported on stderr instead.
139
+
115
140
  A preflight refusal's own text is written to stdout before the verdict line,
116
141
  and into the run log, so the verdict stays the last stdout write while the
117
142
  reason survives both a redirect and a `tail`. See
package/docs/testing.md CHANGED
@@ -87,21 +87,85 @@ a slow build for a silently wrong test.
87
87
  earlier run in this project's objdir. It runs at preflight, before this run
88
88
  spawns anything, so every hit is necessarily a survivor. A match has to be
89
89
  objdir-anchored, since `xpcshell` and `server.js` are far too generic to
90
- report on their own. Survivors slow every later run without appearing in
90
+ report on their own, and structural: the process's executable is a helper
91
+ binary, or an interpreter whose script argument is a helper script. A
92
+ process that merely mentions such a path (an editor, a `grep`, the shell
93
+ FireForge runs under) is not a helper, and FireForge's own ancestors are
94
+ never candidates. Survivors slow every later run without appearing in
91
95
  its output. The usual symptom is a three-second suite taking minutes of
92
- wall clock. The census is report-only by default, and `--reap-orphans`
93
- terminates what it finds. It never refuses a run. A different shape,
96
+ wall clock. The census is report-only by default; `--reap-orphans`, or
97
+ `test.reapOrphans: "reap"` in `fireforge.json`, terminates what it finds
98
+ (SIGTERM, a 500 ms grace, SIGKILL). It never refuses a run. Every reap is
99
+ stamped on the verdict line as `orphans-reaped=<n>`. A different shape,
94
100
  reparented Python `multiprocessing` workers, is covered by the
95
101
  `Orphaned harness workers` doctor check.
102
+ - Every dispatch owns its own kill path. The harness announces what it
103
+ launches (`runtests.py | Server pid: <n>`, the websocket server, the SSL
104
+ tunnel, the websocket/process bridge, and the browser as `Application
105
+ pid`); FireForge tracks those pids and, after mach has exited for any
106
+ reason (a clean finish, a harness crash before a retry, a no-output
107
+ timeout, a forwarded SIGTERM, the parent-exit watchdog below), terminates
108
+ any of them still alive. A tracked pid is re-read from `ps` first and must
109
+ still be anchored to this objdir and still look like the thing that was
110
+ launched, so a recycled pid is never signalled. One helper is never
111
+ announced: mozserve starts `moz-http2` in a session of its own and logs no
112
+ pid, so it is in neither the group nor the announcements. For it, FireForge
113
+ snapshots the same-objdir helpers before the dispatch spawns and, after
114
+ mach exits, reaps any helper-shaped process under this checkout's absolute
115
+ objdir that is new since that snapshot and reparented to launchd. An
116
+ earlier run's survivor is in the snapshot and stays the census's call. This
117
+ reap runs inside the exec layer's close path, so the signal handler's
118
+ bounded child-shutdown wait covers it. The count joins `orphans-reaped=`.
119
+ - FireForge watches its own parent while a dispatch runs. A supervisor that
120
+ SIGKILLs the process above `fireforge test` (an `npm run` step killed at
121
+ its bound) signals nothing below it; FireForge notices the reparenting
122
+ within about two seconds, writes `FIREFORGE-VERDICT: FAIL reason=killed
123
+ signal=parent-exit`, reaps the harness process group and the tracked
124
+ helpers, and ends the run without classifying or retrying.
96
125
  - `--perf-samples <path>` publishes a perf-sample artifact path to the
97
126
  harness (exported as `<BINARYNAME>_PERF_SAMPLE_JSON`).
98
127
 
128
+ ## Process groups and supervisors
129
+
130
+ `fireforge test` spawns mach as the leader of its **own** process group
131
+ (`detached: true`), so that FireForge can signal the whole harness tree with
132
+ one negative-pid kill and sweep the group after mach exits. Two consequences
133
+ for anything that supervises `fireforge test`:
134
+
135
+ - A supervisor that kills _its own_ process group (`kill -- -<npm pid>`) does
136
+ not reach mach: mach is not in that group. Killing the `npm`/`node` chain
137
+ above FireForge leaves the harness tree running.
138
+ - SIGTERM to FireForge is the right signal. FireForge forwards it to the
139
+ mach group, escalates to SIGKILL after a grace period, sweeps the group,
140
+ reaps the tracked helpers, and writes `FAIL reason=killed signal=SIGTERM`.
141
+ (That line, like the watchdog's `signal=parent-exit` line, is written
142
+ before the teardown reap runs, so it cannot carry `orphans-reaped=`; the
143
+ reap reports to stderr.)
144
+ - A supervisor that has to SIGKILL FireForge should pass
145
+ `--pgid-file <path>`. FireForge rewrites the file with the harness group id
146
+ on every mach spawn (each retry attempt and shard) and removes it when the
147
+ run ends under its own control. After a SIGKILL the file is still there:
148
+ `kill -TERM -- -$(cat <path>)`, then `-KILL`, takes mach and every helper
149
+ that shares its group. Two processes are outside it: the browser
150
+ (mozprocess puts it in a group of its own) and `moz-http2` (mozserve
151
+ starts it in its own session). A SIGTERM to the group gives `runtests.py`
152
+ the chance to run its own cleanup, which normally takes the browser with
153
+ it; nothing takes `moz-http2`, so after a SIGKILL of FireForge it stays
154
+ until the next run's census, which is why that run should carry the reap
155
+ posture. A browser that does survive is the Marionette preflight's and
156
+ `--kill-stale-marionette`'s to remove.
157
+
158
+ The mochitest helpers (httpd, websocket server, ssltunnel, process bridge)
159
+ are plain children of `runtests.py` and share mach's group. The browser and
160
+ `moz-http2` do not, which is why the teardown reap above exists alongside the
161
+ group sweep.
162
+
99
163
  ## The verdict line
100
164
 
101
165
  Every test run ends with one machine-readable line:
102
166
 
103
167
  ```
104
- FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [log=<path>]
168
+ FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [shuffle=<seed>] [orphans-reaped=<n>] [log=<path>]
105
169
  ```
106
170
 
107
171
  Automation should branch on this line rather than on the raw process code,
@@ -117,6 +181,18 @@ text is written to stdout before the verdict line and into the run log, so a
117
181
  redirected run keeps the reason as well as the verdict. See
118
182
  [`run-logs.md`](run-logs.md).
119
183
 
184
+ ## Seeded shuffle
185
+
186
+ `fireforge test --shuffle` runs a mochitest selection in a seeded random file
187
+ order and prints the seed; `--shuffle=<seed>` replays it. FireForge forwards
188
+ mach's own `--shuffle`, exports `FIREFORGE_SHUFFLE_SEED=<seed>` to the harness
189
+ so in-file task reordering (harness `head.js` code, not FireForge) can key off
190
+ the same number, and stamps `shuffle=<seed>` on the verdict line. It is
191
+ mochitest-only: the xpcshell harness has no shuffle, and generic `mach test`
192
+ dispatch would hand the flag to every suite. Write the flag as
193
+ `--shuffle=<seed>` or after the paths, because a bare `--shuffle <path>` reads
194
+ the path as the seed.
195
+
120
196
  Exit code 14 (`INCONCLUSIVE`) is not red: it means `engine/` moved while the
121
197
  harness ran and the result was thrown away. Exit 15 (`LOCK_TIMEOUT`) means
122
198
  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.46.2",
3
+ "version": "0.47.1",
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": "^22.0.0",
70
- "@vitest/coverage-v8": "^4.1.2",
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.32.3",
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": "^4.0.18"
85
+ "vitest": "^5.0.0"
86
86
  },
87
87
  "engines": {
88
88
  "node": ">=22.22.2"