@hominis/fireforge 0.52.0 → 0.54.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 +45 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/bootstrap.js +2 -3
- package/dist/src/commands/build-prelock.d.ts +3 -3
- package/dist/src/commands/build-prelock.js +3 -3
- package/dist/src/commands/build.js +4 -0
- package/dist/src/commands/declarations-report.d.ts +9 -3
- package/dist/src/commands/declarations-report.js +18 -11
- package/dist/src/commands/discard.js +3 -3
- package/dist/src/commands/doctor-furnace-config-sync.js +4 -4
- package/dist/src/commands/doctor-furnace.js +5 -5
- package/dist/src/commands/export-adopt-check.d.ts +5 -6
- package/dist/src/commands/export-adopt-check.js +5 -6
- package/dist/src/commands/export-flow.js +6 -9
- package/dist/src/commands/export-staged-registration.d.ts +4 -4
- package/dist/src/commands/export-staged-registration.js +4 -4
- package/dist/src/commands/forward-import-remedy.d.ts +2 -3
- package/dist/src/commands/forward-import-remedy.js +2 -3
- package/dist/src/commands/furnace/create-dry-run.d.ts +2 -2
- package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +2 -2
- package/dist/src/commands/furnace/deploy-fragment-includers.js +2 -2
- package/dist/src/commands/furnace/index.js +2 -2
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/patch/compact.js +1 -1
- package/dist/src/commands/patch/lint-ignore-from.d.ts +3 -4
- package/dist/src/commands/patch/lint-ignore-from.js +3 -4
- package/dist/src/commands/patch/move-files-adopt.d.ts +33 -0
- package/dist/src/commands/patch/move-files-adopt.js +65 -0
- package/dist/src/commands/patch/move-files-create.js +41 -20
- package/dist/src/commands/patch/move-files-into.d.ts +18 -0
- package/dist/src/commands/patch/move-files-into.js +125 -60
- package/dist/src/commands/patch/move-files-retire.d.ts +39 -0
- package/dist/src/commands/patch/move-files-retire.js +91 -0
- package/dist/src/commands/patch/move-files.d.ts +3 -0
- package/dist/src/commands/patch/move-files.js +42 -12
- package/dist/src/commands/patch/move-size-tier.d.ts +27 -0
- package/dist/src/commands/patch/move-size-tier.js +66 -0
- package/dist/src/commands/patch/rename.js +1 -1
- package/dist/src/commands/patch/reorder-projection.d.ts +17 -0
- package/dist/src/commands/patch/reorder-projection.js +49 -0
- package/dist/src/commands/patch/reorder-shift.d.ts +30 -0
- package/dist/src/commands/patch/reorder-shift.js +307 -0
- package/dist/src/commands/patch/reorder.d.ts +9 -15
- package/dist/src/commands/patch/reorder.js +52 -50
- package/dist/src/commands/patch/split-plan.d.ts +31 -3
- package/dist/src/commands/patch/split-plan.js +36 -12
- package/dist/src/commands/patch/split-staged.d.ts +2 -2
- package/dist/src/commands/patch/split-staged.js +2 -2
- package/dist/src/commands/patch/split.js +18 -5
- package/dist/src/commands/patch/staged-dependency-entries.d.ts +9 -1
- package/dist/src/commands/patch/staged-dependency-entries.js +32 -4
- package/dist/src/commands/patch/staged-dependency-from.js +15 -9
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +26 -4
- package/dist/src/commands/patch/staged-dependency-remove.js +60 -23
- package/dist/src/commands/patch/staged-dependency.js +35 -24
- package/dist/src/commands/patch/tier.js +2 -2
- package/dist/src/commands/patch-rename-map.d.ts +14 -3
- package/dist/src/commands/patch-rename-map.js +21 -3
- package/dist/src/commands/re-export-adjacent.d.ts +4 -5
- package/dist/src/commands/re-export-lint.d.ts +4 -3
- package/dist/src/commands/re-export-lint.js +4 -3
- package/dist/src/commands/re-export-options.js +8 -2
- package/dist/src/commands/re-export-preflight.js +1 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +1 -1
- package/dist/src/commands/re-export-purity-scope.js +13 -7
- package/dist/src/commands/re-export-retired-declarations.d.ts +22 -0
- package/dist/src/commands/re-export-retired-declarations.js +113 -0
- package/dist/src/commands/re-export-single.js +2 -2
- package/dist/src/commands/re-export-stale-registrations.d.ts +27 -6
- package/dist/src/commands/re-export-stale-registrations.js +65 -21
- package/dist/src/commands/re-export.js +4 -0
- package/dist/src/commands/rebase/continue.js +3 -3
- package/dist/src/commands/register.js +5 -4
- package/dist/src/commands/run.js +11 -4
- package/dist/src/commands/status.js +1 -1
- package/dist/src/commands/test-build-furnace.d.ts +1 -2
- package/dist/src/commands/test-build-furnace.js +1 -2
- package/dist/src/commands/test-build-phase.d.ts +8 -0
- package/dist/src/commands/test-build-phase.js +42 -7
- package/dist/src/commands/test-control-restore.d.ts +40 -0
- package/dist/src/commands/test-control-restore.js +86 -0
- package/dist/src/commands/test-dispatch.js +68 -7
- package/dist/src/commands/test-multi-verdict.js +1 -0
- package/dist/src/commands/test-orchestrate.d.ts +6 -0
- package/dist/src/commands/test-orchestrate.js +32 -1
- package/dist/src/commands/test-perf-scope.d.ts +1 -1
- package/dist/src/commands/test-perf-scope.js +1 -1
- package/dist/src/commands/test-prelock.d.ts +18 -8
- package/dist/src/commands/test-prelock.js +42 -14
- package/dist/src/commands/test-register.js +4 -3
- package/dist/src/commands/test-results.d.ts +10 -1
- package/dist/src/commands/test-results.js +20 -3
- package/dist/src/commands/test-run.d.ts +6 -0
- package/dist/src/commands/test-run.js +23 -4
- package/dist/src/commands/test-shards.d.ts +7 -3
- package/dist/src/commands/test-shards.js +24 -7
- package/dist/src/commands/test-stale-gate.d.ts +6 -1
- package/dist/src/commands/test-stale-gate.js +40 -2
- package/dist/src/commands/test-verdict.d.ts +5 -3
- package/dist/src/commands/test.js +14 -2
- package/dist/src/commands/token.js +2 -2
- package/dist/src/commands/verify.js +1 -1
- package/dist/src/commands/watch.js +1 -1
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/build-audit-resolve.d.ts +4 -5
- package/dist/src/core/build-audit-resolve.js +4 -5
- package/dist/src/core/build-audit-skip.d.ts +2 -2
- package/dist/src/core/build-audit.d.ts +3 -4
- package/dist/src/core/build-baseline-types.d.ts +24 -3
- package/dist/src/core/build-baseline-validate.d.ts +30 -1
- package/dist/src/core/build-baseline-validate.js +127 -35
- package/dist/src/core/build-baseline.d.ts +8 -2
- package/dist/src/core/build-baseline.js +25 -16
- package/dist/src/core/build-jar-escalation.d.ts +4 -4
- package/dist/src/core/build-jar-escalation.js +4 -4
- package/dist/src/core/build-prepare.js +4 -4
- package/dist/src/core/config-validate.js +4 -4
- package/dist/src/core/coverage-extend.d.ts +5 -3
- package/dist/src/core/coverage-extend.js +8 -3
- package/dist/src/core/coverage-records.d.ts +4 -5
- package/dist/src/core/coverage-records.js +4 -5
- package/dist/src/core/destructive.d.ts +2 -2
- package/dist/src/core/destructive.js +2 -2
- package/dist/src/core/discard-baseline.d.ts +2 -2
- package/dist/src/core/discard-baseline.js +3 -3
- package/dist/src/core/engine-session-lock.d.ts +1 -1
- package/dist/src/core/engine-session-lock.js +1 -1
- package/dist/src/core/file-lock-owner.d.ts +5 -5
- package/dist/src/core/file-lock-owner.js +5 -5
- package/dist/src/core/file-lock-queue.js +6 -8
- package/dist/src/core/file-lock-shared.d.ts +4 -4
- package/dist/src/core/file-lock-shared.js +4 -4
- package/dist/src/core/file-lock.js +7 -10
- package/dist/src/core/firefox.js +2 -2
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +1 -1
- package/dist/src/core/furnace-apply-overwrite-warn.js +1 -1
- package/dist/src/core/furnace-apply.js +3 -3
- package/dist/src/core/furnace-operation.js +5 -5
- package/dist/src/core/furnace-registration-jar-inplace.js +5 -6
- package/dist/src/core/furnace-registration-rename.d.ts +5 -5
- package/dist/src/core/furnace-registration-rename.js +5 -5
- package/dist/src/core/furnace-registration-validate.d.ts +2 -1
- package/dist/src/core/furnace-registration-validate.js +2 -1
- package/dist/src/core/furnace-registration.js +2 -2
- package/dist/src/core/git-base.js +4 -3
- package/dist/src/core/git-diff.js +1 -1
- package/dist/src/core/git.js +1 -1
- package/dist/src/core/harness-orphans.d.ts +6 -6
- package/dist/src/core/harness-orphans.js +6 -6
- package/dist/src/core/harness-port-reservation.d.ts +60 -0
- package/dist/src/core/harness-port-reservation.js +122 -0
- package/dist/src/core/history-log-file.d.ts +9 -9
- package/dist/src/core/history-log-file.js +15 -15
- package/dist/src/core/history-log.d.ts +3 -3
- package/dist/src/core/history-log.js +3 -3
- package/dist/src/core/launchable-build.d.ts +39 -0
- package/dist/src/core/launchable-build.js +85 -0
- package/dist/src/core/marionette-port.js +1 -3
- package/dist/src/core/moz-manifest-rules.js +3 -2
- package/dist/src/core/negative-control-compiled.d.ts +13 -3
- package/dist/src/core/negative-control-compiled.js +27 -5
- package/dist/src/core/negative-control-diff.js +31 -6
- package/dist/src/core/parser-fallback.d.ts +5 -5
- package/dist/src/core/parser-fallback.js +5 -5
- package/dist/src/core/patch-identifier-suggest.d.ts +6 -0
- package/dist/src/core/patch-identifier-suggest.js +11 -4
- package/dist/src/core/patch-lint-checkjs.d.ts +1 -1
- package/dist/src/core/patch-lint-checkjs.js +1 -1
- package/dist/src/core/patch-lint-css.js +2 -2
- package/dist/src/core/patch-lint-file-size.d.ts +5 -7
- package/dist/src/core/patch-lint-file-size.js +5 -7
- package/dist/src/core/patch-lint-ignore.d.ts +3 -3
- package/dist/src/core/patch-lint-ignore.js +3 -3
- package/dist/src/core/patch-lint-prettier.d.ts +4 -6
- package/dist/src/core/patch-lint-prettier.js +4 -6
- package/dist/src/core/patch-lint-projection.d.ts +1 -1
- package/dist/src/core/patch-lint-projection.js +1 -1
- package/dist/src/core/patch-lint-queue-types.d.ts +2 -2
- package/dist/src/core/patch-lint-queue-types.js +2 -2
- package/dist/src/core/patch-manifest-consistency.d.ts +2 -2
- package/dist/src/core/patch-manifest-consistency.js +2 -2
- package/dist/src/core/patch-manifest-io.d.ts +5 -1
- package/dist/src/core/patch-manifest-io.js +12 -10
- package/dist/src/core/patch-manifest-key-order.d.ts +3 -4
- package/dist/src/core/patch-manifest-key-order.js +3 -4
- package/dist/src/core/patch-parse.d.ts +3 -3
- package/dist/src/core/patch-parse.js +3 -3
- package/dist/src/core/patch-policy.d.ts +1 -1
- package/dist/src/core/patch-policy.js +1 -1
- package/dist/src/core/patch-source-metadata.d.ts +1 -1
- package/dist/src/core/patch-source-metadata.js +1 -1
- package/dist/src/core/patch-subset.d.ts +5 -3
- package/dist/src/core/patch-subset.js +8 -7
- package/dist/src/core/patch-transform.d.ts +2 -2
- package/dist/src/core/patch-transform.js +2 -2
- package/dist/src/core/rebase-session.js +4 -6
- package/dist/src/core/register-xpcshell-test.d.ts +28 -7
- package/dist/src/core/register-xpcshell-test.js +76 -27
- package/dist/src/core/run-log.d.ts +8 -8
- package/dist/src/core/run-log.js +8 -8
- package/dist/src/core/status-classify.d.ts +1 -2
- package/dist/src/core/status-classify.js +1 -2
- package/dist/src/core/test-harness-crash.d.ts +5 -3
- package/dist/src/core/test-harness-crash.js +13 -14
- package/dist/src/core/test-harness-infra.d.ts +32 -0
- package/dist/src/core/test-harness-infra.js +69 -0
- package/dist/src/core/test-harness-signature.d.ts +6 -0
- package/dist/src/core/test-stale-check.d.ts +38 -0
- package/dist/src/core/test-stale-check.js +82 -1
- package/dist/src/core/test-stall-triage.js +5 -7
- package/dist/src/core/token-category.js +2 -2
- package/dist/src/core/token-inventory.js +3 -3
- package/dist/src/core/token-manager.d.ts +2 -2
- package/dist/src/core/token-manager.js +5 -7
- package/dist/src/core/token-variant.d.ts +6 -6
- package/dist/src/core/token-variant.js +7 -7
- package/dist/src/core/typecheck-shim.js +1 -1
- package/dist/src/core/wire-destroy.d.ts +1 -1
- package/dist/src/core/wire-destroy.js +1 -1
- package/dist/src/core/wire-dom-fragment.js +1 -1
- package/dist/src/core/wire-init.d.ts +1 -1
- package/dist/src/core/wire-init.js +1 -1
- package/dist/src/core/wire-subscript.d.ts +1 -1
- package/dist/src/core/wire-subscript.js +1 -1
- package/dist/src/core/wire-utils.d.ts +2 -2
- package/dist/src/core/wire-utils.js +3 -3
- package/dist/src/errors/base.d.ts +1 -1
- package/dist/src/errors/base.js +1 -1
- package/dist/src/errors/rebase.d.ts +1 -1
- package/dist/src/errors/rebase.js +1 -1
- package/dist/src/types/commands/options.d.ts +38 -3
- package/dist/src/types/config.d.ts +5 -7
- package/dist/src/utils/options.js +2 -2
- package/dist/src/utils/process.js +2 -2
- package/dist/src/utils/regex.d.ts +2 -2
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/sleep.d.ts +5 -6
- package/dist/src/utils/sleep.js +5 -6
- package/dist/src/utils/validation.js +2 -2
- package/docs/configuration.md +28 -23
- package/docs/exit-codes.md +8 -11
- package/docs/furnace.md +3 -3
- package/docs/lifecycle-invariants.md +36 -45
- package/docs/machine-output.md +41 -24
- package/docs/patch-workflow.md +140 -29
- package/docs/run-logs.md +8 -9
- package/docs/testing.md +114 -48
- package/package.json +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { HarnessRunVerdict } from '../core/test-harness-crash.js';
|
|
2
2
|
/**
|
|
3
3
|
* Reason codes the emission layer can put on a `FIREFORGE-VERDICT: FAIL`
|
|
4
|
-
* line. The first three mirror the harness classifier
|
|
4
|
+
* line. The first three mirror the harness classifier, as does
|
|
5
|
+
* `server-startup`: a harness-crash-class infrastructure failure where a
|
|
6
|
+
* mochitest server never came up, so no test ran. `preflight` covers
|
|
5
7
|
* every failure before harness classification (missing/stale builds,
|
|
6
8
|
* invalid paths, port conflicts, config errors, spawn failures),
|
|
7
9
|
* `inconclusive` marks a run whose harness result exists but cannot be
|
|
@@ -11,9 +13,9 @@ import type { HarnessRunVerdict } from '../core/test-harness-crash.js';
|
|
|
11
13
|
* and `killed` marks a run terminated by a signal before it could reach any
|
|
12
14
|
* of the others.
|
|
13
15
|
*/
|
|
14
|
-
export type FireforgeVerdictReason = 'crash' | 'harness-arguments' | 'no-tests' | 'test-failures' | 'preflight' | 'inconclusive' | 'lock-timeout' | 'killed';
|
|
16
|
+
export type FireforgeVerdictReason = 'crash' | 'harness-arguments' | 'no-tests' | 'test-failures' | 'server-startup' | 'preflight' | 'inconclusive' | 'lock-timeout' | 'killed';
|
|
15
17
|
/** Additive keys a run can stamp on its verdict line. */
|
|
16
|
-
export type VerdictRunAttributeKey = 'shuffle' | 'orphans-reaped' | 'host-load' | 'host-cpu-warning' | 'power-source' | 'power-changed' | 'furnace-applied' | 'furnace-apply' | 'furnace-skipped' | 'control-build' | 'drift-tolerated' | 'lock' | 'jobs' | 'results';
|
|
18
|
+
export type VerdictRunAttributeKey = 'shuffle' | 'orphans-reaped' | 'host-load' | 'host-cpu-warning' | 'power-source' | 'power-changed' | 'furnace-applied' | 'furnace-apply' | 'furnace-skipped' | 'control-build' | 'control-restore-build' | 'build-id' | 'drift-tolerated' | 'lock' | 'jobs' | 'upload-dirs' | 'results';
|
|
17
19
|
/** Records an additive attribute for this run's verdict line. */
|
|
18
20
|
export declare function setVerdictRunAttribute(key: VerdictRunAttributeKey, value: string): void;
|
|
19
21
|
/**
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
4
4
|
import { assertEngineExists } from '../core/engine-precondition.js';
|
|
5
|
+
import { readBuildId } from '../core/launchable-build.js';
|
|
5
6
|
import { hasBuildArtifacts } from '../core/mach.js';
|
|
6
7
|
import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
|
|
7
8
|
import { ensureLaunchableBrowserNotRunning } from '../core/marionette-port.js';
|
|
@@ -24,7 +25,7 @@ import { buildPerfSampleEnv, mergeHarnessEnv, resolveShuffleSeed } from './test-
|
|
|
24
25
|
import { removePgidFile, setActivePgidFile } from './test-harness-teardown.js';
|
|
25
26
|
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, resolveCanaryPath, restartBetweenTestsArgs, } from './test-modes.js';
|
|
26
27
|
import { orchestrateTestRuns } from './test-orchestrate.js';
|
|
27
|
-
import { resolveLaunchablePathForTests, runTestPreLockPreflight, TEST_BUILD_PREFLIGHT, } from './test-prelock.js';
|
|
28
|
+
import { rearmSpentWaits, resolveLaunchablePathForTests, runTestPreLockPreflight, TEST_BUILD_PREFLIGHT, } from './test-prelock.js';
|
|
28
29
|
import { resetTestResults, withRawLogDir, writeTestResults } from './test-results.js';
|
|
29
30
|
import { DEFAULT_HARNESS_RETRIES } from './test-run.js';
|
|
30
31
|
import { emitFailVerdict, resetVerdictEmission, setVerdictRunAttribute, verdictEmitted, } from './test-verdict.js';
|
|
@@ -349,10 +350,21 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
|
|
|
349
350
|
classification,
|
|
350
351
|
normalizedPaths: hooks.coveragePaths ?? normalizedPaths,
|
|
351
352
|
...(hooks.fullBuildReason === undefined ? {} : { fullBuildReason: hooks.fullBuildReason }),
|
|
353
|
+
...(hooks.controlBuild === undefined ? {} : { controlBuild: hooks.controlBuild }),
|
|
354
|
+
launchablePath,
|
|
352
355
|
})) {
|
|
353
356
|
return;
|
|
354
357
|
}
|
|
355
|
-
|
|
358
|
+
// Which binary this run exercises, read after the pre-test build so it
|
|
359
|
+
// names the build the tests actually ran against (`moz:buildID`).
|
|
360
|
+
if (launchablePath !== undefined) {
|
|
361
|
+
const buildId = await readBuildId(paths.engine, launchablePath);
|
|
362
|
+
if (buildId !== undefined)
|
|
363
|
+
setVerdictRunAttribute('build-id', buildId);
|
|
364
|
+
}
|
|
365
|
+
// After a pre-test build, a busy browser or port is a peer that started
|
|
366
|
+
// during it; waiting it out keeps the build this run just paid for.
|
|
367
|
+
const { forwardedPort, effectivePort } = await ensureTestBrowserEnvironment(paths.engine, launchablePath, xpcshellOnly, projectConfig, options.build === true ? rearmSpentWaits(options) : options, buildCheck.objDir);
|
|
356
368
|
if (options.doctor) {
|
|
357
369
|
const doctorOutcome = await runDoctorPreflight({
|
|
358
370
|
engineDir: paths.engine,
|
|
@@ -42,8 +42,8 @@ async function normalizeTokenNameForProject(projectRoot, rawTokenName) {
|
|
|
42
42
|
*
|
|
43
43
|
* A variant skip has no category to name, because the declaration lives in
|
|
44
44
|
* a `:root<selector>` block, so the location reads as that block. The
|
|
45
|
-
*
|
|
46
|
-
*
|
|
45
|
+
* location matters: without it a re-run meant to change a value exits 0
|
|
46
|
+
* having silently changed nothing.
|
|
47
47
|
*
|
|
48
48
|
* @param tokenName - Full token name including prefix
|
|
49
49
|
* @param result - Result carrying the skip location, when known
|
|
@@ -328,7 +328,7 @@ export function registerVerify(program, { getProjectRoot, withErrorHandling }) {
|
|
|
328
328
|
program
|
|
329
329
|
.command('verify')
|
|
330
330
|
.description('Read-only fsck for the patch queue (manifest + cross-patch lint)')
|
|
331
|
-
.option('--patches <names...>', 'Judge only findings that name these patches (filename, stem, order number, manifest name, or slug; repeat or
|
|
331
|
+
.option('--patches <names...>', 'Judge only findings that name these patches (filename, stem, order number, manifest name, or slug; repeat, or separate with commas or whitespace). Queue-wide findings stay; unowned worktree changes drop out')
|
|
332
332
|
.action(withErrorHandling(async (options) => {
|
|
333
333
|
await verifyCommand(getProjectRoot(), options);
|
|
334
334
|
}));
|
|
@@ -195,7 +195,7 @@ export async function watchCommand(projectRoot) {
|
|
|
195
195
|
catch (error) {
|
|
196
196
|
throw new BuildError('Watch process failed to start', 'mach watch', error instanceof Error ? error : undefined);
|
|
197
197
|
}
|
|
198
|
-
// On Ctrl+C the bin signal handler
|
|
198
|
+
// On Ctrl+C the bin signal handler waits for the mach child to shut
|
|
199
199
|
// down (waitForActiveChildShutdown) before exiting, so this continuation
|
|
200
200
|
// can run and whitelist the SIGINT exit code, though the bin may still
|
|
201
201
|
// win the race and terminate first. Both outcomes are acceptable.
|
|
@@ -208,7 +208,7 @@ function buildWireSubscriptOptions(options, domFilePath, domTargetPath, subscrip
|
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
/**
|
|
211
|
-
* Surfaces any
|
|
211
|
+
* Surfaces any regex parser fallbacks the wiring run consumed, so the
|
|
212
212
|
* operator knows which files were mutated by the regex path rather than
|
|
213
213
|
* the AST path.
|
|
214
214
|
*/
|
|
@@ -29,11 +29,10 @@ export declare function countTrailingSegmentMatches(a: string, b: string): numbe
|
|
|
29
29
|
*
|
|
30
30
|
* Shared on purpose: this set governs two halves of one decision,
|
|
31
31
|
* `scoreCandidate` (selection) and `isConfidentMatch` (confirmation, in
|
|
32
|
-
* `build-audit.ts`).
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* automatic.
|
|
32
|
+
* `build-audit.ts`). Separate copies would disagree, so a source path
|
|
33
|
+
* could earn a selection bonus for a segment that confirmation then
|
|
34
|
+
* rejected as meaningless, leaving the chosen artifact unconfirmable. One
|
|
35
|
+
* set makes every addition apply to both halves.
|
|
37
36
|
*/
|
|
38
37
|
export declare const GENERIC_PATH_SEGMENTS: ReadonlySet<string>;
|
|
39
38
|
/**
|
|
@@ -96,11 +96,10 @@ export function countTrailingSegmentMatches(a, b) {
|
|
|
96
96
|
*
|
|
97
97
|
* Shared on purpose: this set governs two halves of one decision,
|
|
98
98
|
* `scoreCandidate` (selection) and `isConfidentMatch` (confirmation, in
|
|
99
|
-
* `build-audit.ts`).
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* automatic.
|
|
99
|
+
* `build-audit.ts`). Separate copies would disagree, so a source path
|
|
100
|
+
* could earn a selection bonus for a segment that confirmation then
|
|
101
|
+
* rejected as meaningless, leaving the chosen artifact unconfirmable. One
|
|
102
|
+
* set makes every addition apply to both halves.
|
|
104
103
|
*/
|
|
105
104
|
export const GENERIC_PATH_SEGMENTS = new Set([
|
|
106
105
|
'content',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Why a changed engine path was not audited.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Each reason is structured so summaries and machine output can count
|
|
5
|
+
* skips by cause instead of reading verbose log strings.
|
|
6
6
|
*/
|
|
7
7
|
export type AuditSkipReason = 'not-packageable' | 'platform-gated' | 'platform-gated-ancestor' | 'tests-not-packaged' | 'source-unreadable' | 'branding-not-selected' | 'storybook-story' | 'declared-unpackaged';
|
|
8
8
|
/** Render order for the `Packaged:` summary's per-class breakdown. */
|
|
@@ -40,10 +40,9 @@ export interface AuditEntry {
|
|
|
40
40
|
status: 'updated' | 'stale' | 'missing' | 'skipped';
|
|
41
41
|
/**
|
|
42
42
|
* Why a `skipped` entry was skipped. Present only for `skipped`. The
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* file an operator must act on.
|
|
43
|
+
* reason lets the counts distinguish a structural non-input from an
|
|
44
|
+
* unregistered file an operator must act on, so the summary line never
|
|
45
|
+
* reports skips as misses.
|
|
47
46
|
*/
|
|
48
47
|
skipReason?: AuditSkipReason;
|
|
49
48
|
}
|
|
@@ -93,10 +93,11 @@ export interface BuildBaseline {
|
|
|
93
93
|
* mozconfig and complete testInputFingerprints outside the rebuilt scope
|
|
94
94
|
* remain anchored. Explicitly rebuilt paths may change. A failed anchor
|
|
95
95
|
* replaces coverage with a dropped-path notice, or refuses when the caller
|
|
96
|
-
* required --extend-coverage.
|
|
97
|
-
* coverage because they cannot prove test scripts and
|
|
96
|
+
* required --extend-coverage. Anchors that fingerprint only packageable
|
|
97
|
+
* inputs cannot retain coverage because they cannot prove test scripts and
|
|
98
|
+
* fixtures unchanged.
|
|
98
99
|
*
|
|
99
|
-
*
|
|
100
|
+
* Retention is decided per staged path: see
|
|
100
101
|
* {@link BuildBaseline.testCoverageRecords} and
|
|
101
102
|
* {@link BuildBaseline.fullCoverageAnchor}.
|
|
102
103
|
*/
|
|
@@ -151,6 +152,26 @@ export interface BuildBaseline {
|
|
|
151
152
|
* session's scoped build replaced can tell whose build that was.
|
|
152
153
|
*/
|
|
153
154
|
recorder?: BaselineRecorder;
|
|
155
|
+
/**
|
|
156
|
+
* Set when this build compiled a negative control into the binary
|
|
157
|
+
* (`test --negative-control --build` on compiled source). The source is
|
|
158
|
+
* restored byte-exact after the run, but the objdir binary keeps the
|
|
159
|
+
* mutation until a build recompiles it. FireForge runs that restore build
|
|
160
|
+
* itself, inside the same lock hold, and its baseline drops the stamp.
|
|
161
|
+
* A stamp still present (the run was killed, or the restore build
|
|
162
|
+
* failed) makes the next `fireforge test` refuse with `control-built`
|
|
163
|
+
* until `fireforge build` replaces the baseline.
|
|
164
|
+
*/
|
|
165
|
+
controlBuild?: ControlBuildStamp;
|
|
166
|
+
}
|
|
167
|
+
/** See {@link BuildBaseline.controlBuild}. */
|
|
168
|
+
export interface ControlBuildStamp {
|
|
169
|
+
/** The `--negative-control` diff file, as given. */
|
|
170
|
+
diffFile: string;
|
|
171
|
+
/** Engine-relative compiled targets the control edited. */
|
|
172
|
+
targets: string[];
|
|
173
|
+
/** ISO-8601 time the control was applied. */
|
|
174
|
+
appliedAt: string;
|
|
154
175
|
}
|
|
155
176
|
/** See {@link BuildBaseline.recorder}. */
|
|
156
177
|
export interface BaselineRecorder {
|
|
@@ -1,3 +1,32 @@
|
|
|
1
1
|
import { type BuildBaseline } from './build-baseline-types.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The first field of a persisted baseline that fails validation, and the
|
|
4
|
+
* offending value: what a refusal names so the operator can tell a scoped
|
|
5
|
+
* coverage claim from a corrupt marker.
|
|
6
|
+
*/
|
|
7
|
+
export interface BuildBaselineProblem {
|
|
8
|
+
/** Field path, e.g. `testPackagingCoverage[2]` or `testCoverageRecords["a/"]`. */
|
|
9
|
+
field: string;
|
|
10
|
+
/** The offending value, as found (may be any JSON value). */
|
|
11
|
+
value: unknown;
|
|
12
|
+
}
|
|
13
|
+
/** True when the problem sits in a packaging-coverage field. */
|
|
14
|
+
export declare function isCoverageProblem(problem: BuildBaselineProblem): boolean;
|
|
15
|
+
/** Renders a problem for a refusal: the field and a bounded JSON rendering of its value. */
|
|
16
|
+
export declare function describeBuildBaselineProblem(problem: BuildBaselineProblem): string;
|
|
17
|
+
/**
|
|
18
|
+
* Validates persisted fields without converting malformed coverage into a
|
|
19
|
+
* full claim, and returns the first failing field with its value, or
|
|
20
|
+
* undefined when the baseline is valid.
|
|
21
|
+
*
|
|
22
|
+
* @param value - The parsed `last-build.json`
|
|
23
|
+
*/
|
|
24
|
+
export declare function findBuildBaselineProblem(value: unknown): BuildBaselineProblem | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Validates persisted fields without converting malformed coverage into a
|
|
27
|
+
* full claim. The boolean form of {@link findBuildBaselineProblem}.
|
|
28
|
+
*
|
|
29
|
+
* @internal Exported only so tests can reach it. Production reads go
|
|
30
|
+
* through `findBuildBaselineProblem`, which names the failing field.
|
|
31
|
+
*/
|
|
3
32
|
export declare function isBuildBaseline(value: unknown): value is BuildBaseline;
|
|
@@ -24,13 +24,14 @@ function isCoverageAnchor(value) {
|
|
|
24
24
|
isFingerprint(value['mozconfigHash']) &&
|
|
25
25
|
isFingerprintMap(value['fingerprints']));
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
/** A `controlBuild` stamp: the control's diff file, its targets and when it went in. */
|
|
28
|
+
function isControlBuildStamp(value) {
|
|
28
29
|
return (isObject(value) &&
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
typeof value['diffFile'] === 'string' &&
|
|
31
|
+
Array.isArray(value['targets']) &&
|
|
32
|
+
value['targets'].every(isBaselinePath) &&
|
|
33
|
+
typeof value['appliedAt'] === 'string' &&
|
|
34
|
+
Number.isFinite(Date.parse(value['appliedAt'])));
|
|
34
35
|
}
|
|
35
36
|
/** A `recorder` stamp: an integer pid, an invocation id and an optional session id. */
|
|
36
37
|
function isBaselineRecorder(value) {
|
|
@@ -39,43 +40,134 @@ function isBaselineRecorder(value) {
|
|
|
39
40
|
typeof value['invocationId'] === 'string' &&
|
|
40
41
|
(value['sessionId'] === undefined || typeof value['sessionId'] === 'string'));
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/** Fields whose problem a scoped `test --build` can leave and a fresh one replaces. */
|
|
44
|
+
const COVERAGE_FIELDS = ['testPackagingCoverage', 'testCoverageRecords', 'fullCoverageAnchor'];
|
|
45
|
+
/** True when the problem sits in a packaging-coverage field. */
|
|
46
|
+
export function isCoverageProblem(problem) {
|
|
47
|
+
return COVERAGE_FIELDS.some((field) => problem.field === field ||
|
|
48
|
+
problem.field.startsWith(`${field}[`) ||
|
|
49
|
+
problem.field.startsWith(`${field}.`));
|
|
50
|
+
}
|
|
51
|
+
/** Renders a problem for a refusal: the field and a bounded JSON rendering of its value. */
|
|
52
|
+
export function describeBuildBaselineProblem(problem) {
|
|
53
|
+
let rendered;
|
|
54
|
+
try {
|
|
55
|
+
// JSON.stringify(undefined) returns undefined at runtime despite its type.
|
|
56
|
+
const json = JSON.stringify(problem.value);
|
|
57
|
+
rendered = json ?? 'undefined';
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
rendered = String(problem.value);
|
|
61
|
+
}
|
|
62
|
+
if (rendered.length > 120)
|
|
63
|
+
rendered = `${rendered.slice(0, 117)}...`;
|
|
64
|
+
return `${problem.field} = ${rendered}`;
|
|
65
|
+
}
|
|
66
|
+
function findFingerprintMapProblem(field, value) {
|
|
67
|
+
if (!isObject(value))
|
|
68
|
+
return { field, value };
|
|
69
|
+
for (const [path, fingerprint] of Object.entries(value)) {
|
|
70
|
+
if (!isBaselinePath(path))
|
|
71
|
+
return { field: `${field}[${JSON.stringify(path)}]`, value: path };
|
|
72
|
+
if (!isFingerprint(fingerprint)) {
|
|
73
|
+
return { field: `${field}[${JSON.stringify(path)}]`, value: fingerprint };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
function findCoverageRecordProblem(value) {
|
|
79
|
+
const field = 'testCoverageRecords';
|
|
80
|
+
if (!isObject(value))
|
|
81
|
+
return { field, value };
|
|
82
|
+
for (const [path, record] of Object.entries(value)) {
|
|
83
|
+
const entry = `${field}[${JSON.stringify(path)}]`;
|
|
84
|
+
if (!isBaselinePath(path))
|
|
85
|
+
return { field: entry, value: path };
|
|
86
|
+
if (!isCoverageAnchor(record) || !isObject(record) || !Array.isArray(record['inputs'])) {
|
|
87
|
+
return { field: entry, value: record };
|
|
88
|
+
}
|
|
89
|
+
const inputs = record['inputs'];
|
|
90
|
+
const input = inputs.find((item) => !isBaselinePath(item));
|
|
91
|
+
if (input !== undefined)
|
|
92
|
+
return { field: `${entry}.inputs`, value: input };
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validates persisted fields without converting malformed coverage into a
|
|
98
|
+
* full claim, and returns the first failing field with its value, or
|
|
99
|
+
* undefined when the baseline is valid.
|
|
100
|
+
*
|
|
101
|
+
* @param value - The parsed `last-build.json`
|
|
102
|
+
*/
|
|
103
|
+
export function findBuildBaselineProblem(value) {
|
|
44
104
|
if (!isObject(value))
|
|
45
|
-
return
|
|
46
|
-
if (typeof value['engineHeadSha'] !== 'string'
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
105
|
+
return { field: '(root)', value };
|
|
106
|
+
if (typeof value['engineHeadSha'] !== 'string') {
|
|
107
|
+
return { field: 'engineHeadSha', value: value['engineHeadSha'] };
|
|
108
|
+
}
|
|
109
|
+
if (typeof value['builtAt'] !== 'string' || !Number.isFinite(Date.parse(value['builtAt']))) {
|
|
110
|
+
return { field: 'builtAt', value: value['builtAt'] };
|
|
111
|
+
}
|
|
112
|
+
if (typeof value['binaryName'] !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value['binaryName'])) {
|
|
113
|
+
return { field: 'binaryName', value: value['binaryName'] };
|
|
114
|
+
}
|
|
52
115
|
for (const field of [
|
|
53
116
|
'packageableFingerprints',
|
|
54
117
|
'testInputFingerprints',
|
|
55
118
|
'buildInputFingerprints',
|
|
56
119
|
]) {
|
|
57
|
-
if (value[field]
|
|
58
|
-
|
|
120
|
+
if (value[field] === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
const problem = findFingerprintMapProblem(field, value[field]);
|
|
123
|
+
if (problem !== undefined)
|
|
124
|
+
return problem;
|
|
59
125
|
}
|
|
60
126
|
const coverage = value['testPackagingCoverage'];
|
|
61
|
-
if (coverage !== undefined &&
|
|
62
|
-
coverage
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (value['
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
127
|
+
if (coverage !== undefined && coverage !== 'full') {
|
|
128
|
+
if (!Array.isArray(coverage))
|
|
129
|
+
return { field: 'testPackagingCoverage', value: coverage };
|
|
130
|
+
const index = coverage.findIndex((path) => !isBaselinePath(path));
|
|
131
|
+
if (index >= 0) {
|
|
132
|
+
return { field: `testPackagingCoverage[${index}]`, value: coverage[index] };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (value['mozconfigHash'] !== undefined && !isFingerprint(value['mozconfigHash'])) {
|
|
136
|
+
return { field: 'mozconfigHash', value: value['mozconfigHash'] };
|
|
137
|
+
}
|
|
138
|
+
if (value['recordedBy'] !== undefined && typeof value['recordedBy'] !== 'string') {
|
|
139
|
+
return { field: 'recordedBy', value: value['recordedBy'] };
|
|
140
|
+
}
|
|
141
|
+
if (value['recorder'] !== undefined && !isBaselineRecorder(value['recorder'])) {
|
|
142
|
+
return { field: 'recorder', value: value['recorder'] };
|
|
143
|
+
}
|
|
144
|
+
if (value['testCoverageRecords'] !== undefined) {
|
|
145
|
+
const problem = findCoverageRecordProblem(value['testCoverageRecords']);
|
|
146
|
+
if (problem !== undefined)
|
|
147
|
+
return problem;
|
|
148
|
+
}
|
|
149
|
+
if (value['fullCoverageAnchor'] !== undefined && !isCoverageAnchor(value['fullCoverageAnchor'])) {
|
|
150
|
+
return { field: 'fullCoverageAnchor', value: value['fullCoverageAnchor'] };
|
|
151
|
+
}
|
|
76
152
|
const components = value['staticComponentsBaseline'];
|
|
77
|
-
|
|
78
|
-
(isObject(components) &&
|
|
153
|
+
if (components !== undefined &&
|
|
154
|
+
!(isObject(components) &&
|
|
79
155
|
typeof components['engineHeadSha'] === 'string' &&
|
|
80
|
-
isFingerprintMap(components['fingerprints'])))
|
|
156
|
+
isFingerprintMap(components['fingerprints']))) {
|
|
157
|
+
return { field: 'staticComponentsBaseline', value: components };
|
|
158
|
+
}
|
|
159
|
+
if (value['controlBuild'] !== undefined && !isControlBuildStamp(value['controlBuild'])) {
|
|
160
|
+
return { field: 'controlBuild', value: value['controlBuild'] };
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Validates persisted fields without converting malformed coverage into a
|
|
166
|
+
* full claim. The boolean form of {@link findBuildBaselineProblem}.
|
|
167
|
+
*
|
|
168
|
+
* @internal Exported only so tests can reach it. Production reads go
|
|
169
|
+
* through `findBuildBaselineProblem`, which names the failing field.
|
|
170
|
+
*/
|
|
171
|
+
export function isBuildBaseline(value) {
|
|
172
|
+
return findBuildBaselineProblem(value) === undefined;
|
|
81
173
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* on successful build completion. A failed build does not update it, so a
|
|
16
16
|
* subsequent run still audits against the last known-good tree.
|
|
17
17
|
*/
|
|
18
|
-
import { type BuildBaseline, type CoverageAnchor, type CoverageRecord, type TestPackagingCoverage } from './build-baseline-types.js';
|
|
18
|
+
import { type BuildBaseline, type ControlBuildStamp, type CoverageAnchor, type CoverageRecord, type TestPackagingCoverage } from './build-baseline-types.js';
|
|
19
19
|
/**
|
|
20
20
|
* Which mach build produced the baseline being written. Decides whether
|
|
21
21
|
* the `jar.mn` half of {@link BuildBaseline.buildInputFingerprints} is
|
|
@@ -35,7 +35,7 @@ export declare function getBuildBaselinePath(projectRoot: string): string;
|
|
|
35
35
|
* Reads the last-build baseline if present. Returns undefined when no
|
|
36
36
|
* previous successful build has been recorded. Builds can replace invalid
|
|
37
37
|
* markers; dispatch preflights pass `refuse` so invalid coverage never
|
|
38
|
-
* becomes the
|
|
38
|
+
* becomes the implicit full claim of a baseline without coverage.
|
|
39
39
|
* @param projectRoot - Root directory of the project
|
|
40
40
|
*/
|
|
41
41
|
export declare function readBuildBaseline(projectRoot: string, invalid?: 'ignore' | 'refuse'): Promise<BuildBaseline | undefined>;
|
|
@@ -85,6 +85,12 @@ export interface WriteBuildBaselineOptions {
|
|
|
85
85
|
* escalation no full build has honoured.
|
|
86
86
|
*/
|
|
87
87
|
buildKind?: BaselineBuildKind | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Stamp for a build that compiled a negative control into the binary.
|
|
90
|
+
* Omitted → field left off, which is how every other build clears it.
|
|
91
|
+
* See {@link BuildBaseline.controlBuild}.
|
|
92
|
+
*/
|
|
93
|
+
controlBuild?: ControlBuildStamp | undefined;
|
|
88
94
|
}
|
|
89
95
|
/**
|
|
90
96
|
* Records a successful build by writing a fresh baseline marker. Captures
|
|
@@ -26,12 +26,11 @@ import { sha256Hex } from '../utils/hash.js';
|
|
|
26
26
|
import { verbose } from '../utils/logger.js';
|
|
27
27
|
import { isBuildInputPath, isJarManifestPath, isPackageablePath, isXpcomManifestPath, } from './build-audit.js';
|
|
28
28
|
import { DELETED_FILE_FINGERPRINT, } from './build-baseline-types.js';
|
|
29
|
-
import {
|
|
29
|
+
import { describeBuildBaselineProblem, findBuildBaselineProblem, isCoverageProblem, } from './build-baseline-validate.js';
|
|
30
30
|
import { FIREFORGE_DIR } from './config-paths.js';
|
|
31
31
|
import { hashEngineFile } from './coverage-extend.js';
|
|
32
32
|
import { getHead, hasChanges, isMissingHeadError } from './git.js';
|
|
33
33
|
import { git } from './git-base.js';
|
|
34
|
-
import { getUntrackedFiles } from './git-status.js';
|
|
35
34
|
import { historyRecordIdentity } from './history-log-file.js';
|
|
36
35
|
/*
|
|
37
36
|
* Keep the IO fan-out bounded: a patched Firefox checkout can carry hundreds
|
|
@@ -52,22 +51,31 @@ export function getBuildBaselinePath(projectRoot) {
|
|
|
52
51
|
* Reads the last-build baseline if present. Returns undefined when no
|
|
53
52
|
* previous successful build has been recorded. Builds can replace invalid
|
|
54
53
|
* markers; dispatch preflights pass `refuse` so invalid coverage never
|
|
55
|
-
* becomes the
|
|
54
|
+
* becomes the implicit full claim of a baseline without coverage.
|
|
56
55
|
* @param projectRoot - Root directory of the project
|
|
57
56
|
*/
|
|
58
57
|
export async function readBuildBaseline(projectRoot, invalid = 'ignore') {
|
|
59
58
|
const path = getBuildBaselinePath(projectRoot);
|
|
59
|
+
let problem;
|
|
60
60
|
try {
|
|
61
61
|
const value = await readJson(path);
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
problem = findBuildBaselineProblem(value);
|
|
63
|
+
if (problem !== undefined) {
|
|
64
|
+
throw new Error(`invalid baseline field ${describeBuildBaselineProblem(problem)}`);
|
|
65
|
+
}
|
|
64
66
|
return value;
|
|
65
67
|
}
|
|
66
68
|
catch (error) {
|
|
67
69
|
if (getNodeErrorCode(error) === 'ENOENT')
|
|
68
70
|
return undefined;
|
|
69
71
|
if (invalid === 'refuse') {
|
|
70
|
-
|
|
72
|
+
// A coverage field is what a scoped `test --build` writes, so a fresh
|
|
73
|
+
// scoped build over the requested paths replaces it as well as a full
|
|
74
|
+
// one does.
|
|
75
|
+
const remedy = problem !== undefined && isCoverageProblem(problem)
|
|
76
|
+
? 'The field is part of the packaging-coverage claim a scoped "fireforge test --build" records. Rerun this test with --build to record a fresh claim for its paths, or run "fireforge build" to record a full baseline.'
|
|
77
|
+
: 'Run "fireforge build" to record a fresh baseline before dispatching tests.';
|
|
78
|
+
throw new PreflightRefusalError(`Build baseline ${path} is invalid: ${toError(error).message}. ${remedy}`, 'invalid-build-baseline');
|
|
71
79
|
}
|
|
72
80
|
verbose(`Ignoring invalid build baseline ${path}: ${toError(error).message}`);
|
|
73
81
|
return undefined;
|
|
@@ -87,7 +95,7 @@ function baselineRecorder() {
|
|
|
87
95
|
* @param options - See {@link WriteBuildBaselineOptions}
|
|
88
96
|
*/
|
|
89
97
|
export async function writeBuildBaseline(options) {
|
|
90
|
-
const { projectRoot, engineDir, binaryName, testPackagingCoverage, testCoverageRecords, fullCoverageAnchor, previousBaseline, recordedBy, staticComponentsHandling = 'auto', buildKind = 'full', } = options;
|
|
98
|
+
const { projectRoot, engineDir, binaryName, testPackagingCoverage, testCoverageRecords, fullCoverageAnchor, previousBaseline, recordedBy, staticComponentsHandling = 'auto', buildKind = 'full', controlBuild, } = options;
|
|
91
99
|
let engineHeadSha = '';
|
|
92
100
|
try {
|
|
93
101
|
engineHeadSha = await getHead(engineDir);
|
|
@@ -104,7 +112,7 @@ export async function writeBuildBaseline(options) {
|
|
|
104
112
|
const packageableFingerprints = await collectPackageableFingerprints(engineDir);
|
|
105
113
|
const testInputFingerprints = await collectDirtyFingerprints(engineDir, () => true, 'test staging fingerprint');
|
|
106
114
|
const buildInputFingerprints = await collectBuildInputFingerprints(engineDir, previousBaseline, buildKind);
|
|
107
|
-
// A full-coverage write (and
|
|
115
|
+
// A full-coverage write (and a no-claim `fireforge build` record)
|
|
108
116
|
// just recompiled the StaticComponents table, so it anchors the table to
|
|
109
117
|
// the current engine state. A scoped write did not, so carry the previous
|
|
110
118
|
// anchor forward verbatim.
|
|
@@ -128,6 +136,7 @@ export async function writeBuildBaseline(options) {
|
|
|
128
136
|
...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
|
|
129
137
|
...(recordedBy !== undefined ? { recordedBy } : {}),
|
|
130
138
|
recorder: baselineRecorder(),
|
|
139
|
+
...(controlBuild !== undefined ? { controlBuild } : {}),
|
|
131
140
|
};
|
|
132
141
|
await writeJson(getBuildBaselinePath(projectRoot), baseline);
|
|
133
142
|
}
|
|
@@ -202,14 +211,14 @@ async function collectDirtyFingerprints(engineDir, includePath, contextLabel) {
|
|
|
202
211
|
try {
|
|
203
212
|
const dirtyPaths = new Set();
|
|
204
213
|
if (await hasChanges(engineDir)) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
// NUL-separated and unquoted: git's default output backslash-quotes a
|
|
215
|
+
// non-ASCII path (`"a/\303\251.js"`), which is neither the file on
|
|
216
|
+
// disk nor a key the baseline validator accepts.
|
|
217
|
+
const worktreeDiff = await git(['-c', 'core.quotePath=false', 'diff', '--name-only', '-z', 'HEAD'], engineDir);
|
|
218
|
+
const untracked = await git(['-c', 'core.quotePath=false', 'ls-files', '--others', '--exclude-standard', '-z'], engineDir);
|
|
219
|
+
for (const path of `${worktreeDiff}\0${untracked}`.split('\0')) {
|
|
220
|
+
if (path.length > 0)
|
|
221
|
+
dirtyPaths.add(path);
|
|
213
222
|
}
|
|
214
223
|
}
|
|
215
224
|
const included = [...dirtyPaths].filter(includePath);
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Decides whether a changed `jar.mn` really forces the pre-test build to
|
|
3
3
|
* escalate from `mach build faster` to a full `mach build`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* minutes against ~20 s for a chrome-only slice and is paid twice
|
|
7
|
-
* green/no-change pair on a Furnace-touching slice. A downstream
|
|
8
|
-
* (two clean runs, 2026-08-29
|
|
5
|
+
* Escalating on every changed jar.mn (`any changed jar.mn ⇒ full build`)
|
|
6
|
+
* costs ~10 minutes against ~20 s for a chrome-only slice and is paid twice
|
|
7
|
+
* per green/no-change pair on a Furnace-touching slice. A downstream
|
|
8
|
+
* experiment (two clean runs, 2026-08-29) showed that rule is over-broad
|
|
9
9
|
* for the common case: a jar-only registration of a new content file added
|
|
10
10
|
* to an existing `dist/bin` manifest was installed by a plain
|
|
11
11
|
* `fireforge build --ui` both times: `faster/install_dist_bin_browser` named
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Decides whether a changed `jar.mn` really forces the pre-test build to
|
|
4
4
|
* escalate from `mach build faster` to a full `mach build`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* minutes against ~20 s for a chrome-only slice and is paid twice
|
|
8
|
-
* green/no-change pair on a Furnace-touching slice. A downstream
|
|
9
|
-
* (two clean runs, 2026-08-29
|
|
6
|
+
* Escalating on every changed jar.mn (`any changed jar.mn ⇒ full build`)
|
|
7
|
+
* costs ~10 minutes against ~20 s for a chrome-only slice and is paid twice
|
|
8
|
+
* per green/no-change pair on a Furnace-touching slice. A downstream
|
|
9
|
+
* experiment (two clean runs, 2026-08-29) showed that rule is over-broad
|
|
10
10
|
* for the common case: a jar-only registration of a new content file added
|
|
11
11
|
* to an existing `dist/bin` manifest was installed by a plain
|
|
12
12
|
* `fireforge build --ui` both times: `faster/install_dist_bin_browser` named
|
|
@@ -122,7 +122,7 @@ async function decideJarEscalation(engineDir, changed) {
|
|
|
122
122
|
// build. Path-shape only: `evaluateJarManifestEscalation` decides whether a
|
|
123
123
|
// given changed manifest actually forces one (a new manifest, or one that
|
|
124
124
|
// redirects its install base directory), so an entry added to an existing
|
|
125
|
-
// `dist/bin` manifest
|
|
125
|
+
// `dist/bin` manifest does not cost a ~10-minute build.
|
|
126
126
|
const decision = await evaluateJarManifestEscalation(engineDir, changed.filter(isJarManifestPath));
|
|
127
127
|
if (decision.cleared.length > 0) {
|
|
128
128
|
verbose(`jar escalation: ${decision.cleared.length} changed jar.mn add entries to an existing dist/bin manifest; not escalating for ${decision.cleared.join(', ')}.`);
|
|
@@ -241,9 +241,9 @@ async function applyFurnaceBeforeBuild(projectRoot, only) {
|
|
|
241
241
|
}
|
|
242
242
|
// The apply computed these (patch-owned engine files whose content
|
|
243
243
|
// differed from components/ and were replaced). `furnace apply`
|
|
244
|
-
// prints them
|
|
245
|
-
// that edited a deployed Furnace file
|
|
246
|
-
// with a green result and only a checksum could tell.
|
|
244
|
+
// prints them, and so does this path: dropped silently, a negative
|
|
245
|
+
// control that edited a deployed Furnace file would run against the
|
|
246
|
+
// shipped code with a green result and only a checksum could tell.
|
|
247
247
|
const overwriteCount = logApplyWarnings(result.warnings);
|
|
248
248
|
if (overwriteCount > 0) {
|
|
249
249
|
notice(`Furnace: ${overwriteCount} Furnace-managed engine file${overwriteCount === 1 ? '' : 's'} differed from components/ and ${overwriteCount === 1 ? 'was' : 'were'} REPLACED before this build. An engine-side edit to a Furnace-managed file (for example a negative control) did not run; edit the component source under components/ instead, or restore the edit after the build.`);
|