nx 23.0.0-beta.20 → 23.0.0-beta.21

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.
@@ -1638,20 +1638,13 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1638
1638
  // render them distinctly per resolution mode.
1639
1639
  const migrationEmittedNextSteps = [];
1640
1640
  const skippedPrompts = [];
1641
- // One record per migration that returned without throwing. The failing
1642
- // migration (if any) has no record `outcomes.length` plus the implicit
1643
- // "the in-flight one threw" equals `migrationIndex` in the catch block.
1641
+ // One record per migration the loop touched. `status: 'completed'` records
1642
+ // are pushed at the end of each successful iteration; `status: 'aborted'`
1643
+ // is pushed by the catch block when a migration throws mid-iteration, so
1644
+ // `outcomes` is the single source of truth for the recap and tally — no
1645
+ // parallel "pending" list. `outcomes.length` always equals `migrationIndex`
1646
+ // after the loop body runs.
1644
1647
  const outcomes = [];
1645
- // Migrations whose own commits failed and whose diffs therefore sit
1646
- // uncommitted in the working tree. The next successful commit will absorb
1647
- // them via `git add -A`; we annotate that commit's body with this list
1648
- // and back-annotate the absorbed outcomes with `committedAsPartOf`.
1649
- //
1650
- // While non-empty, the working tree carries prior-migration state, so the
1651
- // `hasDiffContext` flag in the hybrid-agentic and validation-agentic
1652
- // prompt branches is suppressed (the prompt-only-with-agentic branch
1653
- // doesn't use `hasDiffContext`). See call sites below.
1654
- const pendingMigrations = [];
1655
1648
  // Prompt-only migrations whose agent never ran. Hybrid migrations with a
1656
1649
  // skipped prompt are NOT counted here — their deterministic half still ran.
1657
1650
  let notRunMigrationsCount = 0;
@@ -1659,69 +1652,72 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1659
1652
  ? 'deferred to the AI agent driving this run'
1660
1653
  : 'agentic flow disabled';
1661
1654
  const installDepsIfChanged = () => changedDepInstaller.installDepsIfChanged();
