@webpieces/pr-gate 0.3.201 → 0.3.203

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.
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_BUILD_COMMAND = void 0;
3
+ exports.BuildGateOptions = exports.DEFAULT_BUILD_COMMAND = void 0;
4
4
  exports.resolveBuildCommand = resolveBuildCommand;
5
5
  exports.runBuildAffected = runBuildAffected;
6
6
  exports.runConfiguredBuildGate = runConfiguredBuildGate;
7
+ exports.runBuildGate = runBuildGate;
7
8
  const child_process_1 = require("child_process");
8
9
  const rules_config_1 = require("@webpieces/rules-config");
9
10
  // Single source of truth for the build gate. Both `wp-build-affected` (CI + local) and the
@@ -44,4 +45,41 @@ function runBuildAffected(repoRoot, buildCommand) {
44
45
  function runConfiguredBuildGate(repoRoot) {
45
46
  return runBuildAffected(repoRoot, (0, rules_config_1.loadAndValidate)(repoRoot).prGate.buildCommand);
46
47
  }
48
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
49
+ /**
50
+ * The two facts that differ between the wp-start (advisory) and wp-finish (authoritative) build
51
+ * gates. Everything else — resolve command, run it, print the re-run hint, exit non-zero on failure
52
+ * — is identical, so it lives once in runBuildGate and the difference is passed in here. A class (not
53
+ * an object literal) per the codebase's data-structure convention.
54
+ */
55
+ class BuildGateOptions {
56
+ label; // section header shown above the gate
57
+ rerunCommand; // command the AI re-runs after fixing the build
58
+ failureHeadline; // first line printed on failure
59
+ constructor(label, rerunCommand, failureHeadline) {
60
+ this.label = label;
61
+ this.rerunCommand = rerunCommand;
62
+ this.failureHeadline = failureHeadline;
63
+ }
64
+ }
65
+ exports.BuildGateOptions = BuildGateOptions;
66
+ /**
67
+ * Run the configured build gate with consistent framing, exiting the process on failure. Single
68
+ * source of truth: wp-start-upsert-pr and wp-finish-upsert-pr both call THIS (only the BuildGateOptions
69
+ * differ) so a fix to the gate flow is made once, not in two near-identical copies.
70
+ */
71
+ function runBuildGate(repoRoot, opts) {
72
+ const buildCommand = resolveBuildCommand(repoRoot);
73
+ process.stdout.write('\n' + SEP + opts.label + '\n' + SEP + '\n');
74
+ process.stdout.write(`Running the build gate. To get it passing, run the SAME command yourself and fix everything it reports:\n\n` +
75
+ ` ${buildCommand}\n\n`);
76
+ const buildCode = runConfiguredBuildGate(repoRoot);
77
+ if (buildCode !== 0) {
78
+ process.stderr.write(`\n❌ ${opts.failureHeadline}\n\n` +
79
+ `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\n\n` +
80
+ ` ${buildCommand}\n\n`);
81
+ process.exit(buildCode);
82
+ }
83
+ process.stdout.write('\n✅ Build passed.\n');
84
+ }
47
85
  //# sourceMappingURL=build-affected.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":";;;AAkBA,kDAGC;AAOD,4CAKC;AAQD,wDAEC;AA3CD,iDAA0C;AAC1C,0DAA0D;AAE1D,2FAA2F;AAC3F,0FAA0F;AAC1F,0FAA0F;AAC1F,kCAAkC;AAClC,gGAAgG;AAChG,kGAAkG;AAClG,kGAAkG;AAClG,0FAA0F;AAC7E,QAAA,qBAAqB,GAAG,wEAAwE,CAAC;AAE9G;;;;GAIG;AACH,SAAgB,mBAAmB,CAAC,QAAgB;IAChD,MAAM,UAAU,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;IACjE,OAAO,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,6BAAqB,CAAC;AACrG,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,QAAgB,EAAE,YAAqB;IACpE,MAAM,GAAG,GAAG,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,6BAAqB,CAAC;IAC5G,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,GAAG,MAAM,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChF,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;GAKG;AACH,SAAgB,sBAAsB,CAAC,QAAgB;IACnD,OAAO,gBAAgB,CAAC,QAAQ,EAAE,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;AACrF,CAAC","sourcesContent":["import { spawnSync } from 'child_process';\nimport { loadAndValidate } from '@webpieces/rules-config';\n\n// Single source of truth for the build gate. Both `wp-build-affected` (CI + local) and the\n// merge validation gate (`wp-finish-upsert-pr`) run THIS, so \"what CI runs\" and \"what the\n// PR command runs\" can never drift. nx `affected` only rebuilds changed projects, so this\n// stays fast on a large monorepo.\n// `--base=$(git merge-base origin/main HEAD)` (the fork point) instead of `--base=origin/main`:\n// origin/main rebuilds projects touched by OTHER people's merged PRs (your branch still holds the\n// pre-merge versions, so nx sees them as \"affected\"). The fork point scopes affected to only YOUR\n// branch's changes. The `$(...)` resolves because runBuildAffected runs with shell: true.\nexport const DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=$(git merge-base origin/main HEAD)';\n\n/**\n * Resolve the exact build command this gate will run for a repo: the project's configured\n * PrGateConfig.buildCommand, or the default affected-ci command when none is set. Callers\n * print this so the AI knows precisely which command to run locally to get the gate passing.\n */\nexport function resolveBuildCommand(repoRoot: string): string {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n return configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n}\n\n/**\n * Run the build gate. Returns the process exit code (0 = pass). `buildCommand` overrides\n * the default (sourced from PrGateConfig.buildCommand by callers); empty/undefined uses the\n * default affected-ci command.\n */\nexport function runBuildAffected(repoRoot: string, buildCommand?: string): number {\n const cmd = buildCommand !== undefined && buildCommand.trim() !== '' ? buildCommand : DEFAULT_BUILD_COMMAND;\n process.stdout.write(`\\n▶ Build gate: ${cmd}\\n\\n`);\n const result = spawnSync(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });\n return result.status ?? 1;\n}\n\n/**\n * Run the build gate using the project's configured command (PrGateConfig.buildCommand). The\n * finish-upsert-pr gate and wp-start-upsert-pr both call THIS so they provably build with the same\n * command — the resolution the AI validates is built exactly as the PR command builds it.\n * Returns the exit code; callers print their own re-run hint.\n */\nexport function runConfiguredBuildGate(repoRoot: string): number {\n return runBuildAffected(repoRoot, loadAndValidate(repoRoot).prGate.buildCommand);\n}\n"]}
1
+ {"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":";;;AAkBA,kDAGC;AAOD,4CAKC;AAQD,wDAEC;AA2BD,oCAiBC;AAvFD,iDAA0C;AAC1C,0DAA0D;AAE1D,2FAA2F;AAC3F,0FAA0F;AAC1F,0FAA0F;AAC1F,kCAAkC;AAClC,gGAAgG;AAChG,kGAAkG;AAClG,kGAAkG;AAClG,0FAA0F;AAC7E,QAAA,qBAAqB,GAAG,wEAAwE,CAAC;AAE9G;;;;GAIG;AACH,SAAgB,mBAAmB,CAAC,QAAgB;IAChD,MAAM,UAAU,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;IACjE,OAAO,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,6BAAqB,CAAC;AACrG,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,QAAgB,EAAE,YAAqB;IACpE,MAAM,GAAG,GAAG,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,6BAAqB,CAAC;IAC5G,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,GAAG,MAAM,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChF,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;GAKG;AACH,SAAgB,sBAAsB,CAAC,QAAgB;IACnD,OAAO,gBAAgB,CAAC,QAAQ,EAAE,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;;GAKG;AACH,MAAa,gBAAgB;IACzB,KAAK,CAAS,CAAY,sCAAsC;IAChE,YAAY,CAAS,CAAK,gDAAgD;IAC1E,eAAe,CAAS,CAAE,gCAAgC;IAE1D,YAAY,KAAa,EAAE,YAAoB,EAAE,eAAuB;QACpE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IAC3C,CAAC;CACJ;AAVD,4CAUC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAAC,QAAgB,EAAE,IAAsB;IACjE,MAAM,YAAY,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACnD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;IAClE,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,6GAA6G;QAC7G,OAAO,YAAY,MAAM,CAC5B,CAAC;IACF,MAAM,SAAS,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IACnD,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;QAClB,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,OAAO,IAAI,CAAC,eAAe,MAAM;YACjC,uEAAuE,IAAI,CAAC,YAAY,OAAO;YAC/F,OAAO,YAAY,MAAM,CAC5B,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;AAChD,CAAC","sourcesContent":["import { spawnSync } from 'child_process';\nimport { loadAndValidate } from '@webpieces/rules-config';\n\n// Single source of truth for the build gate. Both `wp-build-affected` (CI + local) and the\n// merge validation gate (`wp-finish-upsert-pr`) run THIS, so \"what CI runs\" and \"what the\n// PR command runs\" can never drift. nx `affected` only rebuilds changed projects, so this\n// stays fast on a large monorepo.\n// `--base=$(git merge-base origin/main HEAD)` (the fork point) instead of `--base=origin/main`:\n// origin/main rebuilds projects touched by OTHER people's merged PRs (your branch still holds the\n// pre-merge versions, so nx sees them as \"affected\"). The fork point scopes affected to only YOUR\n// branch's changes. The `$(...)` resolves because runBuildAffected runs with shell: true.\nexport const DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=$(git merge-base origin/main HEAD)';\n\n/**\n * Resolve the exact build command this gate will run for a repo: the project's configured\n * PrGateConfig.buildCommand, or the default affected-ci command when none is set. Callers\n * print this so the AI knows precisely which command to run locally to get the gate passing.\n */\nexport function resolveBuildCommand(repoRoot: string): string {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n return configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n}\n\n/**\n * Run the build gate. Returns the process exit code (0 = pass). `buildCommand` overrides\n * the default (sourced from PrGateConfig.buildCommand by callers); empty/undefined uses the\n * default affected-ci command.\n */\nexport function runBuildAffected(repoRoot: string, buildCommand?: string): number {\n const cmd = buildCommand !== undefined && buildCommand.trim() !== '' ? buildCommand : DEFAULT_BUILD_COMMAND;\n process.stdout.write(`\\n▶ Build gate: ${cmd}\\n\\n`);\n const result = spawnSync(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });\n return result.status ?? 1;\n}\n\n/**\n * Run the build gate using the project's configured command (PrGateConfig.buildCommand). The\n * finish-upsert-pr gate and wp-start-upsert-pr both call THIS so they provably build with the same\n * command — the resolution the AI validates is built exactly as the PR command builds it.\n * Returns the exit code; callers print their own re-run hint.\n */\nexport function runConfiguredBuildGate(repoRoot: string): number {\n return runBuildAffected(repoRoot, loadAndValidate(repoRoot).prGate.buildCommand);\n}\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The two facts that differ between the wp-start (advisory) and wp-finish (authoritative) build\n * gates. Everything else — resolve command, run it, print the re-run hint, exit non-zero on failure\n * — is identical, so it lives once in runBuildGate and the difference is passed in here. A class (not\n * an object literal) per the codebase's data-structure convention.\n */\nexport class BuildGateOptions {\n label: string; // section header shown above the gate\n rerunCommand: string; // command the AI re-runs after fixing the build\n failureHeadline: string; // first line printed on failure\n\n constructor(label: string, rerunCommand: string, failureHeadline: string) {\n this.label = label;\n this.rerunCommand = rerunCommand;\n this.failureHeadline = failureHeadline;\n }\n}\n\n/**\n * Run the configured build gate with consistent framing, exiting the process on failure. Single\n * source of truth: wp-start-upsert-pr and wp-finish-upsert-pr both call THIS (only the BuildGateOptions\n * differ) so a fix to the gate flow is made once, not in two near-identical copies.\n */\nexport function runBuildGate(repoRoot: string, opts: BuildGateOptions): void {\n const buildCommand = resolveBuildCommand(repoRoot);\n process.stdout.write('\\n' + SEP + opts.label + '\\n' + SEP + '\\n');\n process.stdout.write(\n `Running the build gate. To get it passing, run the SAME command yourself and fix everything it reports:\\n\\n` +\n ` ${buildCommand}\\n\\n`,\n );\n const buildCode = runConfiguredBuildGate(repoRoot);\n if (buildCode !== 0) {\n process.stderr.write(\n `\\n❌ ${opts.failureHeadline}\\n\\n` +\n `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\\n\\n` +\n ` ${buildCommand}\\n\\n`,\n );\n process.exit(buildCode);\n }\n process.stdout.write('\\n✅ Build passed.\\n');\n}\n"]}
@@ -4,3 +4,9 @@
4
4
  * would operate on stale or wrong state.
5
5
  */
6
6
  export declare function runGitChecked(args: string[], errMsg: string): void;
7
+ /**
8
+ * Push HEAD to origin/<currentBranch>. Single source of truth shared by wp-start-upsert-pr and
9
+ * wp-finish-upsert-pr (they pushed with identical copy-pasted logic). Uses --force-with-lease for an
10
+ * existing remote branch (the 3-point squash rewrites history) and -u for a brand-new branch.
11
+ */
12
+ export declare function ensurePushed(currentBranch: string): void;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runGitChecked = runGitChecked;
4
+ exports.ensurePushed = ensurePushed;
4
5
  const child_process_1 = require("child_process");
5
6
  /**
6
7
  * Run a git command that is expected to succeed; abort the process with a clear message
@@ -14,4 +15,18 @@ function runGitChecked(args, errMsg) {
14
15
  process.exit(1);
15
16
  }
16
17
  }
18
+ /**
19
+ * Push HEAD to origin/<currentBranch>. Single source of truth shared by wp-start-upsert-pr and
20
+ * wp-finish-upsert-pr (they pushed with identical copy-pasted logic). Uses --force-with-lease for an
21
+ * existing remote branch (the 3-point squash rewrites history) and -u for a brand-new branch.
22
+ */
23
+ function ensurePushed(currentBranch) {
24
+ const remoteExists = (0, child_process_1.spawnSync)('git', ['ls-remote', '--exit-code', '--heads', 'origin', currentBranch]).status === 0;
25
+ if (remoteExists) {
26
+ runGitChecked(['push', '--force-with-lease', 'origin', `HEAD:${currentBranch}`], 'Failed to push branch');
27
+ }
28
+ else {
29
+ runGitChecked(['push', '-u', 'origin', `HEAD:${currentBranch}`], 'Failed to push new branch');
30
+ }
31
+ }
17
32
  //# sourceMappingURL=git-exec.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"git-exec.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-exec.ts"],"names":[],"mappings":";;AAOA,sCAMC;AAbD,iDAA0C;AAE1C;;;;GAIG;AACH,SAAgB,aAAa,CAAC,IAAc,EAAE,MAAc;IACxD,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAC5D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,SAAS,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACL,CAAC","sourcesContent":["import { spawnSync } from 'child_process';\n\n/**\n * Run a git command that is expected to succeed; abort the process with a clear message\n * if it fails. Used for fetch/pull/checkout/commit where silently continuing on failure\n * would operate on stale or wrong state.\n */\nexport function runGitChecked(args: string[], errMsg: string): void {\n const result = spawnSync('git', args, { stdio: 'inherit' });\n if (result.status !== 0) {\n process.stderr.write(`❌ ${errMsg} (git ${args.join(' ')} exited ${String(result.status)})\\n`);\n process.exit(1);\n }\n}\n"]}
1
+ {"version":3,"file":"git-exec.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-exec.ts"],"names":[],"mappings":";;AAOA,sCAMC;AAOD,oCAOC;AA3BD,iDAA0C;AAE1C;;;;GAIG;AACH,SAAgB,aAAa,CAAC,IAAc,EAAE,MAAc;IACxD,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAC5D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,SAAS,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAAC,aAAqB;IAC9C,MAAM,YAAY,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;IACrH,IAAI,YAAY,EAAE,CAAC;QACf,aAAa,CAAC,CAAC,MAAM,EAAE,oBAAoB,EAAE,QAAQ,EAAE,QAAQ,aAAa,EAAE,CAAC,EAAE,uBAAuB,CAAC,CAAC;IAC9G,CAAC;SAAM,CAAC;QACJ,aAAa,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,aAAa,EAAE,CAAC,EAAE,2BAA2B,CAAC,CAAC;IAClG,CAAC;AACL,CAAC","sourcesContent":["import { spawnSync } from 'child_process';\n\n/**\n * Run a git command that is expected to succeed; abort the process with a clear message\n * if it fails. Used for fetch/pull/checkout/commit where silently continuing on failure\n * would operate on stale or wrong state.\n */\nexport function runGitChecked(args: string[], errMsg: string): void {\n const result = spawnSync('git', args, { stdio: 'inherit' });\n if (result.status !== 0) {\n process.stderr.write(`❌ ${errMsg} (git ${args.join(' ')} exited ${String(result.status)})\\n`);\n process.exit(1);\n }\n}\n\n/**\n * Push HEAD to origin/<currentBranch>. Single source of truth shared by wp-start-upsert-pr and\n * wp-finish-upsert-pr (they pushed with identical copy-pasted logic). Uses --force-with-lease for an\n * existing remote branch (the 3-point squash rewrites history) and -u for a brand-new branch.\n */\nexport function ensurePushed(currentBranch: string): void {\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', currentBranch]).status === 0;\n if (remoteExists) {\n runGitChecked(['push', '--force-with-lease', 'origin', `HEAD:${currentBranch}`], 'Failed to push branch');\n } else {\n runGitChecked(['push', '-u', 'origin', `HEAD:${currentBranch}`], 'Failed to push new branch');\n }\n}\n"]}
@@ -8,6 +8,7 @@ const fs = tslib_1.__importStar(require("fs"));
8
8
  const path = tslib_1.__importStar(require("path"));
