@hominis/fireforge 0.40.0 → 0.42.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 +85 -0
- package/README.md +16 -10
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +20 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +16 -7
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +12 -59
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +151 -50
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +10 -0
- package/dist/src/commands/re-export-adjacent.js +5 -6
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +8 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +113 -268
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +11 -21
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +13 -0
- package/dist/src/commands/status-check.js +31 -13
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +184 -175
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +41 -4
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/tree.d.ts +3 -0
- package/dist/src/commands/tree.js +171 -25
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/branding.d.ts +16 -0
- package/dist/src/core/branding.js +18 -2
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +34 -36
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +1 -1
- package/dist/src/core/lint-cache.js +33 -10
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-mozconfig.js +9 -1
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.js +3 -5
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
- package/dist/src/core/patch-lint-checkjs.js +78 -7
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-reexports.d.ts +1 -0
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +8 -0
- package/dist/src/core/patch-lint.js +84 -6
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +0 -8
- package/dist/src/core/smoke-patterns.js +3 -2
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +6 -1
- package/dist/src/core/status-classify.js +101 -73
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-guard.d.ts +1 -1
- package/dist/src/core/tree-guard.js +67 -12
- package/dist/src/core/tree-store.d.ts +86 -10
- package/dist/src/core/tree-store.js +310 -72
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/types/commands/options.d.ts +86 -1
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +23 -2
- package/dist/src/utils/validation.js +29 -3
- package/package.json +9 -9
|
@@ -1,230 +1,20 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
/* eslint-disable max-lines -- Re-export is at the guardrail; splitting register plumbing is unrelated to this fix. */
|
|
3
2
|
import { multiselect } from '@clack/prompts';
|
|
4
|
-
import { Command, Option } from 'commander';
|
|
5
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
6
|
-
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
7
4
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
8
|
-
import { enforceFreshFurnaceSources } from '../core/furnace-stale-export.js';
|
|
9
5
|
import { isGitRepository } from '../core/git.js';
|
|
10
|
-
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
11
|
-
import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
12
|
-
import { buildPatchQueueContext } from '../core/patch-lint.js';
|
|
13
6
|
import { loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from '../core/patch-manifest.js';
|
|
14
|
-
import { buildProjectedManifest, enforcePatchPolicy } from '../core/patch-policy.js';
|
|
15
7
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
16
|
-
import { isGitIndexLockConflict } from '../errors/git.js';
|
|
17
8
|
import { elapsedSince } from '../utils/elapsed.js';
|
|
18
9
|
import { toError } from '../utils/errors.js';
|
|
19
10
|
import { pathExists } from '../utils/fs.js';
|
|
20
11
|
import { cancel, info, intro, isCancel, outro, spinner, success, warn } from '../utils/logger.js';
|
|
21
|
-
import {
|
|
22
|
-
import { runPatchLint } from './export-shared.js';
|
|
23
|
-
import { findMissingFiles, reportAdjacentUnmanagedFiles } from './re-export-adjacent.js';
|
|
24
|
-
import { loadScanFilesAssignments, withDryRunReExportLock } from './re-export-bulk-scan.js';
|
|
12
|
+
import { loadScanFilesAssignments, withDryRunPurityGuard, withDryRunReExportLock, } from './re-export-bulk-scan.js';
|
|
25
13
|
import { reExportFilesInPlace } from './re-export-files.js';
|
|
14
|
+
import { buildReExportLintContext, finishReExportLint } from './re-export-lint.js';
|
|
26
15
|
import { applyReExportFilesPositionalFolding, validateReExportOptionCombinations, } from './re-export-options.js';
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Retries a single patch re-export exactly once when the failure is
|
|
31
|
-
* transient git `index.lock` contention (an external git process holding
|
|
32
|
-
* the engine repo's index). The retry re-enters `reExportSinglePatch`
|
|
33
|
-
* from the top, so it re-reads clean state; a second lock failure
|
|
34
|
-
* propagates to the loop's honest "N of M" accounting.
|
|
35
|
-
*/
|
|
36
|
-
async function reExportSinglePatchWithIndexLockRetry(patch, paths, manifest, options, isDryRun, config, adjacentCtx) {
|
|
37
|
-
try {
|
|
38
|
-
return await reExportSinglePatch(patch, paths, manifest, options, isDryRun, config, adjacentCtx);
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
if (!isGitIndexLockConflict(error)) {
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
warn(`${patch.filename}: git index.lock contention detected — retrying once after ${String(GIT_INDEX_LOCK_RETRY_MS)} ms...`);
|
|
45
|
-
await new Promise((resolve) => setTimeout(resolve, GIT_INDEX_LOCK_RETRY_MS));
|
|
46
|
-
return await reExportSinglePatch(patch, paths, manifest, options, isDryRun, config, adjacentCtx);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, config, adjacentCtx) {
|
|
50
|
-
let currentFilesAffected = [...patch.filesAffected];
|
|
51
|
-
// --- Scan for new/removed files ---
|
|
52
|
-
if (options.scan) {
|
|
53
|
-
const scanResult = await scanPatchFilesForReExport({
|
|
54
|
-
currentFilesAffected,
|
|
55
|
-
engineDir: paths.engine,
|
|
56
|
-
manifest,
|
|
57
|
-
patchFilename: patch.filename,
|
|
58
|
-
isDryRun,
|
|
59
|
-
...(options.scanFiles !== undefined ? { scanFiles: options.scanFiles } : {}),
|
|
60
|
-
});
|
|
61
|
-
// Forward-import gate at adoption time (runs in dry-run too): refuse
|
|
62
|
-
// to adopt unmanaged files that import modules created by LATER
|
|
63
|
-
// patches — the after-the-fact lint failure this leaves behind is the
|
|
64
|
-
// field-reported footgun. Applies to broad --scan, --scan-file, and
|
|
65
|
-
// the --scan-files bulk assignments alike.
|
|
66
|
-
await assertScanAdoptionsHaveNoForwardImports({
|
|
67
|
-
patchesDir: paths.patches,
|
|
68
|
-
engineDir: paths.engine,
|
|
69
|
-
patchFilename: patch.filename,
|
|
70
|
-
added: scanResult.added,
|
|
71
|
-
});
|
|
72
|
-
if (options.scanFiles === undefined) {
|
|
73
|
-
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
74
|
-
const proceed = await confirmBroadScanAdditions({
|
|
75
|
-
patchFilename: patch.filename,
|
|
76
|
-
added: scanResult.added,
|
|
77
|
-
isDryRun,
|
|
78
|
-
yes: options.yes === true,
|
|
79
|
-
isInteractive,
|
|
80
|
-
});
|
|
81
|
-
if (!proceed) {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
currentFilesAffected = scanResult.updated;
|
|
86
|
-
}
|
|
87
|
-
else if (options.files === undefined) {
|
|
88
|
-
// Finding #16: when neither `--scan` nor `--files` is set and some
|
|
89
|
-
// of the manifest's claimed files no longer exist on disk, the
|
|
90
|
-
// re-export silently writes a refreshed body whose filesAffected
|
|
91
|
-
// still names the vanished paths. That is the documented contract,
|
|
92
|
-
// but it is also a footgun — a later `verify` then fails on
|
|
93
|
-
// manifest-consistency with no obvious trigger. Emit one advisory
|
|
94
|
-
// warning up-front when we can detect the drift cheaply, so the
|
|
95
|
-
// operator has a chance to re-run with `--scan` or `--files`
|
|
96
|
-
// before the stale filesAffected lands in patches.json.
|
|
97
|
-
const args = { patch, paths, manifest, currentFilesAffected, ctx: adjacentCtx };
|
|
98
|
-
if (await reportAdjacentUnmanagedFiles(args))
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
// --- Explicit file-subset path ---
|
|
102
|
-
// When --files is given, the target filesAffected is authoritative — drop
|
|
103
|
-
// anything not in the list, add anything new. This is the surgical repair
|
|
104
|
-
// primitive that replaces hand-editing patches.json; the user has already
|
|
105
|
-
// acknowledged via confirmDestructive (done in the caller) that any drop
|
|
106
|
-
// is intentional.
|
|
107
|
-
if (options.files !== undefined) {
|
|
108
|
-
const requested = [...new Set(options.files)].sort();
|
|
109
|
-
currentFilesAffected = requested;
|
|
110
|
-
const removed = patch.filesAffected.filter((f) => !requested.includes(f));
|
|
111
|
-
const added = requested.filter((f) => !patch.filesAffected.includes(f));
|
|
112
|
-
for (const f of added)
|
|
113
|
-
info(` + ${f}`);
|
|
114
|
-
for (const f of removed)
|
|
115
|
-
info(` - ${f}`);
|
|
116
|
-
}
|
|
117
|
-
// Stale-furnace-source gate (0.37.0 item 4): re-export captures deployed
|
|
118
|
-
// engine copies, so a component source edited after the last furnace
|
|
119
|
-
// apply would land in the patch as its OLD deployed content. Refuse (or
|
|
120
|
-
// warn under --allow-stale-furnace) before diffing. Runs in dry-run too
|
|
121
|
-
// so the failure surfaces early.
|
|
122
|
-
await enforceFreshFurnaceSources(paths.root, currentFilesAffected, options.allowStaleFurnace === true, 're-export');
|
|
123
|
-
const missingFiles = await findMissingFiles(paths.engine, currentFilesAffected);
|
|
124
|
-
if (missingFiles.length === currentFilesAffected.length) {
|
|
125
|
-
warn(`Skipped ${patch.filename}: all affected files missing`);
|
|
126
|
-
warn(`Missing files: ${missingFiles.join(', ')}`);
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
if (missingFiles.length > 0) {
|
|
130
|
-
warn(`${patch.filename}: missing files will be skipped: ${missingFiles.join(', ')}`);
|
|
131
|
-
}
|
|
132
|
-
const missingSet = new Set(missingFiles);
|
|
133
|
-
const existingFiles = currentFilesAffected.filter((f) => !missingSet.has(f));
|
|
134
|
-
const diffContent = await getDiffForFilesAgainstHead(paths.engine, existingFiles);
|
|
135
|
-
assertScanFileAdditionsHaveDiffHunks({
|
|
136
|
-
diffContent,
|
|
137
|
-
patchFilename: patch.filename,
|
|
138
|
-
previousFilesAffected: patch.filesAffected,
|
|
139
|
-
scanFiles: options.scanFiles,
|
|
140
|
-
});
|
|
141
|
-
if (!diffContent.trim()) {
|
|
142
|
-
warn(`Skipped ${patch.filename}: no changes (files unchanged from HEAD)`);
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
// Thread the patch's own `lintIgnore` list through so the per-patch
|
|
146
|
-
// suppression honoured by export/export-all is also honoured here.
|
|
147
|
-
// Without this, `re-export` could not refresh an advisory-noisy but
|
|
148
|
-
// intentional patch (a cohesive branding bundle, a localised-resource
|
|
149
|
-
// pack) without either `--skip-lint` (too blunt) or falling through to
|
|
150
|
-
// the full `rebase` flow (which internally skips the lint pipeline).
|
|
151
|
-
// The paired `patch.tier` threads the explicit branding-threshold
|
|
152
|
-
// opt-in the same way, for the branding patch that also touches a
|
|
153
|
-
// non-allowlisted registration sibling.
|
|
154
|
-
//
|
|
155
|
-
// The CLI flags `--tier` and `--lint-ignore` participate too, with
|
|
156
|
-
// append/union semantics on the lint-ignore list (the operator's
|
|
157
|
-
// intuition for "I want this patch to also suppress X" — explicit
|
|
158
|
-
// removal lives on the `fireforge patch lint-ignore` subcommand).
|
|
159
|
-
// Computed before the lint pass so the new intent takes effect on
|
|
160
|
-
// this invocation, not the next one.
|
|
161
|
-
const existingIgnoreSet = new Set(patch.lintIgnore ?? []);
|
|
162
|
-
const flagIgnoreSet = new Set(options.lintIgnore ?? []);
|
|
163
|
-
const mergedIgnoreSet = new Set([...existingIgnoreSet, ...flagIgnoreSet]);
|
|
164
|
-
const effectiveLintIgnore = mergedIgnoreSet.size > 0 ? [...mergedIgnoreSet] : undefined;
|
|
165
|
-
const ignoreChecks = effectiveLintIgnore ? new Set(effectiveLintIgnore) : undefined;
|
|
166
|
-
const effectiveTier = options.tier ?? patch.tier;
|
|
167
|
-
const updates = {
|
|
168
|
-
filesAffected: currentFilesAffected,
|
|
169
|
-
};
|
|
170
|
-
if (options.tier !== undefined) {
|
|
171
|
-
updates.tier = options.tier;
|
|
172
|
-
}
|
|
173
|
-
if (effectiveLintIgnore !== undefined && flagIgnoreSet.size > 0) {
|
|
174
|
-
updates.lintIgnore = effectiveLintIgnore;
|
|
175
|
-
}
|
|
176
|
-
enforcePatchPolicy({
|
|
177
|
-
config,
|
|
178
|
-
manifest: buildProjectedManifest(manifest, manifest.patches.map((entry) => entry.filename === patch.filename ? { ...entry, ...updates } : entry)),
|
|
179
|
-
command: 're-export',
|
|
180
|
-
forceUnsafe: options.forceUnsafe === true,
|
|
181
|
-
});
|
|
182
|
-
// Pass the whole-queue context so checkJs resolves cross-patch
|
|
183
|
-
// `resource:///` imports against the real owning sources (report scope
|
|
184
|
-
// stays this patch — see runPatchLint).
|
|
185
|
-
const patchQueueCtx = (await pathExists(paths.patches))
|
|
186
|
-
? await buildPatchQueueContext(paths.patches)
|
|
187
|
-
: undefined;
|
|
188
|
-
await runPatchLint(paths.engine, existingFiles, diffContent, config, options.skipLint, patchQueueCtx, ignoreChecks, effectiveTier);
|
|
189
|
-
if (isDryRun) {
|
|
190
|
-
info(`[dry-run] ${patch.filename}: ${existingFiles.length} file(s)`);
|
|
191
|
-
if (effectiveTier !== undefined && effectiveTier !== patch.tier) {
|
|
192
|
-
info(`[dry-run] ${patch.filename}: tier would become ${effectiveTier}`);
|
|
193
|
-
}
|
|
194
|
-
const addedIgnores = [...flagIgnoreSet].filter((id) => !existingIgnoreSet.has(id));
|
|
195
|
-
if (addedIgnores.length > 0) {
|
|
196
|
-
info(`[dry-run] ${patch.filename}: lintIgnore would gain ${addedIgnores.join(', ')}`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
// Atomic body + manifest update under a single patch-directory lock.
|
|
201
|
-
// A split `updatePatch` (lock-free) + `updatePatchMetadata` (lock-guarded)
|
|
202
|
-
// sequence allows a concurrent `resolve` / `rebase --continue` / `patch
|
|
203
|
-
// compact` / `patch reorder` to rewrite the manifest between the two
|
|
204
|
-
// writes and leave patch body and `filesAffected` disagreeing.
|
|
205
|
-
await updatePatchAndMetadata(paths.patches, patch.filename, diffContent, updates, undefined, {
|
|
206
|
-
config,
|
|
207
|
-
command: 're-export',
|
|
208
|
-
forceUnsafe: options.forceUnsafe === true,
|
|
209
|
-
});
|
|
210
|
-
// Keep the in-memory manifest in sync so subsequent iterations (notably
|
|
211
|
-
// `--all --scan`, where `getClaimedFiles` reads from this manifest) see
|
|
212
|
-
// the just-written `filesAffected`. The on-disk write above is the
|
|
213
|
-
// authority; this is a cache update.
|
|
214
|
-
const patchIndex = manifest.patches.findIndex((pm) => pm.filename === patch.filename);
|
|
215
|
-
if (patchIndex !== -1) {
|
|
216
|
-
const existingEntry = manifest.patches[patchIndex];
|
|
217
|
-
if (existingEntry) {
|
|
218
|
-
manifest.patches[patchIndex] = {
|
|
219
|
-
...existingEntry,
|
|
220
|
-
...updates,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
success(`Re-exported ${patch.filename}`);
|
|
225
|
-
}
|
|
226
|
-
return true;
|
|
227
|
-
}
|
|
16
|
+
import { normalizeEngineRelativeInput, normalizeScanFiles } from './re-export-scan.js';
|
|
17
|
+
import { reExportSinglePatchWithIndexLockRetry } from './re-export-single.js';
|
|
228
18
|
async function resolveSelectedPatches(patches, options, manifest) {
|
|
229
19
|
if (options.all) {
|
|
230
20
|
return [...manifest.patches];
|
|
@@ -328,7 +118,7 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
328
118
|
// we write anything.
|
|
329
119
|
if (options.files !== undefined) {
|
|
330
120
|
const filesConfig = await loadConfig(projectRoot);
|
|
331
|
-
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, () => reExportFilesInPlace(paths, selectedPatches, options, filesConfig));
|
|
121
|
+
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, () => withDryRunPurityGuard(paths.engine, paths.patches, isDryRun, () => reExportFilesInPlace(paths, selectedPatches, options, filesConfig)));
|
|
332
122
|
return;
|
|
333
123
|
}
|
|
334
124
|
const config = await loadConfig(projectRoot);
|
|
@@ -340,35 +130,37 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
340
130
|
refuseAdjacentUnmanaged: options.refuseAdjacentUnmanaged === true,
|
|
341
131
|
refusals: [],
|
|
342
132
|
};
|
|
133
|
+
const driftCtx = buildForeignDriftContext(options);
|
|
134
|
+
// Hoisted lint context, one per run (FORGE J1): queue context + checkJs
|
|
135
|
+
// program + per-patch result cache, shared across every loop iteration.
|
|
136
|
+
const lintCtx = await buildReExportLintContext(projectRoot, paths, config, options.noCache === true);
|
|
343
137
|
let reExported = 0;
|
|
344
138
|
const reExportedFilenames = [];
|
|
139
|
+
const failedFilenames = [];
|
|
345
140
|
const progress = spinner('Preparing re-export...');
|
|
346
141
|
const startedAt = Date.now();
|
|
347
|
-
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, async () => {
|
|
142
|
+
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, async () => withDryRunPurityGuard(paths.engine, paths.patches, isDryRun, async () => {
|
|
348
143
|
for (const [index, patch] of selectedPatches.entries()) {
|
|
349
144
|
const assignedScanFiles = scanFilesByPatch?.get(patch.filename);
|
|
350
145
|
const patchOptions = assignedScanFiles !== undefined ? { ...options, scanFiles: assignedScanFiles } : options;
|
|
351
146
|
progress.message(`Re-exporting ${index + 1}/${selectedPatches.length}: ${patch.filename} (${patch.filesAffected.length} file(s), ${elapsedSince(startedAt)} elapsed)...`);
|
|
352
147
|
try {
|
|
353
|
-
const exported = await reExportSinglePatchWithIndexLockRetry(patch, paths, manifest, patchOptions, isDryRun, config, adjacentCtx);
|
|
148
|
+
const exported = await reExportSinglePatchWithIndexLockRetry(patch, paths, manifest, patchOptions, isDryRun, config, adjacentCtx, driftCtx, lintCtx);
|
|
354
149
|
if (exported) {
|
|
355
150
|
reExported++;
|
|
356
151
|
reExportedFilenames.push(patch.filename);
|
|
357
152
|
}
|
|
358
153
|
}
|
|
359
154
|
catch (error) {
|
|
155
|
+
failedFilenames.push(patch.filename);
|
|
360
156
|
warn(`Failed to re-export ${patch.filename}`);
|
|
361
157
|
warn(toError(error).message);
|
|
362
158
|
}
|
|
363
159
|
}
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
throw new GeneralError(`Refused ${String(adjacentCtx.refusals.length)} patch(es) with adjacent unmanaged files ` +
|
|
369
|
-
`(--refuse-adjacent-unmanaged): ${names}. Adopt reviewed files with --scan --scan-file, ` +
|
|
370
|
-
`or export them as a new patch.`);
|
|
371
|
-
}
|
|
160
|
+
}));
|
|
161
|
+
await finishReExportLint(lintCtx);
|
|
162
|
+
warnUnseenExpectedDrift(driftCtx);
|
|
163
|
+
throwRunLevelRefusals(adjacentCtx, driftCtx, progress);
|
|
372
164
|
if (reExported === 0 && selectedPatches.length > 0) {
|
|
373
165
|
progress.error('Re-export failed');
|
|
374
166
|
throw new GeneralError('All selected patches failed to re-export. Check the errors above.');
|
|
@@ -384,60 +176,113 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
384
176
|
if (shouldStamp) {
|
|
385
177
|
await stampPatchVersions(paths.patches, reExportedFilenames, config.firefox.version, config.firefox.product);
|
|
386
178
|
}
|
|
179
|
+
reportReExportOutcome({
|
|
180
|
+
isDryRun,
|
|
181
|
+
reExported,
|
|
182
|
+
selectedCount: selectedPatches.length,
|
|
183
|
+
reExportedFilenames,
|
|
184
|
+
failedFilenames,
|
|
185
|
+
shouldStamp,
|
|
186
|
+
stampRequested: options.stamp === true,
|
|
187
|
+
config,
|
|
188
|
+
progress,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Foreign-drift guard context, one per run (FORGE J2). `--expect` paths
|
|
193
|
+
* (FORGE L6) are normalized once here so the per-patch comparison matches
|
|
194
|
+
* them against engine-relative diff-section paths.
|
|
195
|
+
*/
|
|
196
|
+
function buildForeignDriftContext(options) {
|
|
197
|
+
return {
|
|
198
|
+
refuseForeignDrift: options.refuseForeignDrift === true,
|
|
199
|
+
expectedDriftFiles: new Set((options.expect ?? []).map((file) => normalizeEngineRelativeInput(file, '--expect'))),
|
|
200
|
+
expectedSeen: new Set(),
|
|
201
|
+
evaluationRuns: 0,
|
|
202
|
+
refusals: [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Names `--expect` paths that never drifted this run (FORGE L6). A typo'd
|
|
207
|
+
* `--expect` path silently degrades the flag back to refusing the slice it
|
|
208
|
+
* was meant to admit, so surface the mismatch — but only as a warning: an
|
|
209
|
+
* expected file legitimately shows no drift when the slice was already
|
|
210
|
+
* captured by an earlier export.
|
|
211
|
+
*/
|
|
212
|
+
function warnUnseenExpectedDrift(driftCtx) {
|
|
213
|
+
const unseen = [...driftCtx.expectedDriftFiles].filter((file) => !driftCtx.expectedSeen.has(file));
|
|
214
|
+
if (unseen.length === 0)
|
|
215
|
+
return;
|
|
216
|
+
if (driftCtx.evaluationRuns === 0) {
|
|
217
|
+
warn(`--expect path(s) were not evaluated because no selected patch reached the drift check (for example, an earlier refusal): ${unseen.join(', ')}`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
warn(`--expect path(s) showed no drift this run (typo, or already captured?): ${unseen.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Turns the run's collected `--refuse-adjacent-unmanaged` (FORGE G2) and
|
|
224
|
+
* `--refuse-foreign-drift` (FORGE J2) refusals into the non-zero exit,
|
|
225
|
+
* naming every refused patch and the remedy.
|
|
226
|
+
*/
|
|
227
|
+
function throwRunLevelRefusals(adjacentCtx, driftCtx, progress) {
|
|
228
|
+
if (adjacentCtx.refusals.length > 0) {
|
|
229
|
+
progress.error('Re-export refused');
|
|
230
|
+
const names = adjacentCtx.refusals.map((r) => r.patchFilename).join(', ');
|
|
231
|
+
throw new GeneralError(`Refused ${String(adjacentCtx.refusals.length)} patch(es) with adjacent unmanaged files ` +
|
|
232
|
+
`(--refuse-adjacent-unmanaged): ${names}. Adopt reviewed files with --scan --scan-file, ` +
|
|
233
|
+
`or export them as a new patch.`);
|
|
234
|
+
}
|
|
235
|
+
if (driftCtx.refusals.length > 0) {
|
|
236
|
+
progress.error('Re-export refused');
|
|
237
|
+
const names = driftCtx.refusals.map((r) => r.patchFilename).join(', ');
|
|
238
|
+
const unreadable = driftCtx.refusals.filter((r) => r.reason === 'baseline-unreadable');
|
|
239
|
+
const unreadablePart = unreadable.length > 0
|
|
240
|
+
? ` ${String(unreadable.length)} of these refused because the old patch body was missing or unreadable, so the drift comparison could not run (fail-closed).`
|
|
241
|
+
: '';
|
|
242
|
+
throw new GeneralError(`Refused ${String(driftCtx.refusals.length)} patch(es) whose refreshed body would absorb ` +
|
|
243
|
+
`engine edits not present in the old patch body (--refuse-foreign-drift): ${names}.${unreadablePart} ` +
|
|
244
|
+
`Commit or stash the foreign edits (another session may own them), name the slice's ` +
|
|
245
|
+
`intended files with --expect <path>, or re-run without the flag to capture them intentionally.`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Prints the end-of-run summary and enforces the FORGE H8 exit contract:
|
|
250
|
+
* a partial run exits non-zero BY DEFAULT (adjudicated for 0.41.0, per the
|
|
251
|
+
* follow-up 0.40.0's G7 recorded). "Re-exported 2 of 3" used to print and
|
|
252
|
+
* exit 0, letting a partial refresh ride an `&&` chain as success. The
|
|
253
|
+
* summary still prints first so the honest "N of M" accounting stays
|
|
254
|
+
* visible; the throw then carries the failed/skipped breakdown. Dry-run
|
|
255
|
+
* included: a preview reporting that the real run would partially fail
|
|
256
|
+
* must not read as a passing preview in scripts.
|
|
257
|
+
*/
|
|
258
|
+
function reportReExportOutcome(args) {
|
|
259
|
+
const { isDryRun, reExported, selectedCount, config, progress } = args;
|
|
387
260
|
if (isDryRun) {
|
|
388
261
|
progress.stop('Dry run complete');
|
|
389
|
-
success(`[dry-run] Would re-export ${reExported} of ${
|
|
390
|
-
if (
|
|
262
|
+
success(`[dry-run] Would re-export ${reExported} of ${selectedCount} patch(es)`);
|
|
263
|
+
if (args.stampRequested) {
|
|
391
264
|
info(`[dry-run] Would stamp sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${reExported} patch(es)`);
|
|
392
265
|
}
|
|
393
266
|
outro('Dry run complete');
|
|
394
267
|
}
|
|
395
268
|
else {
|
|
396
269
|
progress.stop('Re-export complete');
|
|
397
|
-
success(`Re-exported ${reExported} of ${
|
|
398
|
-
if (shouldStamp) {
|
|
399
|
-
success(`Stamped sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${reExportedFilenames.length} patch(es)`);
|
|
270
|
+
success(`Re-exported ${reExported} of ${selectedCount} patch(es)`);
|
|
271
|
+
if (args.shouldStamp) {
|
|
272
|
+
success(`Stamped sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${args.reExportedFilenames.length} patch(es)`);
|
|
400
273
|
}
|
|
401
|
-
else if (
|
|
274
|
+
else if (args.stampRequested && reExported !== selectedCount) {
|
|
402
275
|
warn('--stamp was requested but some patches failed or were skipped; refusing to stamp a partial set.');
|
|
403
276
|
}
|
|
404
277
|
outro('Re-export complete');
|
|
405
278
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
.option('-s, --scan', 'Scan directories for new/removed files and update filesAffected')
|
|
416
|
-
.option('--scan-file <path>', 'With --scan, add this explicit engine-relative file to one target patch without collecting adjacent files. Repeatable.', (value, prev) => [...prev, value], [])
|
|
417
|
-
.option('--scan-files <manifest>', 'With --scan, bulk-assign generated files from a JSON manifest: {"assignments":[{"patch":"002-name.patch","files":["path"]}]}. Selects patches from the manifest.')
|
|
418
|
-
.option('--files <paths>', 'Restrict the re-exported filesAffected to this comma-separated list (single target patch only)', (value) => value
|
|
419
|
-
.split(',')
|
|
420
|
-
.map((v) => v.trim())
|
|
421
|
-
.filter((v) => v.length > 0))
|
|
422
|
-
.option('--refuse-adjacent-unmanaged', 'Refuse a scan-less re-export (non-zero exit, patch not written) when unmanaged files exist adjacent to the patch ownership, instead of warning. Mutually exclusive with --scan and --files.')
|
|
423
|
-
.option('--dry-run', 'Show what would change without writing')
|
|
424
|
-
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
425
|
-
.option('--allow-stale-furnace', 'Export the deployed engine copy even when the components/ source changed since the last furnace apply')
|
|
426
|
-
.option('--allow-shrink', 'Allow --files to remove paths currently owned by the patch. Required before --yes can bypass the shrink confirmation.')
|
|
427
|
-
.option('-y, --yes', 'Skip confirmation prompts (required for non-TTY destructive writes)')
|
|
428
|
-
.option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
|
|
429
|
-
.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.")
|
|
430
|
-
.addOption(new Option('--tier <tier>', 'Force a tier override on the selected patch (only "branding" recognised). Mutually exclusive with --all.').choices(['branding']))
|
|
431
|
-
.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], []);
|
|
432
|
-
addWaitLockOption(reExport).action(withErrorHandling(async (patches, options) => {
|
|
433
|
-
const { tier, lintIgnore, scanFile, scanFiles, ...rest } = options;
|
|
434
|
-
const projectRoot = getProjectRoot();
|
|
435
|
-
await withEngineSessionLock(projectRoot, 're-export', () => reExportCommand(projectRoot, patches, {
|
|
436
|
-
...pickDefined(rest),
|
|
437
|
-
...(scanFile !== undefined && scanFile.length > 0 ? { scanFiles: scanFile } : {}),
|
|
438
|
-
...(scanFiles !== undefined ? { scanFilesManifest: scanFiles } : {}),
|
|
439
|
-
...(tier !== undefined ? { tier: tier } : {}),
|
|
440
|
-
...(lintIgnore !== undefined && lintIgnore.length > 0 ? { lintIgnore } : {}),
|
|
441
|
-
}), { waitLockSeconds: resolveWaitLockSeconds(options.waitLock) });
|
|
442
|
-
}));
|
|
279
|
+
if (reExported < selectedCount) {
|
|
280
|
+
const skippedCount = selectedCount - reExported - args.failedFilenames.length;
|
|
281
|
+
const failedPart = args.failedFilenames.length > 0 ? ` Failed: ${args.failedFilenames.join(', ')}.` : '';
|
|
282
|
+
const skippedPart = skippedCount > 0 ? ` Skipped: ${String(skippedCount)} patch(es) (see warnings above).` : '';
|
|
283
|
+
throw new GeneralError(`${isDryRun ? '[dry-run] Would re-export' : 'Re-exported'} only ${String(reExported)} of ` +
|
|
284
|
+
`${String(selectedCount)} selected patch(es).${failedPart}${skippedPart} ` +
|
|
285
|
+
'Partial re-export exits non-zero so it cannot pass as success in scripts; ' +
|
|
286
|
+
'the per-patch errors are listed above.');
|
|
287
|
+
}
|
|
443
288
|
}
|
|
@@ -5,18 +5,27 @@
|
|
|
5
5
|
import { getProjectPaths, updateState } from '../../core/config.js';
|
|
6
6
|
import { clearAppliedFurnaceState } from '../../core/furnace-config.js';
|
|
7
7
|
import { resetChanges } from '../../core/git.js';
|
|
8
|
-
import { clearRebaseSession,
|
|
8
|
+
import { clearRebaseSession, getRebaseSessionPath, readRebaseSession, } from '../../core/rebase-session.js';
|
|
9
9
|
import { NoRebaseSessionError } from '../../errors/rebase.js';
|
|
10
|
-
import { intro, outro, spinner, success } from '../../utils/logger.js';
|
|
10
|
+
import { intro, outro, spinner, success, warn } from '../../utils/logger.js';
|
|
11
11
|
import { confirmDirtyEngineReset } from './confirm.js';
|
|
12
12
|
/**
|
|
13
13
|
* Handles `fireforge rebase --abort`.
|
|
14
14
|
*/
|
|
15
15
|
export async function handleAbort(projectRoot, yes) {
|
|
16
16
|
intro('FireForge Rebase — Abort');
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Abort is the escape hatch, so it deliberately does NOT require a *valid*
|
|
18
|
+
// session — only that one is present. Nothing below reads the session
|
|
19
|
+
// object (the restore works off `paths.engine` and `resetChanges`), and the
|
|
20
|
+
// pre-0.41.0 `if (!session) throw` refused to run against a corrupt file,
|
|
21
|
+
// which was the only command that could have cleared it.
|
|
22
|
+
const read = await readRebaseSession(projectRoot);
|
|
23
|
+
if (!read.present)
|
|
19
24
|
throw new NoRebaseSessionError();
|
|
25
|
+
if (!read.valid) {
|
|
26
|
+
warn(`The rebase session at ${getRebaseSessionPath(projectRoot)} is unreadable ` +
|
|
27
|
+
`(${read.reason}). Aborting anyway: the engine will be restored and the session cleared.`);
|
|
28
|
+
}
|
|
20
29
|
const paths = getProjectPaths(projectRoot);
|
|
21
30
|
if (!(await confirmDirtyEngineReset({
|
|
22
31
|
engineDir: paths.engine,
|
|
@@ -8,10 +8,10 @@ import { getStagedDiffForFiles } from '../../core/git-diff.js';
|
|
|
8
8
|
import { stageFiles, unstageFiles } from '../../core/git-file-ops.js';
|
|
9
9
|
import { updatePatchAndMetadata } from '../../core/patch-export.js';
|
|
10
10
|
import { loadPatchesManifest } from '../../core/patch-manifest.js';
|
|
11
|
-
import {
|
|
11
|
+
import { getRebaseSessionPath, readRebaseSession, saveRebaseSession, } from '../../core/rebase-session.js';
|
|
12
12
|
import { runInSignalCriticalSection } from '../../core/signal-critical.js';
|
|
13
13
|
import { GeneralError } from '../../errors/base.js';
|
|
14
|
-
import { NoRebaseSessionError, RebaseError } from '../../errors/rebase.js';
|
|
14
|
+
import { CorruptRebaseSessionError, NoRebaseSessionError, RebaseError, } from '../../errors/rebase.js';
|
|
15
15
|
import { pathExists } from '../../utils/fs.js';
|
|
16
16
|
import { info, intro, success, warn } from '../../utils/logger.js';
|
|
17
17
|
import { runPatchLoop } from './patch-loop.js';
|
|
@@ -20,9 +20,16 @@ import { runPatchLoop } from './patch-loop.js';
|
|
|
20
20
|
*/
|
|
21
21
|
export async function handleContinue(projectRoot, maxFuzz) {
|
|
22
22
|
intro('FireForge Rebase — Continue');
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// A present-but-unreadable session is NOT "no session in progress" — that
|
|
24
|
+
// conflation is what made a corrupt file unrecoverable. Name the file and
|
|
25
|
+
// point at --abort, which can now clear it.
|
|
26
|
+
const read = await readRebaseSession(projectRoot);
|
|
27
|
+
if (!read.present)
|
|
25
28
|
throw new NoRebaseSessionError();
|
|
29
|
+
if (!read.valid) {
|
|
30
|
+
throw new CorruptRebaseSessionError(getRebaseSessionPath(projectRoot), read.reason);
|
|
31
|
+
}
|
|
32
|
+
const session = read.session;
|
|
26
33
|
const paths = getProjectPaths(projectRoot);
|
|
27
34
|
// Special case: every patch has already applied but a previous run failed
|
|
28
35
|
// somewhere in the post-apply work (re-export, version stamping). In that
|
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { Command } from 'commander';
|
|
14
14
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
15
|
+
import { assertEngineGitReady } from '../../core/engine-precondition.js';
|
|
15
16
|
import { clearAppliedFurnaceState } from '../../core/furnace-config.js';
|
|
16
|
-
import { getHead,
|
|
17
|
+
import { getHead, resetChanges } from '../../core/git.js';
|
|
17
18
|
import { discoverPatches } from '../../core/patch-files.js';
|
|
18
19
|
import { loadPatchesManifest } from '../../core/patch-manifest.js';
|
|
19
20
|
import { getPatchSourceProduct, getPatchSourceVersion } from '../../core/patch-source-metadata.js';
|
|
20
|
-
import {
|
|
21
|
+
import { getRebaseSessionPath, readRebaseSession, saveRebaseSession, } from '../../core/rebase-session.js';
|
|
21
22
|
import { GeneralError } from '../../errors/base.js';
|
|
22
|
-
import { RebaseSessionExistsError } from '../../errors/rebase.js';
|
|
23
|
-
import { pathExists } from '../../utils/fs.js';
|
|
23
|
+
import { CorruptRebaseSessionError, RebaseSessionExistsError } from '../../errors/rebase.js';
|
|
24
24
|
import { info, intro, outro, spinner } from '../../utils/logger.js';
|
|
25
25
|
import { commanderArgParser, pickDefined } from '../../utils/options.js';
|
|
26
26
|
import { parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
@@ -33,34 +33,24 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
33
33
|
const isDryRun = options.dryRun === true;
|
|
34
34
|
const maxFuzz = options.maxFuzz ?? 3;
|
|
35
35
|
intro(isDryRun ? 'FireForge Rebase (dry run)' : 'FireForge Rebase');
|
|
36
|
-
|
|
36
|
+
// One read decides liveness AND validity: a pathExists pre-probe here left
|
|
37
|
+
// a window where the file vanished between probe and read, reporting
|
|
38
|
+
// "already in progress" with no session on disk.
|
|
39
|
+
const existing = await readRebaseSession(projectRoot);
|
|
40
|
+
if (existing.present) {
|
|
41
|
+
// A corrupt session must not be reported as "already in progress": that
|
|
42
|
+
// message tells the operator to run --continue or --abort, and before
|
|
43
|
+
// 0.41.0 both of those then reported "no rebase session in progress".
|
|
44
|
+
if (!existing.valid) {
|
|
45
|
+
throw new CorruptRebaseSessionError(getRebaseSessionPath(projectRoot), existing.reason);
|
|
46
|
+
}
|
|
37
47
|
throw new RebaseSessionExistsError();
|
|
38
48
|
}
|
|
39
49
|
const paths = getProjectPaths(projectRoot);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
45
|
-
}
|
|
46
|
-
// 2026-04-24 eval Finding 11: `rebase --dry-run` used to print
|
|
47
|
-
// "Dry run complete" without validating that the engine had a valid
|
|
48
|
-
// HEAD. A previous `download --force` abort could leave `.git/`
|
|
49
|
-
// initialized but unborn (no baseline commit); the real rebase then
|
|
50
|
-
// failed immediately with `fatal: ambiguous argument 'HEAD'` on the
|
|
51
|
-
// first `git rev-parse HEAD` call. Replicate the same baseline check
|
|
52
|
-
// here so dry-run mirrors the real-run preconditions and operators
|
|
53
|
-
// cannot mistake a broken baseline for a ready-to-rebase tree.
|
|
54
|
-
try {
|
|
55
|
-
await getHead(paths.engine);
|
|
56
|
-
}
|
|
57
|
-
catch (err) {
|
|
58
|
-
if (isMissingHeadError(err)) {
|
|
59
|
-
throw new GeneralError('Engine repository has no baseline commit yet — a previous "fireforge download" was interrupted before git created the initial Firefox source commit. ' +
|
|
60
|
-
'Re-run "fireforge download --force" to recreate the baseline repository cleanly, then retry the rebase.');
|
|
61
|
-
}
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
50
|
+
// Keeps the rebase-specific tail on the unborn-HEAD remediation (2026-04-24
|
|
51
|
+
// eval Finding 11: --dry-run used to skip this check entirely, so the real
|
|
52
|
+
// run then failed on `git rev-parse HEAD`).
|
|
53
|
+
await assertEngineGitReady(paths.engine, { unbornHeadSuffix: ', then retry the rebase.' });
|
|
64
54
|
const config = await loadConfig(projectRoot);
|
|
65
55
|
const currentVersion = config.firefox.version;
|
|
66
56
|
const manifest = await loadPatchesManifest(paths.patches);
|
|
@@ -19,6 +19,7 @@ import { elapsedSince } from '../../utils/elapsed.js';
|
|
|
19
19
|
import { toError } from '../../utils/errors.js';
|
|
20
20
|
import { pathExists } from '../../utils/fs.js';
|
|
21
21
|
import { error, info, outro, spinner, success, warn } from '../../utils/logger.js';
|
|
22
|
+
import { isValidFirefoxVersion } from '../../utils/validation.js';
|
|
22
23
|
import { buildRebaseConflictSummary } from './conflict-summary.js';
|
|
23
24
|
import { printSummary } from './summary.js';
|
|
24
25
|
/**
|
|
@@ -164,6 +165,17 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
|
|
|
164
165
|
// succeeded on the patch side, so the operator is committing to the
|
|
165
166
|
// new source baseline; per-component health checking stays with
|
|
166
167
|
// `fireforge furnace validate` / `doctor --repair-furnace`.
|
|
168
|
+
//
|
|
169
|
+
// "Unconditional" means unconditional on component health, not on the value:
|
|
170
|
+
// `stampFurnaceOverrideBaseVersions` assigns and persists whatever string it
|
|
171
|
+
// is handed, so a session whose `toVersion` is not a real Firefox version
|
|
172
|
+
// would rewrite every override's baseline to that value. `isValidSession`
|
|
173
|
+
// now rejects such a session at read time; this refuses to persist it even
|
|
174
|
+
// if a future caller reaches here another way.
|
|
175
|
+
if (!isValidFirefoxVersion(session.toVersion)) {
|
|
176
|
+
throw new RebaseError(`Refusing to stamp Furnace override baseVersion(s): the rebase session's target version ` +
|
|
177
|
+
`("${session.toVersion}") is not a valid Firefox version.`);
|
|
178
|
+
}
|
|
167
179
|
try {
|
|
168
180
|
const overridesStamped = await stampFurnaceOverrideBaseVersions(projectRoot, session.toVersion);
|
|
169
181
|
if (overridesStamped > 0) {
|