1662
- // Single funnel for per-migration commit attempts. Returns `{ sha, failed }`:
1663
- // - `failed: false, sha: string`: commit landed cleanly.
1664
- // - `failed: false, sha: null`: either (a) HEAD-resolve race (commit
1665
- // landed but `git rev-parse HEAD` failed transiently the diff
1666
- // cleared, sha unrecoverable), or (b) no commit attempted/needed
1667
- // (`--no-create-commits` or no diff to commit). Both are non-failure.
1668
- // - `failed: true, sha: null`: commit was attempted and errored. Diff
1669
- // retained in WT; pushed onto `pendingMigrations`.
1655
+ // Returns the migrations whose own commits failed and whose diffs are
1656
+ // still sitting in the working tree — derived live from `outcomes`. The
1657
+ // next successful commit absorbs them via `git add -A`; its commit body
1658
+ // lists them so a reader of `git log -p` can see which prior migrations'
1659
+ // diffs got pulled in.
1660
+ const pendingForCommitBody = () => outcomes
1661
+ .filter((o) => o.commit.kind === 'failed')
1662
+ .map((o) => ({ package: o.migration.package, name: o.migration.name }));
1663
+ // True while at least one prior migration's commit has failed and its
1664
+ // diff hasn't been absorbed yet. While true, the working tree carries
1665
+ // prior-migration state, so the `hasDiffContext` flag in the hybrid-
1666
+ // agentic and validation-agentic prompt branches is suppressed (the
1667
+ // prompt-only-with-agentic branch doesn't use `hasDiffContext`).
1668
+ const hasPendingCommitDebt = () => outcomes.some((o) => o.commit.kind === 'failed');
1669
+ // Single funnel for per-migration commit attempts. Returns the
1670
+ // `CommitState` to record on the migration's outcome. On `committed`,
1671
+ // back-annotates any prior failed-commit outcomes to `kind: 'absorbed'`
1672
+ // (their diffs were just rolled into this commit via `git add -A`).
1670
1673
  async function attemptMigrationCommit(m) {
1671
- let result;
1672
- try {
1673
- result = await (0, migrate_commits_1.commitMigrationIfRequested)(root, m, shouldCreateCommits, commitPrefix, installDepsIfChanged, pendingMigrations);
1674
- }
1675
- catch (err) {
1676
- // `commitMigrationIfRequested` awaits `installDepsIfChanged` before
1677
- // any commit attempt; a `runInstall` failure (e.g. NpmPeerDepsInstall)
1678
- // throws past it. Treat that as a commit failure for tracking
1679
- // purposes — the migration's diff is on disk, and pretending we never
1680
- // attempted would silently drop it from the retained-state recap and
1681
- // body annotations — then re-raise so the outer loop runs its
1682
- // failure path.
1683
- pendingMigrations.push({ package: m.package, name: m.name });
1684
- throw err;
1685
- }
1674
+ const pending = pendingForCommitBody();
1675
+ const result = await (0, migrate_commits_1.commitMigrationIfRequested)(root, m, shouldCreateCommits, commitPrefix, installDepsIfChanged, pending);
1686
1676
  if (result.status === 'committed') {
1687
- // The commit absorbed every pending migration's diff. Back-annotate
1688
- // the earlier outcome records so the failure recap can anchor them,
1689
- // then clear pending.
1677
+ // This commit absorbed every pending failed-commit migration's diff.
1678
+ // Transition their `commit.kind: 'failed'` records to `'absorbed'` so
1679
+ // the failure recap (if a later migration throws) can anchor them
1680
+ // and the retained-state filter no longer counts them as
1681
+ // uncommitted.
1690
1682
  //
1691
1683
  // The key is `package:name`; matching on `name` alone would conflate
1692
- // across packages. Guard `!o.committedAsPartOf` so a subsequent
1693
- // absorption-of-same-name cannot overwrite an earlier annotation.
1694
- if (pendingMigrations.length > 0) {
1695
- const absorbedKeys = new Set(pendingMigrations.map((p) => `${p.package}:${p.name}`));
1684
+ // across packages. Guard the kind check so a subsequent absorption-
1685
+ // of-same-name cannot overwrite an earlier annotation.
1686
+ if (pending.length > 0) {
1687
+ const absorbedKeys = new Set(pending.map((p) => `${p.package}:${p.name}`));
1696
1688
  for (const o of outcomes) {
1697
1689
  const key = `${o.migration.package}:${o.migration.name}`;
1698
- if (absorbedKeys.has(key) && !o.committedAsPartOf) {
1699
- o.committedAsPartOf = { name: m.name, sha: result.sha };
1690
+ if (absorbedKeys.has(key) && o.commit.kind === 'failed') {
1691
+ o.commit = {
1692
+ kind: 'absorbed',
1693
+ into: { name: m.name, sha: result.sha },
1694
+ };
1700
1695
  }
1701
1696
  }
1702
1697
  }
1703
- pendingMigrations.length = 0;
1704
- return { sha: result.sha, failed: false };
1698
+ return { kind: 'landed', sha: result.sha };
1705
1699
  }
1706
1700
  if (result.status === 'failed') {
1707
1701
  // Diff is still in WT. Subsequent prompts cannot claim git-isolation
1708
1702
  // until a later commit absorbs the backlog.
1709
- pendingMigrations.push({ package: m.package, name: m.name });
1710
- return { sha: null, failed: true };
1703
+ return { kind: 'failed' };
1711
1704
  }
1712
- // `no-changes` and `disabled` leave pending untouched and are not
1713
- // commit failures the user gets no surprise warning in the recap.
1714
- return { sha: null, failed: false };
1705
+ // `no-changes` and `disabled` no commit attempted, nothing to record
1706
+ // as a commit failure.
1707
+ return { kind: 'none' };
1715
1708
  }
1716
1709
  const totalMigrations = sortedMigrations.length;
1717
1710
  let migrationIndex = 0;
1718
1711
  for (const m of sortedMigrations) {
1719
1712
  migrationIndex++;
1720
1713
  (0, migrate_output_1.logMigrationBoundary)(migrationIndex, totalMigrations, m.package, m.name);
1714
+ // Snapshot the WT for before/after comparison in the catch block.
1715
+ // Content-sensitive so a dirty→dirty case (this migration mutating an
1716
+ // already-dirty shared file like `package.json`) doesn't collapse.
1717
+ const baselineWorkingTreeSnapshot = (0, git_utils_1.getUncommittedChangesSnapshot)(root);
1721
1718
  try {
1722
1719
  let outcome;
1723
- let committedSha = null;
1724
- let commitFailed = false;
1720
+ let commit = { kind: 'none' };
1725
1721
  if ((0, migration_shape_1.isPromptOnlyMigration)(m)) {
1726
1722
  if (agenticRun) {
1727
1723
  const stepResult = await agenticRun.runStep({
@@ -1731,9 +1727,8 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1731
1727
  runDir: agenticRun.runDir,
1732
1728
  installDepsIfChanged,
1733
1729
  });
1734
- ({ sha: committedSha, failed: commitFailed } =
1735
- await attemptMigrationCommit(m));
1736
- (0, migrate_output_1.logAgenticSuccessOutcome)(stepResult.ambiguous ? 'Marked complete by user' : 'Applied', committedSha, stepResult.summary);
1730
+ commit = await attemptMigrationCommit(m);
1731
+ (0, migrate_output_1.logAgenticSuccessOutcome)(stepResult.ambiguous ? 'Marked complete by user' : 'Applied', commit.kind === 'landed' ? commit.sha : null, stepResult.summary);
1737
1732
  outcome = 'applied';
1738
1733
  }
1739
1734
  else {
@@ -1765,12 +1760,11 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1765
1760
  // When prior commits failed, the working tree carries their
1766
1761
  // diff. The git-inspect path of the prompt would mislead the
1767
1762
  // agent in that case; fall back to embedded `<files_changed>`.
1768
- hasDiffContext: agenticHasDiffContext && pendingMigrations.length === 0,
1763
+ hasDiffContext: agenticHasDiffContext && !hasPendingCommitDebt(),
1769
1764
  },
1770
1765
  });
1771
- ({ sha: committedSha, failed: commitFailed } =
1772
- await attemptMigrationCommit(m));
1773
- (0, migrate_output_1.logAgenticSuccessOutcome)(stepResult.ambiguous ? 'Marked complete by user' : 'Applied', committedSha, stepResult.summary);
1766
+ commit = await attemptMigrationCommit(m);
1767
+ (0, migrate_output_1.logAgenticSuccessOutcome)(stepResult.ambiguous ? 'Marked complete by user' : 'Applied', commit.kind === 'landed' ? commit.sha : null, stepResult.summary);
1774
1768
  outcome = 'applied';
1775
1769
  }
1776
1770
  else {
@@ -1794,11 +1788,10 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1794
1788
  // diffs — confusing `git log` / `git blame` attribution. Pending
1795
1789
  // stays pending and the next change-producing migration absorbs.
1796
1790
  if (madeChanges) {
1797
- ({ sha: committedSha, failed: commitFailed } =
1798
- await attemptMigrationCommit(m));
1791
+ commit = await attemptMigrationCommit(m);
1799
1792
  }
1800
- if (committedSha) {
1801
- logger_1.logger.info(pc.dim(`Committed as ${committedSha}`));
1793
+ if (commit.kind === 'landed' && commit.sha) {
1794
+ logger_1.logger.info(pc.dim(`Committed as ${commit.sha}`));
1802
1795
  }
1803
1796
  outcome = 'deferred';
1804
1797
  }
@@ -1826,16 +1819,15 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1826
1819
  changes,
1827
1820
  agentContext,
1828
1821
  // See the hybrid agentic branch above for the rationale on
1829
- // why `pendingMigrations` gates git-inspect context.
1830
- hasDiffContext: agenticHasDiffContext && pendingMigrations.length === 0,
1822
+ // why pending commit debt gates git-inspect context.
1823
+ hasDiffContext: agenticHasDiffContext && !hasPendingCommitDebt(),
1831
1824
  },
1832
1825
  mode: 'generic-validation',
1833
1826
  });