9
9
  const rules_config_1 = require("@webpieces/rules-config");
10
10
  const git_readAiBranchName_1 = require("./git-readAiBranchName");
11
+ const merge_state_1 = require("./merge-state");
11
12
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
12
13
  function printMergeFromMainError(commit, parent, featureName, currentBranch) {
13
14
  process.stderr.write('\n');
@@ -75,7 +76,14 @@ async function findForkPoint(workflow) {
75
76
  const featureName = (0, git_readAiBranchName_1.getFeatureName)();
76
77
  const currentBranch = (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
77
78
  const repoRoot = (0, child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
78
- const outputDir = path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, `${workflow}-${featureName}`);
79
+ // Single source of truth for the per-feature dir — the SAME nested home the readers use
80
+ // (git-gatherInfo / merge-start read updatemain-hashes.json from mergeDirFor). Previously this
81
+ // wrote to the legacy flat `.webpieces/<workflow>-<feature>/` while readers looked under
82
+ // `.webpieces/merge-info/<feature>/`, so the hash file was written to one path and read from
83
+ // another. Route both through mergeDirFor/prDirFor so they can never diverge again.
84
+ const outputDir = workflow === 'review'
85
+ ? (0, rules_config_1.prDirFor)(repoRoot, featureName)
86
+ : (0, merge_state_1.mergeDirFor)(repoRoot, featureName);
79
87
  const prefix = workflow === 'review' ? 'review-' : 'updatemain-';
80
88
  fs.mkdirSync(outputDir, { recursive: true });
81
89
  (0, child_process_1.spawnSync)('git', ['fetch', 'origin', 'main'], { stdio: 'ignore' });
@@ -1 +1 @@
1
- {"version":3,"file":"git-findForkPoint.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-findForkPoint.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { WEBPIECES_TMP_DIR } from '@webpieces/rules-config';\nimport { getFeatureName } from './git-readAiBranchName';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\nfunction printMergeFromMainError(commit: string, parent: string, featureName: string, currentBranch: string): void {\n process.stderr.write('\\n');\n process.stderr.write(SEP);\n process.stderr.write('❌ This branch merged main without pnpm wp-git-update\\n');\n process.stderr.write(SEP);\n process.stderr.write('\\n');\n process.stderr.write(`Merge commit detected: ${commit}\\n`);\n process.stderr.write(`Parent from main: ${parent}\\n`);\n process.stderr.write('\\n');\n process.stderr.write('This prevents clean squash-merge. To recover, follow these steps:\\n');\n process.stderr.write('\\n');\n process.stderr.write('1. Switch to main branch:\\n');\n process.stderr.write(' git checkout main\\n');\n process.stderr.write('\\n');\n process.stderr.write('2. Pull latest changes:\\n');\n process.stderr.write(' git pull\\n');\n process.stderr.write('\\n');\n process.stderr.write('3. Create new branch with a new name:\\n');\n process.stderr.write(` git checkout -b ${featureName}-v2\\n`);\n process.stderr.write('\\n');\n process.stderr.write('4. Squash merge your old branch:\\n');\n process.stderr.write(` git merge --squash ${currentBranch}\\n`);\n process.stderr.write('\\n');\n process.stderr.write('5. Commit the squashed changes:\\n');\n process.stderr.write(` git add -A && git commit -m \"Squashed from ${currentBranch}\"\\n`);\n process.stderr.write('\\n');\n process.stderr.write('6. If you have an existing PR:\\n');\n process.stderr.write(` - Create a NEW PR for ${featureName}-v2\\n`);\n process.stderr.write(` - Close the old PR for ${currentBranch}\\n`);\n process.stderr.write('\\n');\n process.stderr.write(SEP);\n process.stderr.write('\\n');\n}\n\nfunction checkMergeCommits(mergeCommits: string[], outputDir: string, prefix: string, featureName: string, currentBranch: string): void {\n process.stderr.write(`Found ${mergeCommits.length} merge commit(s) to check...\\n`);\n\n for (const commit of mergeCommits) {\n const parentsResult = spawnSync('git', ['rev-list', '--parents', '-n', '1', commit], { encoding: 'utf8' });\n const parents = (parentsResult.stdout ?? '').trim().split(' ').slice(1);\n\n for (const parent of parents) {\n const ancestorCheck = spawnSync('git', ['merge-base', '--is-ancestor', parent, 'origin/main']);\n const reverseCheck = spawnSync('git', ['merge-base', '--is-ancestor', 'origin/main', parent]);\n\n if (ancestorCheck.status === 0 && reverseCheck.status === 0) {\n const errorJson = JSON.stringify({\n error: 'Merge from main detected',\n mergeCommit: commit,\n parentFromMain: parent,\n timestamp: new Date().toISOString(),\n }, null, 2);\n\n fs.writeFileSync(path.join(outputDir, `${prefix}forkpoint-error.json`), errorJson + '\\n');\n printMergeFromMainError(commit, parent, featureName, currentBranch);\n process.exit(1);\n }\n }\n }\n process.stderr.write('✅ No improper merges from main detected\\n');\n}\n\nexport async function findForkPoint(workflow: string): Promise<void> {\n if (workflow !== 'review' && workflow !== 'merge') {\n process.stderr.write('ERROR: Workflow argument required\\n');\n process.stderr.write('Usage: git-findForkPoint <workflow>\\n');\n process.stderr.write(\" workflow: 'review' or 'merge'\\n\");\n process.exit(1);\n }\n\n const featureName = getFeatureName();\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n\n const outputDir = path.join(repoRoot, WEBPIECES_TMP_DIR, `${workflow}-${featureName}`);\n const prefix = workflow === 'review' ? 'review-' : 'updatemain-';\n fs.mkdirSync(outputDir, { recursive: true });\n\n spawnSync('git', ['fetch', 'origin', 'main'], { stdio: 'ignore' });\n\n const featureHead = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();\n const originMain = execSync('git rev-parse origin/main', { encoding: 'utf8' }).trim();\n\n process.stderr.write('Finding fork point using git merge-base...\\n');\n\n const forkPointResult = spawnSync('git', ['merge-base', 'origin/main', 'HEAD'], { encoding: 'utf8' });\n const forkPoint = (forkPointResult.stdout ?? '').trim();\n\n if (!forkPoint || forkPointResult.status !== 0) {\n process.stderr.write('ERROR: Could not find common ancestor with origin/main\\n');\n process.exit(1);\n }\n\n process.stderr.write(`✅ Fork point found: ${forkPoint.slice(0, 7)}\\n`);\n process.stderr.write('Checking for improper merges from main...\\n');\n\n const mergeCommitsResult = spawnSync('git', ['log', `${forkPoint}..HEAD`, '--merges', '--format=%H'], { encoding: 'utf8' });\n const mergeCommitsRaw = (mergeCommitsResult.stdout ?? '').trim();\n const mergeCommits = mergeCommitsRaw ? mergeCommitsRaw.split('\\n') : [];\n\n if (mergeCommits.length > 0) {\n checkMergeCommits(mergeCommits, outputDir, prefix, featureName, currentBranch);\n } else {\n process.stderr.write('✅ No merge commits found (clean history)\\n');\n }\n\n const hashesJson = JSON.stringify({\n hashForkPoint: forkPoint,\n hashFeatureHead: featureHead,\n hashMainHead: originMain,\n timestamp: new Date().toISOString(),\n }, null, 2);\n\n fs.writeFileSync(path.join(outputDir, `${prefix}hashes.json`), hashesJson + '\\n');\n process.stderr.write(`✅ Hash points written to: ${outputDir}/${prefix}hashes.json\\n`);\n}\n\nexport async function main(): Promise<void> {\n await findForkPoint(process.argv[2] ?? '');\n}\n\nif (require.main === module) {\n main().catch((err) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(message + '\\n');\n process.exit(1);\n });\n}\n"]}
1
+ {"version":3,"file":"git-findForkPoint.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-findForkPoint.ts"],"names":[],"mappings":";;AAuEA,sCA4DC;AAED,oBAEC;;AAvID,iDAAoD;AACpD,+CAAyB;AACzB,mDAA6B;AAC7B,0DAAmD;AACnD,iEAAwD;AACxD,+CAA4C;AAE5C,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,SAAS,uBAAuB,CAAC,MAAc,EAAE,MAAc,EAAE,WAAmB,EAAE,aAAqB;IACvG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC/E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0BAA0B,MAAM,IAAI,CAAC,CAAC;IAC3D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0BAA0B,MAAM,IAAI,CAAC,CAAC;IAC3D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;IAC5F,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACpD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC/C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAClD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAChE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sBAAsB,WAAW,OAAO,CAAC,CAAC;IAC/D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;IAC3D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,aAAa,IAAI,CAAC,CAAC;IACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;IAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iDAAiD,aAAa,KAAK,CAAC,CAAC;IAC1F,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACzD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4BAA4B,WAAW,OAAO,CAAC,CAAC;IACrE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,aAAa,IAAI,CAAC,CAAC;IACrE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,iBAAiB,CAAC,YAAsB,EAAE,SAAiB,EAAE,MAAc,EAAE,WAAmB,EAAE,aAAqB;IAC5H,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,YAAY,CAAC,MAAM,gCAAgC,CAAC,CAAC;IAEnF,KAAK,MAAM,MAAM,IAAI,YAAY,EAAE,CAAC;QAChC,MAAM,aAAa,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,UAAU,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAC3G,MAAM,OAAO,GAAG,CAAC,aAAa,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAExE,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC3B,MAAM,aAAa,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,YAAY,EAAE,eAAe,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;YAC/F,MAAM,YAAY,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;YAE9F,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;oBAC7B,KAAK,EAAE,0BAA0B;oBACjC,WAAW,EAAE,MAAM;oBACnB,cAAc,EAAE,MAAM;oBACtB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;iBACtC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;gBAEZ,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,MAAM,sBAAsB,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC,CAAC;gBAC1F,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC;gBACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACpB,CAAC;QACL,CAAC;IACL,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;AACtE,CAAC;AAEM,KAAK,UAAU,aAAa,CAAC,QAAgB;IAChD,IAAI,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;QAC5D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC9D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;QAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,MAAM,WAAW,GAAG,IAAA,qCAAc,GAAE,CAAC;IACrC,MAAM,aAAa,GAAG,IAAA,wBAAQ,EAAC,2BAA2B,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACzF,MAAM,QAAQ,GAAG,IAAA,wBAAQ,EAAC,+BAA+B,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAExF,wFAAwF;IACxF,+FAA+F;IAC/F,yFAAyF;IACzF,6FAA6F;IAC7F,oFAAoF;IACpF,MAAM,SAAS,GAAG,QAAQ,KAAK,QAAQ;QACnC,CAAC,CAAC,IAAA,uBAAQ,EAAC,QAAQ,EAAE,WAAW,CAAC;QACjC,CAAC,CAAC,IAAA,yBAAW,EAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC;IACjE,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE7C,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IAEnE,MAAM,WAAW,GAAG,IAAA,wBAAQ,EAAC,oBAAoB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChF,MAAM,UAAU,GAAG,IAAA,wBAAQ,EAAC,2BAA2B,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAEtF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAErE,MAAM,eAAe,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,YAAY,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACtG,MAAM,SAAS,GAAG,CAAC,eAAe,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAExD,IAAI,CAAC,SAAS,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAC;QACjF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACvE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;IAEpE,MAAM,kBAAkB,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,KAAK,EAAE,GAAG,SAAS,QAAQ,EAAE,UAAU,EAAE,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5H,MAAM,eAAe,GAAG,CAAC,kBAAkB,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACjE,MAAM,YAAY,GAAG,eAAe,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAExE,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,iBAAiB,CAAC,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC;IACnF,CAAC;SAAM,CAAC;QACJ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC;QAC9B,aAAa,EAAE,SAAS;QACxB,eAAe,EAAE,WAAW;QAC5B,YAAY,EAAE,UAAU;QACxB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACtC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAEZ,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,MAAM,aAAa,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC,CAAC;IAClF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,SAAS,IAAI,MAAM,eAAe,CAAC,CAAC;AAC1F,CAAC;AAEM,KAAK,UAAU,IAAI;IACtB,MAAM,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AAC/C,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC1B,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACjB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { prDirFor } from '@webpieces/rules-config';\nimport { getFeatureName } from './git-readAiBranchName';\nimport { mergeDirFor } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\nfunction printMergeFromMainError(commit: string, parent: string, featureName: string, currentBranch: string): void {\n process.stderr.write('\\n');\n process.stderr.write(SEP);\n process.stderr.write('❌ This branch merged main without pnpm wp-git-update\\n');\n process.stderr.write(SEP);\n process.stderr.write('\\n');\n process.stderr.write(`Merge commit detected: ${commit}\\n`);\n process.stderr.write(`Parent from main: ${parent}\\n`);\n process.stderr.write('\\n');\n process.stderr.write('This prevents clean squash-merge. To recover, follow these steps:\\n');\n process.stderr.write('\\n');\n process.stderr.write('1. Switch to main branch:\\n');\n process.stderr.write(' git checkout main\\n');\n process.stderr.write('\\n');\n process.stderr.write('2. Pull latest changes:\\n');\n process.stderr.write(' git pull\\n');\n process.stderr.write('\\n');\n process.stderr.write('3. Create new branch with a new name:\\n');\n process.stderr.write(` git checkout -b ${featureName}-v2\\n`);\n process.stderr.write('\\n');\n process.stderr.write('4. Squash merge your old branch:\\n');\n process.stderr.write(` git merge --squash ${currentBranch}\\n`);\n process.stderr.write('\\n');\n process.stderr.write('5. Commit the squashed changes:\\n');\n process.stderr.write(` git add -A && git commit -m \"Squashed from ${currentBranch}\"\\n`);\n process.stderr.write('\\n');\n process.stderr.write('6. If you have an existing PR:\\n');\n process.stderr.write(` - Create a NEW PR for ${featureName}-v2\\n`);\n process.stderr.write(` - Close the old PR for ${currentBranch}\\n`);\n process.stderr.write('\\n');\n process.stderr.write(SEP);\n process.stderr.write('\\n');\n}\n\nfunction checkMergeCommits(mergeCommits: string[], outputDir: string, prefix: string, featureName: string, currentBranch: string): void {\n process.stderr.write(`Found ${mergeCommits.length} merge commit(s) to check...\\n`);\n\n for (const commit of mergeCommits) {\n const parentsResult = spawnSync('git', ['rev-list', '--parents', '-n', '1', commit], { encoding: 'utf8' });\n const parents = (parentsResult.stdout ?? '').trim().split(' ').slice(1);\n\n for (const parent of parents) {\n const ancestorCheck = spawnSync('git', ['merge-base', '--is-ancestor', parent, 'origin/main']);\n const reverseCheck = spawnSync('git', ['merge-base', '--is-ancestor', 'origin/main', parent]);\n\n if (ancestorCheck.status === 0 && reverseCheck.status === 0) {\n const errorJson = JSON.stringify({\n error: 'Merge from main detected',\n mergeCommit: commit,\n parentFromMain: parent,\n timestamp: new Date().toISOString(),\n }, null, 2);\n\n fs.writeFileSync(path.join(outputDir, `${prefix}forkpoint-error.json`), errorJson + '\\n');\n printMergeFromMainError(commit, parent, featureName, currentBranch);\n process.exit(1);\n }\n }\n }\n process.stderr.write('✅ No improper merges from main detected\\n');\n}\n\nexport async function findForkPoint(workflow: string): Promise<void> {\n if (workflow !== 'review' && workflow !== 'merge') {\n process.stderr.write('ERROR: Workflow argument required\\n');\n process.stderr.write('Usage: git-findForkPoint <workflow>\\n');\n process.stderr.write(\" workflow: 'review' or 'merge'\\n\");\n process.exit(1);\n }\n\n const featureName = getFeatureName();\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n\n // Single source of truth for the per-feature dir — the SAME nested home the readers use\n // (git-gatherInfo / merge-start read updatemain-hashes.json from mergeDirFor). Previously this\n // wrote to the legacy flat `.webpieces/<workflow>-<feature>/` while readers looked under\n // `.webpieces/merge-info/<feature>/`, so the hash file was written to one path and read from\n // another. Route both through mergeDirFor/prDirFor so they can never diverge again.\n const outputDir = workflow === 'review'\n ? prDirFor(repoRoot, featureName)\n : mergeDirFor(repoRoot, featureName);\n const prefix = workflow === 'review' ? 'review-' : 'updatemain-';\n fs.mkdirSync(outputDir, { recursive: true });\n\n spawnSync('git', ['fetch', 'origin', 'main'], { stdio: 'ignore' });\n\n const featureHead = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();\n const originMain = execSync('git rev-parse origin/main', { encoding: 'utf8' }).trim();\n\n process.stderr.write('Finding fork point using git merge-base...\\n');\n\n const forkPointResult = spawnSync('git', ['merge-base', 'origin/main', 'HEAD'], { encoding: 'utf8' });\n const forkPoint = (forkPointResult.stdout ?? '').trim();\n\n if (!forkPoint || forkPointResult.status !== 0) {\n process.stderr.write('ERROR: Could not find common ancestor with origin/main\\n');\n process.exit(1);\n }\n\n process.stderr.write(`✅ Fork point found: ${forkPoint.slice(0, 7)}\\n`);\n process.stderr.write('Checking for improper merges from main...\\n');\n\n const mergeCommitsResult = spawnSync('git', ['log', `${forkPoint}..HEAD`, '--merges', '--format=%H'], { encoding: 'utf8' });\n const mergeCommitsRaw = (mergeCommitsResult.stdout ?? '').trim();\n const mergeCommits = mergeCommitsRaw ? mergeCommitsRaw.split('\\n') : [];\n\n if (mergeCommits.length > 0) {\n checkMergeCommits(mergeCommits, outputDir, prefix, featureName, currentBranch);\n } else {\n process.stderr.write('✅ No merge commits found (clean history)\\n');\n }\n\n const hashesJson = JSON.stringify({\n hashForkPoint: forkPoint,\n hashFeatureHead: featureHead,\n hashMainHead: originMain,\n timestamp: new Date().toISOString(),\n }, null, 2);\n\n fs.writeFileSync(path.join(outputDir, `${prefix}hashes.json`), hashesJson + '\\n');\n process.stderr.write(`✅ Hash points written to: ${outputDir}/${prefix}hashes.json\\n`);\n}\n\nexport async function main(): Promise<void> {\n await findForkPoint(process.argv[2] ?? '');\n}\n\nif (require.main === module) {\n main().catch((err) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(message + '\\n');\n process.exit(1);\n });\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import { MergeContext } from './merge-start';
2
+ /**
3
+ * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the AI
4
+ * and must be validated + committed before finalizing; null means a clean merge that merge-START
5
+ * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.
6
+ */
7
+ export declare function mergeEnd(repoRoot: string, mergeDir: string, ctx: MergeContext, conflictedFiles: string[] | null): Promise<void>;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeEnd = mergeEnd;
4
+ const tslib_1 = require("tslib");
5
+ const child_process_1 = require("child_process");
6
+ const fs = tslib_1.__importStar(require("fs"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const rules_config_1 = require("@webpieces/rules-config");
9
+ const cleanTmp_1 = require("./cleanTmp");
10
+ const git_exec_1 = require("./git-exec");
11
+ const merge_state_1 = require("./merge-state");
12
+ // merge-END: the second half of the 3-point squash-merge lifecycle, symmetric with merge-START. Given
13
+ // the branch context, it (optionally) validates + commits the AI's conflict resolution, then ALWAYS
14
+ // finalizes the merge — swaps `<branch>Squash` back over the feature branch, force-pushes, stamps a
15
+ // clean main-sync status, clears the marker and sweeps stale tmp. Both wp-git-update (clean path /
16
+ // validated resume) and wp-finish-upsert-pr (conflict resolution) call THIS, so finalization happens
17
+ // in exactly one place and the conflict path can no longer post a PR from the un-swapped squash branch.
18
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
19
+ // Validate the AI's resolution of the conflicted files — the part of the process the AI owns
20
+ // (branch creation/finalization is the script's job, so it is not re-checked here). Exits the
21
+ // process with a fix instruction on any failure; returns only when all three checks pass.
22
+ function validateResolution(repoRoot, mergeDir, conflictedFiles) {
23
+ // 1. Scoped conflict-marker scan (only the conflicted files — O(conflicts), not O(repo)).
24
+ const scan = (0, merge_state_1.scanConflictMarkers)(repoRoot, conflictedFiles);
25
+ if (!scan.clean) {
26
+ process.stderr.write('❌ Unresolved conflict markers (<<<<<<< / ======= / >>>>>>>) remain in:\n');
27
+ for (const file of scan.filesWithMarkers)
28
+ process.stderr.write(` - ${file}\n`);
29
+ process.stderr.write('\nResolve them, then re-run: pnpm wp-finish-upsert-pr\n');
30
+ process.exit(1);
31
+ }
32
+ // 2. Ensure git itself has no remaining unmerged entries.
33
+ const unmerged = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();
34
+ if (unmerged !== '') {
35
+ process.stderr.write('❌ Git still reports unmerged files:\n' + unmerged + '\n');
36
+ process.stderr.write('\nResolve and `git add` them, then re-run: pnpm wp-finish-upsert-pr\n');
37
+ process.exit(1);
38
+ }
39
+ process.stdout.write('✅ No conflict markers in resolved files.\n');
40
+ // 3. Explanation check — every conflicted file must have a non-empty merge-explanation.md in
41
+ // its per-file context dir, proving the AI deliberately 3-point merged it (and recording how)
42
+ // rather than blindly taking one side. A sidecar file works for any type, incl. JSON/deletes.
43
+ const explanations = (0, merge_state_1.scanMergeExplanations)(mergeDir, conflictedFiles);
44
+ if (!explanations.clean) {
45
+ process.stderr.write(`❌ Missing/empty merge explanation (${rules_config_1.MERGE_EXPLANATION_FILE}) for:\n`);
46
+ for (const file of explanations.filesWithMarkers) {
47
+ process.stderr.write(` - ${file}\n → ${path.join((0, merge_state_1.perFileContextDir)(mergeDir, file), rules_config_1.MERGE_EXPLANATION_FILE)}\n`);
48
+ }
49
+ process.stderr.write('\nWrite a few sentences on how you resolved each (which side, what you combined, why),\n' +
50
+ 'then re-run: pnpm wp-finish-upsert-pr\n');
51
+ process.exit(1);
52
+ }
53
+ process.stdout.write('✅ Merge explanations present for all resolved files.\n');
54
+ }
55
+ // Swap the squash branch back over the feature branch, force-push, and stamp clean main-sync.
56
+ function finalizeBranch(ctx) {
57
+ process.stdout.write('\n' + SEP + '🗑️ Finalizing\n' + SEP + '\n');
58
+ (0, git_exec_1.runGitChecked)(['branch', '-D', ctx.currentBranch], 'Failed to delete old feature branch');
59
+ const remoteExists = (0, child_process_1.spawnSync)('git', ['ls-remote', '--exit-code', '--heads', 'origin', ctx.currentBranch]).status === 0;
60
+ if (remoteExists) {
61
+ process.stdout.write(ctx.prNumber ? `Updating PR #${ctx.prNumber} (force-with-lease)...\n` : 'Updating remote branch (force-with-lease)...\n');
62
+ (0, git_exec_1.runGitChecked)(['push', '-u', '--force-with-lease', 'origin', `${ctx.squashBranch}:${ctx.currentBranch}`], 'Failed to push to origin');
63
+ }
64
+ else {
65
+ process.stdout.write('No remote branch — local only.\n');
66
+ }
67
+ (0, git_exec_1.runGitChecked)(['checkout', ctx.squashBranch], 'Failed to checkout squash branch');
68
+ (0, git_exec_1.runGitChecked)(['branch', '-m', ctx.currentBranch], 'Failed to rename squash branch');
69
+ // Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard
70
+ // unblocks edits immediately (no wait for the async refresher).
71
+ (0, rules_config_1.stampCleanMainSyncStatus)((0, child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim());
72
+ process.stdout.write(`\n✅ Branch ${ctx.currentBranch} updated from main. Backup: ${ctx.backupBranch}\n`);
73
+ process.stdout.write(` Delete backup when safe: git branch -D ${ctx.backupBranch}\n\n`);
74
+ }
75
+ /**
76
+ * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the AI
77
+ * and must be validated + committed before finalizing; null means a clean merge that merge-START
78
+ * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.
79
+ */
80
+ async function mergeEnd(repoRoot, mergeDir, ctx, conflictedFiles) {
81
+ if (conflictedFiles !== null) {
82
+ process.stdout.write('\n' + SEP + '🔎 Validating Merge Resolution\n' + SEP + '\n');
83
+ validateResolution(repoRoot, mergeDir, conflictedFiles);
84
+ (0, git_exec_1.runGitChecked)(['add', '-A'], 'Failed to stage resolved files');
85
+ const nothingStaged = (0, child_process_1.spawnSync)('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;
86
+ if (!nothingStaged) {
87
+ (0, git_exec_1.runGitChecked)(['commit', '-m', `Squash merge of ${ctx.currentBranch} (conflicts resolved)`], 'Failed to commit resolved merge');
88
+ }
89
+ fs.writeFileSync(path.join(mergeDir, 'conflicts-resolved'), '');
90
+ process.stdout.write('\n✅ Merge validated and committed.\n');
91
+ }
92
+ finalizeBranch(ctx);
93
+ (0, merge_state_1.clearMergeMarker)(mergeDir);
94
+ await (0, cleanTmp_1.main)();
95
+ }
96
+ //# sourceMappingURL=merge-end.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-end.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-end.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { MERGE_EXPLANATION_FILE, stampCleanMainSyncStatus } from '@webpieces/rules-config';\nimport { main as cleanTmp } from './cleanTmp';\nimport { runGitChecked } from './git-exec';\nimport { MergeContext } from './merge-start';\nimport { clearMergeMarker, perFileContextDir, scanConflictMarkers, scanMergeExplanations } from './merge-state';\n\n// merge-END: the second half of the 3-point squash-merge lifecycle, symmetric with merge-START. Given\n// the branch context, it (optionally) validates + commits the AI's conflict resolution, then ALWAYS\n// finalizes the merge — swaps `<branch>Squash` back over the feature branch, force-pushes, stamps a\n// clean main-sync status, clears the marker and sweeps stale tmp. Both wp-git-update (clean path /\n// validated resume) and wp-finish-upsert-pr (conflict resolution) call THIS, so finalization happens\n// in exactly one place and the conflict path can no longer post a PR from the un-swapped squash branch.\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Validate the AI's resolution of the conflicted files — the part of the process the AI owns\n// (branch creation/finalization is the script's job, so it is not re-checked here). Exits the\n// process with a fix instruction on any failure; returns only when all three checks pass.\nfunction validateResolution(repoRoot: string, mergeDir: string, conflictedFiles: string[]): void {\n // 1. Scoped conflict-marker scan (only the conflicted files — O(conflicts), not O(repo)).\n const scan = scanConflictMarkers(repoRoot, conflictedFiles);\n if (!scan.clean) {\n process.stderr.write('❌ Unresolved conflict markers (<<<<<<< / ======= / >>>>>>>) remain in:\\n');\n for (const file of scan.filesWithMarkers) process.stderr.write(` - ${file}\\n`);\n process.stderr.write('\\nResolve them, then re-run: pnpm wp-finish-upsert-pr\\n');\n process.exit(1);\n }\n\n // 2. Ensure git itself has no remaining unmerged entries.\n const unmerged = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n if (unmerged !== '') {\n process.stderr.write('❌ Git still reports unmerged files:\\n' + unmerged + '\\n');\n process.stderr.write('\\nResolve and `git add` them, then re-run: pnpm wp-finish-upsert-pr\\n');\n process.exit(1);\n }\n process.stdout.write('✅ No conflict markers in resolved files.\\n');\n\n // 3. Explanation check — every conflicted file must have a non-empty merge-explanation.md in\n // its per-file context dir, proving the AI deliberately 3-point merged it (and recording how)\n // rather than blindly taking one side. A sidecar file works for any type, incl. JSON/deletes.\n const explanations = scanMergeExplanations(mergeDir, conflictedFiles);\n if (!explanations.clean) {\n process.stderr.write(`❌ Missing/empty merge explanation (${MERGE_EXPLANATION_FILE}) for:\\n`);\n for (const file of explanations.filesWithMarkers) {\n process.stderr.write(` - ${file}\\n → ${path.join(perFileContextDir(mergeDir, file), MERGE_EXPLANATION_FILE)}\\n`);\n }\n process.stderr.write(\n '\\nWrite a few sentences on how you resolved each (which side, what you combined, why),\\n' +\n 'then re-run: pnpm wp-finish-upsert-pr\\n',\n );\n process.exit(1);\n }\n process.stdout.write('✅ Merge explanations present for all resolved files.\\n');\n}\n\n// Swap the squash branch back over the feature branch, force-push, and stamp clean main-sync.\nfunction finalizeBranch(ctx: MergeContext): void {\n process.stdout.write('\\n' + SEP + '🗑️ Finalizing\\n' + SEP + '\\n');\n runGitChecked(['branch', '-D', ctx.currentBranch], 'Failed to delete old feature branch');\n\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', ctx.currentBranch]).status === 0;\n if (remoteExists) {\n process.stdout.write(ctx.prNumber ? `Updating PR #${ctx.prNumber} (force-with-lease)...\\n` : 'Updating remote branch (force-with-lease)...\\n');\n runGitChecked(['push', '-u', '--force-with-lease', 'origin', `${ctx.squashBranch}:${ctx.currentBranch}`], 'Failed to push to origin');\n } else {\n process.stdout.write('No remote branch — local only.\\n');\n }\n runGitChecked(['checkout', ctx.squashBranch], 'Failed to checkout squash branch');\n runGitChecked(['branch', '-m', ctx.currentBranch], 'Failed to rename squash branch');\n\n // Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard\n // unblocks edits immediately (no wait for the async refresher).\n stampCleanMainSyncStatus(execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim());\n\n process.stdout.write(`\\n✅ Branch ${ctx.currentBranch} updated from main. Backup: ${ctx.backupBranch}\\n`);\n process.stdout.write(` Delete backup when safe: git branch -D ${ctx.backupBranch}\\n\\n`);\n}\n\n/**\n * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the AI\n * and must be validated + committed before finalizing; null means a clean merge that merge-START\n * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.\n */\nexport async function mergeEnd(\n repoRoot: string, mergeDir: string, ctx: MergeContext, conflictedFiles: string[] | null,\n): Promise<void> {\n if (conflictedFiles !== null) {\n process.stdout.write('\\n' + SEP + '🔎 Validating Merge Resolution\\n' + SEP + '\\n');\n validateResolution(repoRoot, mergeDir, conflictedFiles);\n runGitChecked(['add', '-A'], 'Failed to stage resolved files');\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (!nothingStaged) {\n runGitChecked(\n ['commit', '-m', `Squash merge of ${ctx.currentBranch} (conflicts resolved)`],\n 'Failed to commit resolved merge',\n );\n }\n fs.writeFileSync(path.join(mergeDir, 'conflicts-resolved'), '');\n process.stdout.write('\\n✅ Merge validated and committed.\\n');\n }\n\n finalizeBranch(ctx);\n clearMergeMarker(mergeDir);\n await cleanTmp();\n}\n"]}
@@ -0,0 +1,13 @@
1
+ export declare class MergeContext {
2
+ currentBranch: string;
3
+ squashBranch: string;
4
+ backupBranch: string;
5
+ prNumber: string;
6
+ constructor(currentBranch: string, squashBranch: string, backupBranch: string, prNumber: string);
7
+ }
8
+ export declare class MergeStartResult {
9
+ status: 'clean' | 'conflict';
10
+ context: MergeContext | null;
11
+ constructor(status: 'clean' | 'conflict', context: MergeContext | null);
12
+ }
13
+ export declare function mergeStart(repoRoot: string, mergeDir: string): Promise<MergeStartResult>;
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MergeStartResult = exports.MergeContext = void 0;
4
+ exports.mergeStart = mergeStart;
5
+ const tslib_1 = require("tslib");
6
+ const child_process_1 = require("child_process");
7
+ const fs = tslib_1.__importStar(require("fs"));
8
+ const path = tslib_1.__importStar(require("path"));
9
+ const rules_config_1 = require("@webpieces/rules-config");
10
+ const git_gatherInfo_1 = require("../git-gatherInfo");
11
+ const git_exec_1 = require("./git-exec");
12
+ const merge_state_1 = require("./merge-state");
13
+ // merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh
14
+ // `<branch>Squash`, and on conflict writes the 3-point context files + the unvalidated marker + the
15
+ // process doc, then hands control back to the AI. On a clean merge it commits the squash and returns
16
+ // the branch context so the caller (wp-git-update) can run merge-END to finalize. It NEVER finalizes
17
+ // or posts a PR — that is merge-END's / wp-finish-upsert-pr's job. Shared so wp-git-update (and, via
18
+ // it, wp-start-upsert-pr) all set up a merge through one code path.
19
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
20
+ // The four branch names merge-END needs to finalize a merge (swap squash→feature, push, clean up).
21
+ class MergeContext {
22
+ currentBranch;
23
+ squashBranch;
24
+ backupBranch;
25
+ prNumber;
26
+ constructor(currentBranch, squashBranch, backupBranch, prNumber) {
27
+ this.currentBranch = currentBranch;
28
+ this.squashBranch = squashBranch;
29
+ this.backupBranch = backupBranch;
30
+ this.prNumber = prNumber;
31
+ }
32
+ }
33
+ exports.MergeContext = MergeContext;
34
+ // Outcome of merge-start: 'clean' carries the context for merge-END to finalize; 'conflict' means the
35
+ // marker + context files were written and the caller should hand back to the AI (exit 2).
36
+ class MergeStartResult {
37
+ status;
38
+ context;
39
+ constructor(status, context) {
40
+ this.status = status;
41
+ this.context = context;
42
+ }
43
+ }
44
+ exports.MergeStartResult = MergeStartResult;
45
+ function detectPr(currentBranch) {
46
+ const result = (0, child_process_1.spawnSync)('gh', ['pr', 'list', '--head', currentBranch, '--json', 'number', '--jq', '.[0].number'], { encoding: 'utf8' });
47
+ return result.status === 0 ? (result.stdout ?? '').trim() : '';
48
+ }
49
+ function createBackup(currentBranch) {
50
+ process.stdout.write('\n' + SEP + '💾 Creating Incremental Backup\n' + SEP + '\n');
51
+ let n = 1;
52
+ while ((0, child_process_1.spawnSync)('git', ['show-ref', '--verify', '--quiet', `refs/heads/${currentBranch}Backup${n}`]).status === 0) {
53
+ n += 1;
54
+ }
55
+ const backupBranch = `${currentBranch}Backup${n}`;
56
+ (0, git_exec_1.runGitChecked)(['checkout', '-b', backupBranch], 'Failed to create backup branch');
57
+ (0, git_exec_1.runGitChecked)(['checkout', currentBranch], 'Failed to return to feature branch');
58
+ process.stdout.write(`✅ Backup created: ${backupBranch}\n\n`);
59
+ return backupBranch;
60
+ }
61
+ function saveConflictContext(conflictedFiles, mergeDir, forkPoint, featureHead, mainHead) {
62
+ for (const file of conflictedFiles) {
63
+ const fileDir = (0, merge_state_1.perFileContextDir)(mergeDir, file);
64
+ fs.mkdirSync(fileDir, { recursive: true });
65
+ const fork = (0, child_process_1.spawnSync)('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });
66
+ fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\n');
67
+ const feature = (0, child_process_1.spawnSync)('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });
68
+ fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\n');
69
+ const main = (0, child_process_1.spawnSync)('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });
70
+ fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\n');
71
+ const ba = (0, child_process_1.spawnSync)('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });
72
+ fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');
73
+ const ca = (0, child_process_1.spawnSync)('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });
74
+ fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');
75
+ }
76
+ }
77
+ // Single source of truth for the merge process. The script WRITES it at conflict time (rather
78
+ // than the AI relying on a separate hand-maintained doc), parameterized with the live MERGE_DIR
79
+ // and conflicted-file list, so the instructions can never drift from the actual layout. The body
80
+ // is a template constant with {{...}} placeholders so this stays a small filler function.
81
+ const MERGE_PROCESS_TEMPLATE = `# AI-Assisted Squash-Merge Conflict Resolution (generated)
82
+
83
+ This file was generated by \`pnpm wp-git-update\` when the 3-point squash-merge hit conflicts.
84
+ It is the single source of truth for the merge process — follow it exactly.
85
+
86
+ You are on branch \`{{SQUASH_BRANCH}}\` with conflict markers in the working tree.
87
+ \`MERGE_DIR = {{MERGE_DIR}}\`
88
+
89
+ ## How the gate works
90
+
91
+ - Resolve every conflicted file in the working tree.
92
+ - Run **\`pnpm wp-finish-upsert-pr\`** — the validation + finish gate. It scans for leftover conflict
93
+ markers, checks each conflicted file has a written merge explanation, runs the \`nx affected\`
94
+ build, and — only if all pass — stages, commits, then renders the dashboard and creates/updates the PR.
95
+ - **Do NOT run \`git add\` / \`git commit\` / \`git push\` / \`gh pr\` yourself.** They are blocked by
96
+ the \`merge-in-progress-guard\` hook until the gate validates. The gate does the commit.
97
+
98
+ ## STEP 1 — Load the merge context
99
+
100
+ Per conflicted file, \`MERGE_DIR/updatemain-<safe_path>/\` holds (\`<safe_path>\` = path with \`/\`→\`__\`):
101
+
102
+ \`\`\`
103
+ A-forkpoint.txt # file at fork point (base)
104
+ B-feature.txt # file on your feature branch
105
+ C-main.txt # file on main
106
+ B-A.diff # what your feature changed (B−A)
107
+ C-A.diff # what main changed (C−A)
108
+ \`\`\`
109
+
110
+ \`updatemain-hashes.json\` holds A/B/C commit hashes. To see why main changed:
111
+ \`git log <A>..<C> --oneline\`.
112
+
113
+ ## STEP 2 — Resolve each conflicted file
114
+
115
+ For each file: read the working-tree file (the markers) and its \`B-A.diff\` / \`C-A.diff\` (intent),
116
+ then Edit to the resolved version, removing ALL conflict markers.
117
+
118
+ Strategies: goals align & non-overlapping → merge both · one side removes what the other modifies
119
+ → prefer the removal · same lines, simple (imports/format) → merge both · same lines, complex or
120
+ conflicting goals → ask the user · feature re-implements what main already squashed → prefer
121
+ main's, then re-apply only the genuinely new feature work.
122
+
123
+ **Then write a merge explanation** for each conflicted file — NOT a comment in the source (that
124
+ breaks for JSON and deleted files). Write it next to that file's diffs, at:
125
+
126
+ \`\`\`
127
+ MERGE_DIR/updatemain-<safe_path>/{{EXPLANATION_FILE}}
128
+ \`\`\`
129
+
130
+ (\`<safe_path>\` = the conflict file path with \`/\` → \`__\`, the same dir that holds its
131
+ \`A-forkpoint.txt\` / \`B-A.diff\` / \`C-A.diff\`.) In it, explain in a few sentences how you resolved
132
+ this file: which side you took where, what you combined from B-A.diff vs C-A.diff, and why. The
133
+ gate fails if any conflicted file's explanation is missing or empty. Do not paste A/B/C context
134
+ blocks into the source code.
135
+
136
+ ## STEP 3 — Run the gate (validates the merge AND finishes the PR)
137
+
138
+ \`\`\`
139
+ pnpm wp-finish-upsert-pr
140
+ \`\`\`
141
+
142
+ - Leftover conflict markers → fix those files and re-run.
143
+ - Missing merge explanation → write it (see STEP 2) and re-run.
144
+ - Build failure → fix the TypeScript/lint errors and re-run (the gate re-stages for you).
145
+ - Missing review.json → write it in the printed format (your PR review), then re-run.
146
+ - On success it commits, renders the dashboard, and creates/updates the PR.
147
+
148
+ ## Conflicted files
149
+
150
+ {{FILE_LIST}}
151
+
152
+ ## If you need to bail out
153
+
154
+ A backup branch was created (e.g. \`<feature>Backup1\`). To abandon:
155
+
156
+ \`\`\`
157
+ git merge --abort 2>/dev/null; git checkout <feature> ; git branch -D {{SQUASH_BRANCH}}
158
+ \`\`\`
159
+
160
+ Then delete \`{{MERGE_DIR}}/\` for a clean slate.
161
+ `;
162
+ function mergeProcessDoc(mergeDir, squashBranch, conflictedFiles) {
163
+ const fileList = conflictedFiles.map((f) => `- \`${f}\``).join('\n');
164
+ return MERGE_PROCESS_TEMPLATE
165
+ .replace(/\{\{SQUASH_BRANCH\}\}/g, squashBranch)
166
+ .replace(/\{\{MERGE_DIR\}\}/g, mergeDir)
167
+ .replace(/\{\{EXPLANATION_FILE\}\}/g, rules_config_1.MERGE_EXPLANATION_FILE)
168
+ .replace(/\{\{FILE_LIST\}\}/g, fileList);
169
+ }
170
+ // Returns the absolute path of the written doc.
171
+ function writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles) {
172
+ const docDir = path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, 'instruct-ai');
173
+ fs.mkdirSync(docDir, { recursive: true });
174
+ const docPath = path.join(docDir, 'webpieces.mergeprocess.md');
175
+ fs.writeFileSync(docPath, mergeProcessDoc(mergeDir, squashBranch, conflictedFiles));
176
+ return docPath;
177
+ }
178
+ function printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles) {
179
+ process.stdout.write('\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\n` + SEP + '\n');
180
+ process.stdout.write(`You are on branch ${squashBranch} with conflicts in the working tree.\n\n`);
181
+ process.stdout.write(`FOLLOW THE MERGE PROCESS: ${docPath}\n`);
182
+ process.stdout.write(`3-point context per file in: ${mergeDir}/updatemain-<file>/\n\n`);
183
+ process.stdout.write('Conflicted files:\n');
184
+ for (const file of conflictedFiles)
185
+ process.stdout.write(` - ${file}\n`);
186
+ process.stdout.write('\n' + SEP);
187
+ }
188
+ // Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit — the
189
+ // caller decides (wp-git-update exits 2 to hand back to the AI).
190
+ function handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes) {
191
+ const raw = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();
192
+ const conflictedFiles = raw.split('\n').filter((f) => f.trim() !== '');
193
+ fs.writeFileSync(path.join(mergeDir, 'updatemain-conflicted-files.txt'), raw + '\n');
194
+ saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);
195
+ const marker = new merge_state_1.MergeMarker(currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false);
196
+ (0, merge_state_1.writeMergeMarker)(mergeDir, marker);
197
+ const docPath = writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles);
198
+ printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles);
199
+ }
200
+ async function mergeStart(repoRoot, mergeDir) {
201
+ const currentBranch = (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
202
+ if (currentBranch.endsWith('Squash')) {
203
+ process.stderr.write(`❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}\n`);
204
+ process.exit(1);
205
+ }
206
+ process.stdout.write('\n' + SEP + '🔄 Squash-Merge Update from Main\n' + SEP + '\n');
207
+ await (0, git_gatherInfo_1.main)();
208
+ const hashes = JSON.parse(fs.readFileSync(path.join(mergeDir, 'updatemain-hashes.json'), 'utf8'));
209
+ const prNumber = detectPr(currentBranch);
210
+ process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\n` : 'No existing PR (one can be created later).\n');
211
+ const backupBranch = createBackup(currentBranch);
212
+ const squashBranch = `${currentBranch}Squash`;
213
+ if ((0, child_process_1.spawnSync)('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {
214
+ process.stderr.write(`❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}\n`);
215
+ process.exit(1);
216
+ }
217
+ (0, git_exec_1.runGitChecked)(['checkout', 'main'], 'Failed to checkout main');
218
+ (0, git_exec_1.runGitChecked)(['pull', 'origin', 'main'], 'Failed to pull origin/main');
219
+ (0, git_exec_1.runGitChecked)(['checkout', '-b', squashBranch], 'Failed to create squash branch');
220
+ process.stdout.write('\n' + SEP + `🔀 Squash merging ${currentBranch}\n` + SEP + '\n');
221
+ const merge = (0, child_process_1.spawnSync)('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });
222
+ if (merge.status !== 0) {
223
+ handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes);
224
+ return new MergeStartResult('conflict', null);
225
+ }
226
+ const nothingStaged = (0, child_process_1.spawnSync)('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;
227
+ if (nothingStaged) {
228
+ process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\n');
229
+ }
230
+ else {
231
+ (0, git_exec_1.runGitChecked)(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');
232
+ }
233
+ return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber));
234
+ }
235
+ //# sourceMappingURL=merge-start.js.map