@hominis/fireforge 0.36.0 → 0.38.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 +26 -0
- package/README.md +1 -1
- package/dist/src/commands/build.js +6 -6
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/doctor-orphaned-harness.d.ts +52 -0
- package/dist/src/commands/doctor-orphaned-harness.js +132 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/download.js +5 -2
- package/dist/src/commands/export-flow.d.ts +5 -1
- package/dist/src/commands/export-flow.js +10 -7
- package/dist/src/commands/export-placement-gate.js +3 -5
- package/dist/src/commands/export-placement-policy.d.ts +11 -3
- package/dist/src/commands/export-placement-policy.js +32 -29
- package/dist/src/commands/export.js +12 -5
- package/dist/src/commands/furnace/index.js +23 -18
- package/dist/src/commands/patch/move-files-create.d.ts +19 -0
- package/dist/src/commands/patch/move-files-create.js +116 -0
- package/dist/src/commands/patch/move-files.d.ts +3 -1
- package/dist/src/commands/patch/move-files.js +49 -4
- package/dist/src/commands/patch/split.d.ts +11 -1
- package/dist/src/commands/patch/split.js +6 -5
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
- package/dist/src/commands/patch/staged-dependency.js +113 -51
- package/dist/src/commands/re-export-files.js +4 -0
- package/dist/src/commands/re-export-scan.d.ts +17 -0
- package/dist/src/commands/re-export-scan.js +51 -0
- package/dist/src/commands/re-export.js +25 -6
- package/dist/src/commands/source.js +28 -3
- package/dist/src/commands/test-diagnose.js +20 -1
- package/dist/src/commands/test-register.js +6 -5
- package/dist/src/commands/test-stale-gate.d.ts +44 -0
- package/dist/src/commands/test-stale-gate.js +81 -0
- package/dist/src/commands/test.js +40 -23
- package/dist/src/core/build-audit.d.ts +12 -0
- package/dist/src/core/build-audit.js +14 -0
- package/dist/src/core/build-baseline-types.d.ts +58 -0
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +51 -7
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +6 -1
- package/dist/src/core/engine-session-lock.d.ts +10 -1
- package/dist/src/core/engine-session-lock.js +28 -2
- package/dist/src/core/file-lock.d.ts +33 -0
- package/dist/src/core/file-lock.js +32 -6
- package/dist/src/core/firefox-archive.d.ts +2 -1
- package/dist/src/core/firefox-archive.js +32 -8
- package/dist/src/core/firefox.d.ts +8 -3
- package/dist/src/core/firefox.js +11 -6
- package/dist/src/core/furnace-config-custom.js +10 -0
- package/dist/src/core/furnace-stale-export.d.ts +59 -0
- package/dist/src/core/furnace-stale-export.js +123 -0
- package/dist/src/core/furnace-validate-compatibility.js +9 -2
- package/dist/src/core/furnace-validate-helpers.js +28 -1
- package/dist/src/core/furnace-validate-structure.js +3 -2
- package/dist/src/core/license-headers.d.ts +12 -0
- package/dist/src/core/license-headers.js +76 -1
- package/dist/src/core/mach-known-noise-filter.d.ts +60 -0
- package/dist/src/core/mach-known-noise-filter.js +193 -0
- package/dist/src/core/mach.d.ts +8 -0
- package/dist/src/core/mach.js +36 -3
- package/dist/src/core/patch-lint-cross.js +80 -2
- package/dist/src/core/patch-lint.d.ts +3 -0
- package/dist/src/core/patch-lint.js +25 -7
- package/dist/src/core/patch-manifest-io.d.ts +3 -2
- package/dist/src/core/patch-manifest-io.js +27 -16
- package/dist/src/core/patch-manifest-validate.js +24 -0
- package/dist/src/core/status-classify.d.ts +11 -2
- package/dist/src/core/status-classify.js +49 -25
- package/dist/src/core/test-harness-crash.d.ts +16 -0
- package/dist/src/core/test-harness-crash.js +55 -0
- package/dist/src/core/test-stale-check.d.ts +69 -1
- package/dist/src/core/test-stale-check.js +134 -1
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +84 -7
- package/dist/src/types/commands/patches.d.ts +29 -0
- package/dist/src/types/config.d.ts +7 -0
- package/dist/src/types/furnace.d.ts +13 -0
- package/dist/src/utils/options.d.ts +20 -0
- package/dist/src/utils/options.js +39 -0
- package/dist/src/utils/process-group.d.ts +33 -0
- package/dist/src/utils/process-group.js +161 -0
- package/dist/src/utils/process.d.ts +15 -0
- package/dist/src/utils/process.js +88 -44
- package/dist/src/utils/validation.d.ts +5 -0
- package/dist/src/utils/validation.js +7 -0
- package/package.json +3 -3
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { getProjectPaths } from '../core/config.js';
|
|
4
4
|
import { appendHistory, confirmDestructive } from '../core/destructive.js';
|
|
5
|
+
import { enforceFreshFurnaceSources } from '../core/furnace-stale-export.js';
|
|
5
6
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
6
7
|
import { computeProjectedLintRegressions } from '../core/lint-projection.js';
|
|
7
8
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
@@ -151,6 +152,9 @@ export async function reExportFilesInPlace(paths, selectedPatches, options, conf
|
|
|
151
152
|
throw new InvalidArgumentError('reExportFilesInPlace called with no --files.', '--files');
|
|
152
153
|
}
|
|
153
154
|
const requested = [...new Set(filesOption)].sort();
|
|
155
|
+
// Stale-furnace-source gate (0.37.0 item 4): same refusal as the generic
|
|
156
|
+
// re-export path — the projected diff would capture stale deployed copies.
|
|
157
|
+
await enforceFreshFurnaceSources(paths.root, requested, options.allowStaleFurnace === true, 're-export');
|
|
154
158
|
const removed = target.filesAffected.filter((f) => !requested.includes(f));
|
|
155
159
|
const added = requested.filter((f) => !target.filesAffected.includes(f));
|
|
156
160
|
const retained = target.filesAffected.filter((f) => requested.includes(f));
|
|
@@ -28,6 +28,23 @@ export declare function confirmBroadScanAdditions(args: {
|
|
|
28
28
|
yes: boolean;
|
|
29
29
|
isInteractive: boolean;
|
|
30
30
|
}): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Refuses scan adoptions whose candidate files import modules created by
|
|
33
|
+
* LATER patches. Modeled on the `re-export --files` cross-patch
|
|
34
|
+
* projection: the candidates' diffs are projected into the adopting
|
|
35
|
+
* patch's queue entry, the queue lint runs baseline vs projection, and
|
|
36
|
+
* only forward-import regressions attributable to the candidates block.
|
|
37
|
+
* Staged-dependency declarations and inline lint-ignore markers are
|
|
38
|
+
* honored automatically because the real lint rule evaluates them.
|
|
39
|
+
* Covers broad `--scan`, `--scan-file`, and the `--scan-files` bulk flow
|
|
40
|
+
* (all adopt through the same scan result), including dry-run.
|
|
41
|
+
*/
|
|
42
|
+
export declare function assertScanAdoptionsHaveNoForwardImports(args: {
|
|
43
|
+
patchesDir: string;
|
|
44
|
+
engineDir: string;
|
|
45
|
+
patchFilename: string;
|
|
46
|
+
added: readonly string[];
|
|
47
|
+
}): Promise<void>;
|
|
31
48
|
/** Refuses explicit `--scan-file` additions that did not produce patch hunks. */
|
|
32
49
|
export declare function assertScanFileAdditionsHaveDiffHunks(args: {
|
|
33
50
|
diffContent: string;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { confirm } from '@clack/prompts';
|
|
4
|
+
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
4
5
|
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
6
|
+
import { computeProjectedLintRegressions } from '../core/lint-projection.js';
|
|
5
7
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
8
|
+
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../core/patch-lint.js';
|
|
6
9
|
import { getClaimedFiles } from '../core/patch-manifest.js';
|
|
10
|
+
import { extractNewFileContentFromDiff } from '../core/patch-transform.js';
|
|
7
11
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
8
12
|
import { pathExists } from '../utils/fs.js';
|
|
9
13
|
import { cancel, info, isCancel, warn } from '../utils/logger.js';
|
|
@@ -132,6 +136,53 @@ function scanAdditionsNeedConfirmation(added) {
|
|
|
132
136
|
return true;
|
|
133
137
|
return new Set(added.map((f) => dirname(f))).size >= SCAN_DIR_COUNT_THRESHOLD;
|
|
134
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Refuses scan adoptions whose candidate files import modules created by
|
|
141
|
+
* LATER patches. Modeled on the `re-export --files` cross-patch
|
|
142
|
+
* projection: the candidates' diffs are projected into the adopting
|
|
143
|
+
* patch's queue entry, the queue lint runs baseline vs projection, and
|
|
144
|
+
* only forward-import regressions attributable to the candidates block.
|
|
145
|
+
* Staged-dependency declarations and inline lint-ignore markers are
|
|
146
|
+
* honored automatically because the real lint rule evaluates them.
|
|
147
|
+
* Covers broad `--scan`, `--scan-file`, and the `--scan-files` bulk flow
|
|
148
|
+
* (all adopt through the same scan result), including dry-run.
|
|
149
|
+
*/
|
|
150
|
+
export async function assertScanAdoptionsHaveNoForwardImports(args) {
|
|
151
|
+
const { patchesDir, engineDir, patchFilename, added } = args;
|
|
152
|
+
if (added.length === 0)
|
|
153
|
+
return;
|
|
154
|
+
const candidateDiff = await getDiffForFilesAgainstHead(engineDir, [...added]);
|
|
155
|
+
if (!candidateDiff.trim())
|
|
156
|
+
return;
|
|
157
|
+
const candidateNewFiles = new Map();
|
|
158
|
+
for (const path of detectNewFilesInDiff(candidateDiff)) {
|
|
159
|
+
candidateNewFiles.set(path, extractNewFileContentFromDiff(candidateDiff, path));
|
|
160
|
+
}
|
|
161
|
+
const candidateAdditions = buildModifiedFileAdditionsFromDiff(candidateDiff);
|
|
162
|
+
const baseCtx = await buildPatchQueueContext(patchesDir);
|
|
163
|
+
const projectedEntries = baseCtx.entries.map((entry) => {
|
|
164
|
+
if (entry.filename !== patchFilename)
|
|
165
|
+
return entry;
|
|
166
|
+
return {
|
|
167
|
+
...entry,
|
|
168
|
+
diff: `${entry.diff}\n${candidateDiff}`,
|
|
169
|
+
newFiles: new Map([...entry.newFiles, ...candidateNewFiles]),
|
|
170
|
+
modifiedFileAdditions: new Map([...entry.modifiedFileAdditions, ...candidateAdditions]),
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
const baselineIssues = lintPatchQueue(baseCtx).filter((i) => i.severity === 'error');
|
|
174
|
+
const projectedIssues = lintPatchQueue({ entries: projectedEntries }).filter((i) => i.severity === 'error');
|
|
175
|
+
const addedSet = new Set(added);
|
|
176
|
+
const offending = computeProjectedLintRegressions(baselineIssues, projectedIssues).filter((issue) => issue.check === 'forward-import' && addedSet.has(issue.file));
|
|
177
|
+
if (offending.length === 0)
|
|
178
|
+
return;
|
|
179
|
+
const details = offending.map((issue) => ` - ${issue.file}: ${issue.message}`).join('\n');
|
|
180
|
+
throw new GeneralError(`Refusing to adopt ${offending.length} scanned file${offending.length === 1 ? '' : 's'} into ${patchFilename} ` +
|
|
181
|
+
`because they import modules created by later patches:\n${details}\n` +
|
|
182
|
+
'Export those files as their own later patch ("fireforge export --order <n>" / ' +
|
|
183
|
+
'"fireforge patch split --order <n>") or declare the intentional dependency with ' +
|
|
184
|
+
'"fireforge patch staged-dependency --add" before re-running the scan.');
|
|
185
|
+
}
|
|
135
186
|
/** Refuses explicit `--scan-file` additions that did not produce patch hunks. */
|
|
136
187
|
export function assertScanFileAdditionsHaveDiffHunks(args) {
|
|
137
188
|
const { diffContent, patchFilename, previousFilesAffected, scanFiles } = args;
|
|
@@ -5,6 +5,7 @@ import { multiselect } from '@clack/prompts';
|
|
|
5
5
|
import { Command, Option } from 'commander';
|
|
6
6
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
7
7
|
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
8
|
+
import { enforceFreshFurnaceSources } from '../core/furnace-stale-export.js';
|
|
8
9
|
import { isGitRepository } from '../core/git.js';
|
|
9
10
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
10
11
|
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
@@ -17,12 +18,12 @@ import { elapsedSince } from '../utils/elapsed.js';
|
|
|
17
18
|
import { toError } from '../utils/errors.js';
|
|
18
19
|
import { pathExists } from '../utils/fs.js';
|
|
19
20
|
import { cancel, info, intro, isCancel, outro, spinner, success, warn } from '../utils/logger.js';
|
|
20
|
-
import { pickDefined } from '../utils/options.js';
|
|
21
|
+
import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../utils/options.js';
|
|
21
22
|
import { runPatchLint } from './export-shared.js';
|
|
22
23
|
import { loadScanFilesAssignments, withDryRunReExportLock } from './re-export-bulk-scan.js';
|
|
23
24
|
import { reExportFilesInPlace } from './re-export-files.js';
|
|
24
25
|
import { applyReExportFilesPositionalFolding, validateReExportOptionCombinations, } from './re-export-options.js';
|
|
25
|
-
import { assertScanFileAdditionsHaveDiffHunks, confirmBroadScanAdditions, normalizeScanFiles, scanPatchFilesForReExport, } from './re-export-scan.js';
|
|
26
|
+
import { assertScanAdoptionsHaveNoForwardImports, assertScanFileAdditionsHaveDiffHunks, confirmBroadScanAdditions, normalizeScanFiles, scanPatchFilesForReExport, } from './re-export-scan.js';
|
|
26
27
|
async function findMissingFiles(engineDir, files) {
|
|
27
28
|
const missingFiles = [];
|
|
28
29
|
for (const file of files) {
|
|
@@ -86,6 +87,17 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
86
87
|
isDryRun,
|
|
87
88
|
...(options.scanFiles !== undefined ? { scanFiles: options.scanFiles } : {}),
|
|
88
89
|
});
|
|
90
|
+
// Forward-import gate at adoption time (runs in dry-run too): refuse
|
|
91
|
+
// to adopt unmanaged files that import modules created by LATER
|
|
92
|
+
// patches — the after-the-fact lint failure this leaves behind is the
|
|
93
|
+
// field-reported footgun. Applies to broad --scan, --scan-file, and
|
|
94
|
+
// the --scan-files bulk assignments alike.
|
|
95
|
+
await assertScanAdoptionsHaveNoForwardImports({
|
|
96
|
+
patchesDir: paths.patches,
|
|
97
|
+
engineDir: paths.engine,
|
|
98
|
+
patchFilename: patch.filename,
|
|
99
|
+
added: scanResult.added,
|
|
100
|
+
});
|
|
89
101
|
if (options.scanFiles === undefined) {
|
|
90
102
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
91
103
|
const proceed = await confirmBroadScanAdditions({
|
|
@@ -129,6 +141,12 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
129
141
|
for (const f of removed)
|
|
130
142
|
info(` - ${f}`);
|
|
131
143
|
}
|
|
144
|
+
// Stale-furnace-source gate (0.37.0 item 4): re-export captures deployed
|
|
145
|
+
// engine copies, so a component source edited after the last furnace
|
|
146
|
+
// apply would land in the patch as its OLD deployed content. Refuse (or
|
|
147
|
+
// warn under --allow-stale-furnace) before diffing. Runs in dry-run too
|
|
148
|
+
// so the failure surfaces early.
|
|
149
|
+
await enforceFreshFurnaceSources(paths.root, currentFilesAffected, options.allowStaleFurnace === true, 're-export');
|
|
132
150
|
const missingFiles = await findMissingFiles(paths.engine, currentFilesAffected);
|
|
133
151
|
if (missingFiles.length === currentFilesAffected.length) {
|
|
134
152
|
warn(`Skipped ${patch.filename}: all affected files missing`);
|
|
@@ -400,7 +418,7 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
400
418
|
}
|
|
401
419
|
/** Registers the re-export command on the CLI program. */
|
|
402
420
|
export function registerReExport(program, { getProjectRoot, withErrorHandling }) {
|
|
403
|
-
program
|
|
421
|
+
const reExport = program
|
|
404
422
|
.command('re-export [patches...]')
|
|
405
423
|
.description('Refresh existing patch bodies (and filesAffected with --scan) from the current engine ' +
|
|
406
424
|
'state. Does NOT change sourceVersion/sourceProduct by default — use --stamp or run ' +
|
|
@@ -415,13 +433,14 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
415
433
|
.filter((v) => v.length > 0))
|
|
416
434
|
.option('--dry-run', 'Show what would change without writing')
|
|
417
435
|
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
436
|
+
.option('--allow-stale-furnace', 'Export the deployed engine copy even when the components/ source changed since the last furnace apply')
|
|
418
437
|
.option('--allow-shrink', 'Allow --files to remove paths currently owned by the patch. Required before --yes can bypass the shrink confirmation.')
|
|
419
438
|
.option('-y, --yes', 'Skip confirmation prompts (required for non-TTY destructive writes)')
|
|
420
439
|
.option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
|
|
421
440
|
.option('--stamp', "After every selected patch refreshes cleanly, stamp each re-exported patch's sourceVersion/sourceProduct in patches.json to firefox.version/firefox.product from fireforge.json. No effect on a partial run.")
|
|
422
441
|
.addOption(new Option('--tier <tier>', 'Force a tier override on the selected patch (only "branding" recognised). Mutually exclusive with --all.').choices(['branding']))
|
|
423
|
-
.option('--lint-ignore <check-id>', 'Append a lint check ID to the patch\'s PatchMetadata.lintIgnore (union, de-duped, repeatable). Mutually exclusive with --all. Use "fireforge patch lint-ignore" for --remove / --clear.', (value, prev) => [...prev, value], [])
|
|
424
|
-
|
|
442
|
+
.option('--lint-ignore <check-id>', 'Append a lint check ID to the patch\'s PatchMetadata.lintIgnore (union, de-duped, repeatable). Mutually exclusive with --all. Use "fireforge patch lint-ignore" for --remove / --clear.', (value, prev) => [...prev, value], []);
|
|
443
|
+
addWaitLockOption(reExport).action(withErrorHandling(async (patches, options) => {
|
|
425
444
|
const { tier, lintIgnore, scanFile, scanFiles, ...rest } = options;
|
|
426
445
|
const projectRoot = getProjectRoot();
|
|
427
446
|
await withEngineSessionLock(projectRoot, 're-export', () => reExportCommand(projectRoot, patches, {
|
|
@@ -430,6 +449,6 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
430
449
|
...(scanFiles !== undefined ? { scanFilesManifest: scanFiles } : {}),
|
|
431
450
|
...(tier !== undefined ? { tier: tier } : {}),
|
|
432
451
|
...(lintIgnore !== undefined && lintIgnore.length > 0 ? { lintIgnore } : {}),
|
|
433
|
-
}));
|
|
452
|
+
}), { waitLockSeconds: resolveWaitLockSeconds(options.waitLock) });
|
|
434
453
|
}));
|
|
435
454
|
}
|
|
@@ -4,7 +4,7 @@ import { configExists, loadRawConfigDocument, validateConfig, withConfigFileLock
|
|
|
4
4
|
import { resolveArchive } from '../core/firefox-archive.js';
|
|
5
5
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
6
6
|
import { info, intro, outro, success } from '../utils/logger.js';
|
|
7
|
-
import { isValidFirefoxProduct } from '../utils/validation.js';
|
|
7
|
+
import { isValidFirefoxCandidate, isValidFirefoxProduct } from '../utils/validation.js';
|
|
8
8
|
const SOURCE_PRODUCTS = [
|
|
9
9
|
'firefox',
|
|
10
10
|
'firefox-esr',
|
|
@@ -27,6 +27,12 @@ function parseSourceProduct(product) {
|
|
|
27
27
|
}
|
|
28
28
|
throw new InvalidArgumentError(`--product must be one of: ${SOURCE_PRODUCTS.join(', ')}`, '--product');
|
|
29
29
|
}
|
|
30
|
+
function parseSourceCandidate(candidate) {
|
|
31
|
+
if (isValidFirefoxCandidate(candidate)) {
|
|
32
|
+
return candidate;
|
|
33
|
+
}
|
|
34
|
+
throw new InvalidArgumentError(`--candidate must look like "buildN" (e.g. "build2"), got "${candidate}"`, '--candidate');
|
|
35
|
+
}
|
|
30
36
|
/**
|
|
31
37
|
* Atomically updates the Firefox source tuple in fireforge.json.
|
|
32
38
|
*/
|
|
@@ -38,6 +44,9 @@ export async function sourceSetCommand(projectRoot, options) {
|
|
|
38
44
|
if (options.sha256 !== undefined && options.clearSha256 === true) {
|
|
39
45
|
throw new InvalidArgumentError('--sha256 cannot be combined with --clear-sha256', '--sha256');
|
|
40
46
|
}
|
|
47
|
+
if (options.candidate !== undefined && options.clearCandidate === true) {
|
|
48
|
+
throw new InvalidArgumentError('--candidate cannot be combined with --clear-candidate', '--candidate');
|
|
49
|
+
}
|
|
41
50
|
const written = await withConfigFileLock(projectRoot, async () => {
|
|
42
51
|
const raw = await loadRawConfigDocument(projectRoot);
|
|
43
52
|
const updated = cloneRawConfig(raw);
|
|
@@ -50,6 +59,12 @@ export async function sourceSetCommand(projectRoot, options) {
|
|
|
50
59
|
else if (options.sha256 !== undefined) {
|
|
51
60
|
firefox['sha256'] = options.sha256;
|
|
52
61
|
}
|
|
62
|
+
if (options.clearCandidate === true) {
|
|
63
|
+
delete firefox['candidate'];
|
|
64
|
+
}
|
|
65
|
+
else if (options.candidate !== undefined) {
|
|
66
|
+
firefox['candidate'] = options.candidate;
|
|
67
|
+
}
|
|
53
68
|
updated['firefox'] = firefox;
|
|
54
69
|
const validated = validateConfig(updated);
|
|
55
70
|
if (validated.firefox.sha256 !== undefined) {
|
|
@@ -58,7 +73,7 @@ export async function sourceSetCommand(projectRoot, options) {
|
|
|
58
73
|
await writeConfigDocument(projectRoot, updated);
|
|
59
74
|
return validated.firefox;
|
|
60
75
|
});
|
|
61
|
-
const archive = resolveArchive(written.version, written.product);
|
|
76
|
+
const archive = resolveArchive(written.version, written.product, written.candidate);
|
|
62
77
|
success(`Set firefox.version = ${written.version}`);
|
|
63
78
|
success(`Set firefox.product = ${written.product}`);
|
|
64
79
|
success(`Resolved source URL: ${archive.url}`);
|
|
@@ -68,6 +83,12 @@ export async function sourceSetCommand(projectRoot, options) {
|
|
|
68
83
|
else if (options.clearSha256 === true) {
|
|
69
84
|
info('Cleared firefox.sha256');
|
|
70
85
|
}
|
|
86
|
+
if (written.candidate !== undefined) {
|
|
87
|
+
success(`Set firefox.candidate = ${written.candidate}`);
|
|
88
|
+
}
|
|
89
|
+
else if (options.clearCandidate === true) {
|
|
90
|
+
info('Cleared firefox.candidate');
|
|
91
|
+
}
|
|
71
92
|
outro('');
|
|
72
93
|
}
|
|
73
94
|
/** Registers the source command on the CLI program. */
|
|
@@ -82,13 +103,17 @@ export function registerSource(program, { getProjectRoot, withErrorHandling }) {
|
|
|
82
103
|
.makeOptionMandatory())
|
|
83
104
|
.option('--sha256 <hash>', 'Pinned SHA-256 for the resolved source archive')
|
|
84
105
|
.option('--clear-sha256', 'Clear any existing pinned SHA-256')
|
|
106
|
+
.option('--candidate <buildN>', 'Release-candidate build directory (e.g. "build2")', parseSourceCandidate)
|
|
107
|
+
.option('--clear-candidate', 'Clear any existing release-candidate build directory')
|
|
85
108
|
.action(withErrorHandling(async (options) => {
|
|
86
|
-
const { product, version, sha256, clearSha256 } = options;
|
|
109
|
+
const { product, version, sha256, clearSha256, candidate, clearCandidate } = options;
|
|
87
110
|
await sourceSetCommand(getProjectRoot(), {
|
|
88
111
|
version,
|
|
89
112
|
product: parseSourceProduct(product),
|
|
90
113
|
...(sha256 !== undefined ? { sha256 } : {}),
|
|
91
114
|
...(clearSha256 !== undefined ? { clearSha256 } : {}),
|
|
115
|
+
...(candidate !== undefined ? { candidate } : {}),
|
|
116
|
+
...(clearCandidate !== undefined ? { clearCandidate } : {}),
|
|
92
117
|
});
|
|
93
118
|
}));
|
|
94
119
|
}
|
|
@@ -209,6 +209,10 @@ export function finalizeSingleRunOutcome(outcome, normalizedPaths, binaryName, p
|
|
|
209
209
|
}
|
|
210
210
|
if (outcome.verdict.realFailureLine !== undefined) {
|
|
211
211
|
info(`First real test failure: ${outcome.verdict.realFailureLine}`);
|
|
212
|
+
const blocks = formatFailureBlocks(outcome.verdict.realFailureBlocks);
|
|
213
|
+
if (blocks !== undefined) {
|
|
214
|
+
info(blocks);
|
|
215
|
+
}
|
|
212
216
|
if (outcome.verdict.secondaryHarnessSignature !== undefined) {
|
|
213
217
|
info(`Secondary harness noise also present: ${outcome.verdict.secondaryHarnessSignature.reason}; ` +
|
|
214
218
|
outcome.verdict.secondaryHarnessSignature.line);
|
|
@@ -233,6 +237,21 @@ export function diagnoseShardOutcome(outcome, path, binaryName, postRebuildConte
|
|
|
233
237
|
return undefined;
|
|
234
238
|
}
|
|
235
239
|
catch (error) {
|
|
236
|
-
|
|
240
|
+
const diagnosis = error instanceof Error ? error.message : String(error);
|
|
241
|
+
const blocks = formatFailureBlocks(outcome.verdict.realFailureBlocks);
|
|
242
|
+
return blocks !== undefined ? `${blocks}\n${diagnosis}` : diagnosis;
|
|
237
243
|
}
|
|
238
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Renders the verdict's TEST-UNEXPECTED blocks (verbatim failing lines +
|
|
247
|
+
* their assertion/diff context) for the failure summary. Returns undefined
|
|
248
|
+
* when the verdict carries none, so callers can skip the section entirely.
|
|
249
|
+
*/
|
|
250
|
+
function formatFailureBlocks(blocks) {
|
|
251
|
+
if (blocks === undefined || blocks.length === 0)
|
|
252
|
+
return undefined;
|
|
253
|
+
// The collector appends a `…(+N more …)` note when it truncated; that
|
|
254
|
+
// note is not a failure block, so it does not count toward the header.
|
|
255
|
+
const shown = blocks.filter((block) => !block.startsWith('…(')).length;
|
|
256
|
+
return `Unexpected test failures (first ${shown}):\n${blocks.join('\n\n')}`;
|
|
257
|
+
}
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
4
4
|
import { GeneralError } from '../errors/base.js';
|
|
5
|
-
import { commanderArgParser, pickDefined } from '../utils/options.js';
|
|
5
|
+
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../utils/options.js';
|
|
6
6
|
import { testCommand } from './test.js';
|
|
7
7
|
import { DEFAULT_HARNESS_RETRIES } from './test-run.js';
|
|
8
8
|
/** Registers the test command on the CLI program. */
|
|
9
9
|
export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
10
|
-
program
|
|
10
|
+
const test = program
|
|
11
11
|
.command('test [paths...]')
|
|
12
12
|
.description('Run tests via mach test')
|
|
13
13
|
.option('--headless', 'Run tests in headless mode')
|
|
14
14
|
.option('--build', 'Run incremental UI build before testing')
|
|
15
15
|
.option('--auto', 'Forward mach test --auto. Valid only when no explicit paths are provided.')
|
|
16
16
|
.option('--allow-stale-build', 'Allow tests to run even when packageable engine files changed since the last successful FireForge build')
|
|
17
|
+
.option('--allow-stale-components', 'Run tests despite components.conf changes that only a full "fireforge build" compiles in (the packaged child process will resolve the OLD StaticComponents table)')
|
|
17
18
|
.option('--kill-stale-marionette', 'Terminate a recognized stale browser process holding the Marionette port before running tests')
|
|
18
19
|
.option('--canary [path]', 'Run one short browser-chrome harness canary. Uses test.canaryPath from fireforge.json when no path is supplied.')
|
|
19
20
|
.option('--doctor', 'Run a marionette handshake preflight before tests (exit 1 on FAIL). With no paths, runs the preflight only.')
|
|
@@ -56,9 +57,9 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
56
57
|
'runs (one browser instance per argument) by default, which does',
|
|
57
58
|
'not exercise cross-argument state; --no-shard restores the',
|
|
58
59
|
'combined single-instance invocation.',
|
|
59
|
-
].join('\n'))
|
|
60
|
-
|
|
60
|
+
].join('\n'));
|
|
61
|
+
addWaitLockOption(test).action(withErrorHandling(async (paths, options) => {
|
|
61
62
|
const projectRoot = getProjectRoot();
|
|
62
|
-
await withEngineSessionLock(projectRoot, 'test', () => testCommand(projectRoot, paths, pickDefined(options)));
|
|
63
|
+
await withEngineSessionLock(projectRoot, 'test', () => testCommand(projectRoot, paths, pickDefined(options)), { waitLockSeconds: resolveWaitLockSeconds(options.waitLock) });
|
|
63
64
|
}));
|
|
64
65
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-dispatch gates for `fireforge test` that stand between the request
|
|
3
|
+
* and a run against a stale or under-packaged runtime: the packaging
|
|
4
|
+
* coverage refusal, the stale-content refusal, and the compiled
|
|
5
|
+
* StaticComponents refusal. Split out of `test.ts` so the command module
|
|
6
|
+
* stays wiring; the probes and refusal copy live in
|
|
7
|
+
* `src/core/test-stale-check.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import type { BuildBaseline } from '../core/build-baseline-types.js';
|
|
10
|
+
import type { TestOptions } from '../types/commands/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Stale-build preflight — when `--build` was NOT requested, detect
|
|
13
|
+
* packageable engine edits since the last successful build and fail
|
|
14
|
+
* UP-FRONT unless the operator explicitly accepts the stale package risk.
|
|
15
|
+
*
|
|
16
|
+
* Packaging COVERAGE is checked first, on EVERY non-`--build` run and
|
|
17
|
+
* regardless of staleness or `--allow-stale-build`: a runtime packaged by
|
|
18
|
+
* a file-scoped `test --build` can lack support fixtures for OTHER
|
|
19
|
+
* manifests even when nothing changed since — dispatching such a run
|
|
20
|
+
* hangs on missing fixtures rather than failing, so the flag (which only
|
|
21
|
+
* accepts stale CONTENT) must not be the trigger. Field incident: a
|
|
22
|
+
* three-file scoped rebuild left `file_tiles_audio.html` unpackaged and a
|
|
23
|
+
* later run over different files timed out twice at 45s waiting on
|
|
24
|
+
* `DOMAudioPlaybackStarted`.
|
|
25
|
+
*
|
|
26
|
+
* Exception: a path-less `test --doctor` stops at the Marionette health
|
|
27
|
+
* check (`runDoctorPreflight` returns 'stop' when no test paths were
|
|
28
|
+
* given) and never dispatches a test, so it needs no packaging coverage —
|
|
29
|
+
* treating it as a full-suite request would refuse a probe that touches
|
|
30
|
+
* no fixtures. The stale-content refusal still applies to it unchanged.
|
|
31
|
+
*/
|
|
32
|
+
export declare function enforceStaleBuildGate(projectRoot: string, engineDir: string, options: TestOptions, normalizedPaths: readonly string[]): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Compiled-StaticComponents gate — refuses runs whose child process would
|
|
35
|
+
* resolve a stale compiled component table. `components.conf` entries bake
|
|
36
|
+
* into compiled code that only a FULL build regenerates; a scoped
|
|
37
|
+
* `test --build` repackages the file but the failure surfaces as
|
|
38
|
+
* `NS_ERROR_MALFORMED_URI` inside the test. Applies to build-less runs
|
|
39
|
+
* (after the coverage refusal) AND to scoped `test --build` runs (before
|
|
40
|
+
* the pre-test build — that build cannot fix the table). A path-less
|
|
41
|
+
* `test --build` is exempt: its full build refreshes the anchor itself.
|
|
42
|
+
* `--allow-stale-components` downgrades the refusal to a warning.
|
|
43
|
+
*/
|
|
44
|
+
export declare function enforceStaticComponentsGate(engineDir: string, baseline: BuildBaseline | undefined, options: TestOptions): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Pre-dispatch gates for `fireforge test` that stand between the request
|
|
4
|
+
* and a run against a stale or under-packaged runtime: the packaging
|
|
5
|
+
* coverage refusal, the stale-content refusal, and the compiled
|
|
6
|
+
* StaticComponents refusal. Split out of `test.ts` so the command module
|
|
7
|
+
* stays wiring; the probes and refusal copy live in
|
|
8
|
+
* `src/core/test-stale-check.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import { checkStaleBuildForTest, checkStaticComponentsStale, findUncoveredRequestPaths, formatStaleBuildWarning, formatStaticComponentsRefusal, formatTestCoverageRefusal, } from '../core/test-stale-check.js';
|
|
11
|
+
import { GeneralError } from '../errors/base.js';
|
|
12
|
+
import { warn } from '../utils/logger.js';
|
|
13
|
+
/**
|
|
14
|
+
* Stale-build preflight — when `--build` was NOT requested, detect
|
|
15
|
+
* packageable engine edits since the last successful build and fail
|
|
16
|
+
* UP-FRONT unless the operator explicitly accepts the stale package risk.
|
|
17
|
+
*
|
|
18
|
+
* Packaging COVERAGE is checked first, on EVERY non-`--build` run and
|
|
19
|
+
* regardless of staleness or `--allow-stale-build`: a runtime packaged by
|
|
20
|
+
* a file-scoped `test --build` can lack support fixtures for OTHER
|
|
21
|
+
* manifests even when nothing changed since — dispatching such a run
|
|
22
|
+
* hangs on missing fixtures rather than failing, so the flag (which only
|
|
23
|
+
* accepts stale CONTENT) must not be the trigger. Field incident: a
|
|
24
|
+
* three-file scoped rebuild left `file_tiles_audio.html` unpackaged and a
|
|
25
|
+
* later run over different files timed out twice at 45s waiting on
|
|
26
|
+
* `DOMAudioPlaybackStarted`.
|
|
27
|
+
*
|
|
28
|
+
* Exception: a path-less `test --doctor` stops at the Marionette health
|
|
29
|
+
* check (`runDoctorPreflight` returns 'stop' when no test paths were
|
|
30
|
+
* given) and never dispatches a test, so it needs no packaging coverage —
|
|
31
|
+
* treating it as a full-suite request would refuse a probe that touches
|
|
32
|
+
* no fixtures. The stale-content refusal still applies to it unchanged.
|
|
33
|
+
*/
|
|
34
|
+
export async function enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths) {
|
|
35
|
+
const stale = await checkStaleBuildForTest(projectRoot, engineDir);
|
|
36
|
+
const dispatchesNoTests = options.doctor === true && normalizedPaths.length === 0;
|
|
37
|
+
if (!dispatchesNoTests) {
|
|
38
|
+
const recordedCoverage = stale.baseline?.testPackagingCoverage;
|
|
39
|
+
const uncovered = findUncoveredRequestPaths(recordedCoverage, normalizedPaths);
|
|
40
|
+
if (uncovered.length > 0) {
|
|
41
|
+
throw new GeneralError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : []));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
await enforceStaticComponentsGate(engineDir, stale.baseline, options);
|
|
45
|
+
const staleMessage = stale.stale
|
|
46
|
+
? `${formatStaleBuildWarning(stale)}\n\n` +
|
|
47
|
+
'Run `fireforge test --build` to refresh the packaged runtime first, or pass ' +
|
|
48
|
+
'`--allow-stale-build` if you intentionally rebuilt out-of-band and accept the risk.'
|
|
49
|
+
: undefined;
|
|
50
|
+
if (staleMessage !== undefined) {
|
|
51
|
+
if (options.allowStaleBuild === true) {
|
|
52
|
+
warn(staleMessage);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
throw new GeneralError(staleMessage);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Compiled-StaticComponents gate — refuses runs whose child process would
|
|
61
|
+
* resolve a stale compiled component table. `components.conf` entries bake
|
|
62
|
+
* into compiled code that only a FULL build regenerates; a scoped
|
|
63
|
+
* `test --build` repackages the file but the failure surfaces as
|
|
64
|
+
* `NS_ERROR_MALFORMED_URI` inside the test. Applies to build-less runs
|
|
65
|
+
* (after the coverage refusal) AND to scoped `test --build` runs (before
|
|
66
|
+
* the pre-test build — that build cannot fix the table). A path-less
|
|
67
|
+
* `test --build` is exempt: its full build refreshes the anchor itself.
|
|
68
|
+
* `--allow-stale-components` downgrades the refusal to a warning.
|
|
69
|
+
*/
|
|
70
|
+
export async function enforceStaticComponentsGate(engineDir, baseline, options) {
|
|
71
|
+
const result = await checkStaticComponentsStale(engineDir, baseline);
|
|
72
|
+
if (!result.stale) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const message = formatStaticComponentsRefusal(result.changedManifests);
|
|
76
|
+
if (options.allowStaleComponents === true) {
|
|
77
|
+
warn(message);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
throw new GeneralError(message);
|
|
81
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
-
import { readBuildBaseline } from '../core/build-baseline.js';
|
|
3
|
+
import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
|
|
4
4
|
import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
5
5
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
6
6
|
import { assertEngineGenerationUnchanged, snapshotEngineGeneration, } from '../core/engine-session-lock.js';
|
|
@@ -10,16 +10,17 @@ import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionette
|
|
|
10
10
|
import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
|
|
11
11
|
import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
12
12
|
import { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-scope.js';
|
|
13
|
-
import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
|
|
14
13
|
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
15
14
|
import { GeneralError } from '../errors/base.js';
|
|
16
15
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
16
|
+
import { toError } from '../utils/errors.js';
|
|
17
17
|
import { pathExists } from '../utils/fs.js';
|
|
18
|
-
import { info, intro, outro, spinner, success,
|
|
18
|
+
import { info, intro, outro, spinner, success, verbose } from '../utils/logger.js';
|
|
19
19
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
20
20
|
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
21
21
|
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
22
22
|
import { DEFAULT_HARNESS_RETRIES, runShardedTests, runTestsWithRetries, } from './test-run.js';
|
|
23
|
+
import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
|
|
23
24
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
24
25
|
const missingPaths = [];
|
|
25
26
|
for (const testPath of testPaths) {
|
|
@@ -103,7 +104,7 @@ async function resolveLaunchablePathForTests(engineDir, binaryName, objDir) {
|
|
|
103
104
|
}
|
|
104
105
|
return bundleCheck.expectedPath;
|
|
105
106
|
}
|
|
106
|
-
async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries) {
|
|
107
|
+
async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage) {
|
|
107
108
|
await withBuildLock(projectRoot, async () => {
|
|
108
109
|
// Pass the previous baseline exactly like `fireforge build` does, so
|
|
109
110
|
// auto-configure runs under the same conditions on both paths. The
|
|
@@ -128,6 +129,22 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
|
|
|
128
129
|
});
|
|
129
130
|
if (result.exitCode === 0) {
|
|
130
131
|
s.stop('Build complete');
|
|
132
|
+
// Same record, same failure tolerance as `fireforge build` /
|
|
133
|
+
// `build --ui` (build.ts): a green pre-test build refreshes the
|
|
134
|
+
// stale-build baseline so a later plain `fireforge test` over the
|
|
135
|
+
// same files — in any invocation shape — is not refused. A failed
|
|
136
|
+
// write never fails the run. The coverage claim is scoped to the
|
|
137
|
+
// requested test paths, since a file-scoped `test --build` only
|
|
138
|
+
// guarantees packaging for those manifests. The previous baseline is
|
|
139
|
+
// passed through so a scoped write carries the static-components
|
|
140
|
+
// anchor forward (a `mach build faster` does not rebake
|
|
141
|
+
// components.conf into the compiled table).
|
|
142
|
+
try {
|
|
143
|
+
await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage, previousBaseline);
|
|
144
|
+
}
|
|
145
|
+
catch (baselineError) {
|
|
146
|
+
verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
|
|
147
|
+
}
|
|
131
148
|
return;
|
|
132
149
|
}
|
|
133
150
|
s.error('Pre-test build failed');
|
|
@@ -297,28 +314,30 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
297
314
|
// during spawn (exit code 1, signal none). stderr tail: (empty)`.
|
|
298
315
|
const launchablePath = await resolveLaunchablePathForTests(paths.engine, projectConfig.binaryName, buildCheck.objDir);
|
|
299
316
|
const harnessRetries = options.harnessRetries ?? DEFAULT_HARNESS_RETRIES;
|
|
317
|
+
// Normalized engine-relative request paths, hoisted above the build/stale
|
|
318
|
+
// gate: the pre-test build records them as the packaging-coverage claim,
|
|
319
|
+
// and the --allow-stale-build path checks the request against the
|
|
320
|
+
// recorded coverage. (Existence is still asserted later, after the gate —
|
|
321
|
+
// stale/coverage refusals keep precedence over missing-path errors.)
|
|
322
|
+
const requestedPaths = canaryPath !== undefined ? [canaryPath] : testPaths;
|
|
323
|
+
const normalizedPaths = requestedPaths.map((p) => stripEnginePrefix(p).trim());
|
|
300
324
|
// Run incremental build if requested
|
|
301
325
|
if (options.build) {
|
|
302
|
-
|
|
326
|
+
// A path-less `test --build` runs (and packages for) the full suite;
|
|
327
|
+
// a scoped invocation only vouches for the requested paths. A SCOPED
|
|
328
|
+
// rebuild also cannot regenerate the compiled StaticComponents table,
|
|
329
|
+
// so it runs the components.conf gate up-front (the path-less shape
|
|
330
|
+
// refreshes the anchor itself and skips it).
|
|
331
|
+
const coverage = normalizedPaths.length === 0 ? 'full' : normalizedPaths;
|
|
332
|
+
if (normalizedPaths.length > 0) {
|
|
333
|
+
const previousBaseline = await readBuildBaseline(projectRoot);
|
|
334
|
+
await enforceStaticComponentsGate(paths.engine, previousBaseline, options);
|
|
335
|
+
}
|
|
336
|
+
await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, coverage);
|
|
303
337
|
info('');
|
|
304
338
|
}
|
|
305
339
|
else {
|
|
306
|
-
|
|
307
|
-
// packageable engine edits since the last successful `fireforge build`
|
|
308
|
-
// and fail UP-FRONT unless the operator explicitly accepts the stale
|
|
309
|
-
// package risk.
|
|
310
|
-
const stale = await checkStaleBuildForTest(projectRoot, paths.engine);
|
|
311
|
-
if (stale.stale) {
|
|
312
|
-
const message = `${formatStaleBuildWarning(stale)}\n\n` +
|
|
313
|
-
'Run `fireforge test --build` to refresh the packaged runtime first, or pass ' +
|
|
314
|
-
'`--allow-stale-build` if you intentionally rebuilt out-of-band and accept the risk.';
|
|
315
|
-
if (options.allowStaleBuild === true) {
|
|
316
|
-
warn(message);
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
throw new GeneralError(message);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
340
|
+
await enforceStaleBuildGate(projectRoot, paths.engine, options, normalizedPaths);
|
|
322
341
|
}
|
|
323
342
|
// Resolve the effective Marionette port. Operator precedence:
|
|
324
343
|
// 1. `--marionette-port` (first-class option, parsed at the CLI layer)
|
|
@@ -355,8 +374,6 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
355
374
|
if (doctorOutcome === 'stop')
|
|
356
375
|
return;
|
|
357
376
|
}
|
|
358
|
-
const requestedPaths = canaryPath !== undefined ? [canaryPath] : testPaths;
|
|
359
|
-
const normalizedPaths = requestedPaths.map((p) => stripEnginePrefix(p).trim());
|
|
360
377
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
361
378
|
const classification = await classifyTestHarnesses(paths.engine, normalizedPaths);
|
|
362
379
|
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
@@ -36,6 +36,18 @@ export interface AuditSummary {
|
|
|
36
36
|
* @returns True when the path implies packaging.
|
|
37
37
|
*/
|
|
38
38
|
export declare function isPackageablePath(sourcePath: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Decides whether a source path is an XPCOM static-component manifest —
|
|
41
|
+
* i.e. a file whose registrations are baked into the compiled
|
|
42
|
+
* StaticComponents table at FULL-build time rather than packaged into
|
|
43
|
+
* `dist/`. This basename check is the single extension point for that
|
|
44
|
+
* classification; parsing `moz.build` `XPCOM_MANIFESTS` entries to follow
|
|
45
|
+
* renamed manifests is out of scope.
|
|
46
|
+
*
|
|
47
|
+
* @param sourcePath Engine-relative POSIX path.
|
|
48
|
+
* @returns True when the path is a `components.conf` manifest.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isXpcomManifestPath(sourcePath: string): boolean;
|
|
39
51
|
/**
|
|
40
52
|
* Runs the post-build audit. Emits per-file warnings for missing or
|
|
41
53
|
* stale artifacts and a summary info line at the end. Always returns
|
|
@@ -111,6 +111,20 @@ export function isPackageablePath(sourcePath) {
|
|
|
111
111
|
}
|
|
112
112
|
return false;
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Decides whether a source path is an XPCOM static-component manifest —
|
|
116
|
+
* i.e. a file whose registrations are baked into the compiled
|
|
117
|
+
* StaticComponents table at FULL-build time rather than packaged into
|
|
118
|
+
* `dist/`. This basename check is the single extension point for that
|
|
119
|
+
* classification; parsing `moz.build` `XPCOM_MANIFESTS` entries to follow
|
|
120
|
+
* renamed manifests is out of scope.
|
|
121
|
+
*
|
|
122
|
+
* @param sourcePath Engine-relative POSIX path.
|
|
123
|
+
* @returns True when the path is a `components.conf` manifest.
|
|
124
|
+
*/
|
|
125
|
+
export function isXpcomManifestPath(sourcePath) {
|
|
126
|
+
return basename(sourcePath.replace(/\\/g, '/')) === 'components.conf';
|
|
127
|
+
}
|
|
114
128
|
/*
|
|
115
129
|
* Finds the unique obj-star directory with a dist subtree, or undefined
|
|
116
130
|
* when zero or multiple match. The ambiguous case is already rejected
|