1834
- ({ sha: committedSha, failed: commitFailed } =
1835
- await attemptMigrationCommit(m));
1827
+ commit = await attemptMigrationCommit(m);
1836
1828
  (0, migrate_output_1.logAgenticSuccessOutcome)(stepResult.ambiguous
1837
1829
  ? 'Marked complete by user'
1838
- : 'Validation passed', committedSha, stepResult.summary);
1830
+ : 'Validation passed', commit.kind === 'landed' ? commit.sha : null, stepResult.summary);
1839
1831
  outcome = 'applied';
1840
1832
  }
1841
1833
  else {
@@ -1849,10 +1841,9 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1849
1841
  outcome = 'no-changes';
1850
1842
  }
1851
1843
  else {
1852
- ({ sha: committedSha, failed: commitFailed } =
1853
- await attemptMigrationCommit(m));
1854
- if (committedSha) {
1855
- logger_1.logger.info(pc.dim(`Committed as ${committedSha}`));
1844
+ commit = await attemptMigrationCommit(m);
1845
+ if (commit.kind === 'landed' && commit.sha) {
1846
+ logger_1.logger.info(pc.dim(`Committed as ${commit.sha}`));
1856
1847
  }
1857
1848
  outcome = 'applied';
1858
1849
  }
@@ -1860,13 +1851,26 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1860
1851
  }
1861
1852
  outcomes.push({
1862
1853
  migration: { package: m.package, name: m.name },
1863
- outcome,
1864
- committedSha,
1865
- commitFailed,
1854
+ status: 'completed',
1855
+ kind: outcome,
1856
+ commit,
1866
1857
  });
1867
1858
  logger_1.logger.info('');
1868
1859
  }
1869
1860
  catch (e) {
1861
+ // Record the in-flight migration as `aborted` so the recap and tally
1862
+ // see it. `commit: 'failed'` requires both: (1) commits were
1863
+ // requested — otherwise the "could not be created" recap line is
1864
+ // false; (2) the WT snapshot diverged from the iteration baseline —
1865
+ // net-new state, not the pre-existing pending diff. Else `'none'`.
1866
+ const leftNewDiff = (0, git_utils_1.getUncommittedChangesSnapshot)(root) !== baselineWorkingTreeSnapshot;
1867
+ outcomes.push({
1868
+ migration: { package: m.package, name: m.name },
1869
+ status: 'aborted',
1870
+ commit: shouldCreateCommits && leftNewDiff
1871
+ ? { kind: 'failed' }
1872
+ : { kind: 'none' },
1873
+ });
1870
1874
  if (!(e instanceof NpmPeerDepsInstallError)) {
1871
1875
  // `withGeneratorOutputCapture` attaches the generator's `console.*`
1872
1876
  // output as `capturedLogs` (best-effort; may be absent). Surface it
@@ -1887,7 +1891,6 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1887
1891
  migrationIndex,
1888
1892
  totalMigrations,
1889
1893
  outcomes,
1890
- pendingMigrations,
1891
1894
  migrationEmittedNextSteps,
1892
1895
  insideAgent: agentic?.kind === 'inside-agent',
1893
1896
  });
@@ -1914,12 +1917,12 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1914
1917
  nextSteps: combinedNextSteps,
1915
1918
  skippedPrompts,
1916
1919
  migrationEmittedNextSteps,
1917
- committedShasCount: outcomes.filter((o) => o.committedSha).length,
1920
+ committedShasCount: (0, migrate_output_1.countLandedCommits)(outcomes),
1918
1921
  // Migrations whose commits failed and never got absorbed by a later
1919
1922
  // commit. The caller surfaces them so a successful run doesn't claim
1920
1923
  // "up to date" while leaving uncommitted diffs in the working tree.
1921
1924
  // Formatted as `package: name` for direct display.
1922
- retainedAtSuccess: pendingMigrations.map((p) => `${p.package}: ${p.name}`),
1925
+ retainedAtSuccess: (0, migrate_output_1.retainedMigrations)(outcomes).map((p) => `${p.package}: ${p.name}`),
1923
1926
  };
1924
1927
  }
1925
1928
  class ChangedDepInstaller {
@@ -46,13 +46,21 @@ async function runMigrationProcess() {
46
46
  );
47
47
 
48
48
  if (configuration.createCommits) {
49
- await commitMigrationIfRequested(
49
+ const commitResult = await commitMigrationIfRequested(
50
50
  workspacePath,
51
51
  migration,
52
52
  true,
53
53
  configuration.commitPrefix,
54
54
  installDepsIfChanged
55
55
  );
56
+ if (commitResult.status === 'failed') {
57
+ // Single-migration UI child surfaces the failure via stdout (logged
58
+ // inside commitMigrationIfRequested) and continues with success-
59
+ // with-warning. The executor's absorption flow does not apply here:
60
+ // there is no later migration that could pick up this migration's
61
+ // diff, so the working tree is left in its post-migration state
62
+ // for the user to commit or revert through the UI.
63
+ }
56
64
  } else {
57
65
  await installDepsIfChanged();
58
66
  }