@webpieces/pr-gate 0.4.565 → 0.4.566

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.
Files changed (30) hide show
  1. package/bin/wp-finish-push-dev.js +27 -0
  2. package/bin/wp-push-dev.js +27 -0
  3. package/package.json +5 -3
  4. package/src/scripts/commands/finish-push-dev-command.d.ts +28 -0
  5. package/src/scripts/commands/finish-push-dev-command.js +62 -0
  6. package/src/scripts/commands/finish-push-dev-command.js.map +1 -0
  7. package/src/scripts/commands/push-dev-command.d.ts +89 -0
  8. package/src/scripts/commands/push-dev-command.js +249 -0
  9. package/src/scripts/commands/push-dev-command.js.map +1 -0
  10. package/src/scripts/pr-gate-app.d.ts +22 -1
  11. package/src/scripts/pr-gate-app.js +46 -2
  12. package/src/scripts/pr-gate-app.js.map +1 -1
  13. package/src/scripts/workflow/dev-deploy-refs.d.ts +66 -0
  14. package/src/scripts/workflow/dev-deploy-refs.js +143 -0
  15. package/src/scripts/workflow/dev-deploy-refs.js.map +1 -0
  16. package/src/scripts/workflow/dev-resolve-runner.d.ts +72 -0
  17. package/src/scripts/workflow/dev-resolve-runner.js +180 -0
  18. package/src/scripts/workflow/dev-resolve-runner.js.map +1 -0
  19. package/src/scripts/workflow/git-exec.d.ts +20 -1
  20. package/src/scripts/workflow/git-exec.js +30 -3
  21. package/src/scripts/workflow/git-exec.js.map +1 -1
  22. package/src/scripts/workflow/push-dev-state.d.ts +52 -0
  23. package/src/scripts/workflow/push-dev-state.js +147 -0
  24. package/src/scripts/workflow/push-dev-state.js.map +1 -0
  25. package/src/scripts/wp-finish-push-dev.d.ts +2 -0
  26. package/src/scripts/wp-finish-push-dev.js +20 -0
  27. package/src/scripts/wp-finish-push-dev.js.map +1 -0
  28. package/src/scripts/wp-push-dev.d.ts +2 -0
  29. package/src/scripts/wp-push-dev.js +44 -0
  30. package/src/scripts/wp-push-dev.js.map +1 -0
@@ -13,6 +13,10 @@ const land_pr_command_1 = require("./commands/land-pr-command");
13
13
  const check_pr_command_1 = require("./commands/check-pr-command");
14
14
  const review_upsert_pr_command_1 = require("./commands/review-upsert-pr-command");
15
15
  const reap_worktree_command_1 = require("./commands/reap-worktree-command");
16
+ const push_dev_command_1 = require("./commands/push-dev-command");
17
+ const finish_push_dev_command_1 = require("./commands/finish-push-dev-command");
18
+ const push_dev_state_1 = require("./workflow/push-dev-state");
19
+ const rules_config_2 = require("@webpieces/rules-config");
16
20
  /**
17
21
  * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command
18
22
  * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the
@@ -29,7 +33,11 @@ let PrGateApp = class PrGateApp {
29
33
  checkPrCommand;
30
34
  reviewUpsertPrCommand;
31
35
  reapWorktreeCommand;
32
- constructor(startUpdateCommand, finishUpdateCommand, startUpsertPrCommand, finishUpsertPrCommand, cleanupCommand, landPrCommand, checkPrCommand, reviewUpsertPrCommand, reapWorktreeCommand) {
36
+ pushDevCommand;
37
+ finishPushDevCommand;
38
+ pushDevStateStore;
39
+ repoRootFinder;
40
+ constructor(startUpdateCommand, finishUpdateCommand, startUpsertPrCommand, finishUpsertPrCommand, cleanupCommand, landPrCommand, checkPrCommand, reviewUpsertPrCommand, reapWorktreeCommand, pushDevCommand, finishPushDevCommand, pushDevStateStore, repoRootFinder) {
33
41
  this.startUpdateCommand = startUpdateCommand;
34
42
  this.finishUpdateCommand = finishUpdateCommand;
35
43
  this.startUpsertPrCommand = startUpsertPrCommand;
@@ -39,6 +47,31 @@ let PrGateApp = class PrGateApp {
39
47
  this.checkPrCommand = checkPrCommand;
40
48
  this.reviewUpsertPrCommand = reviewUpsertPrCommand;
41
49
  this.reapWorktreeCommand = reapWorktreeCommand;
50
+ this.pushDevCommand = pushDevCommand;
51
+ this.finishPushDevCommand = finishPushDevCommand;
52
+ this.pushDevStateStore = pushDevStateStore;
53
+ this.repoRootFinder = repoRootFinder;
54
+ }
55
+ /**
56
+ * Refuse `command` while a `wp-push-dev --resolve` is half-finished.
57
+ *
58
+ * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve
59
+ * parks the checkout on a throwaway branch, so every command below would act on a branch that is not
60
+ * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the
61
+ * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.
62
+ */
63
+ assertNoResolveInProgress(command) {
64
+ if (!this.pushDevStateStore.isBlockedDuringResolve(command))
65
+ return;
66
+ this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);
67
+ }
68
+ /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */
69
+ pushDev(opts = new push_dev_command_1.PushDevOptions()) {
70
+ return this.pushDevCommand.run(opts);
71
+ }
72
+ /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */
73
+ finishPushDev(opts = new finish_push_dev_command_1.FinishPushDevOptions()) {
74
+ return this.finishPushDevCommand.run(opts);
42
75
  }
43
76
  /**
44
77
  * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.
@@ -50,26 +83,32 @@ let PrGateApp = class PrGateApp {
50
83
  }
51
84
  /** `wp-start-update`: 3-point squash-update from main (no PR). */
52
85
  startUpdate() {
86
+ this.assertNoResolveInProgress('wp-start-update');
53
87
  return this.startUpdateCommand.run();
54
88
  }
55
89
  /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */
56
90
  finishUpdate() {
91
+ this.assertNoResolveInProgress('wp-finish-update');
57
92
  return this.finishUpdateCommand.run();
58
93
  }
59
94
  /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */
60
95
  startUpsertPr() {
96
+ this.assertNoResolveInProgress('wp-start-upsert-pr');
61
97
  return this.startUpsertPrCommand.run();
62
98
  }
63
99
  /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */
64
100
  finishUpsertPr() {
101
+ this.assertNoResolveInProgress('wp-finish-upsert-pr');
65
102
  return this.finishUpsertPrCommand.run();
66
103
  }
67
104
  /** `wp-cleanup`: reap what is provably merged, and ASK about everything that merely looks dead. */
68
105
  cleanup() {
106
+ this.assertNoResolveInProgress('wp-cleanup');
69
107
  return this.cleanupCommand.run();
70
108
  }
71
109
  /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */
72
110
  landPr() {
111
+ this.assertNoResolveInProgress('wp-land-pr');
73
112
  return this.landPrCommand.run();
74
113
  }
75
114
  /**
@@ -78,6 +117,7 @@ let PrGateApp = class PrGateApp {
78
117
  * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.
79
118
  */
80
119
  reviewUpsertPr(opts = new review_upsert_pr_command_1.ReviewUpsertPrOptions()) {
120
+ this.assertNoResolveInProgress('wp-review-upsert-pr');
81
121
  return this.reviewUpsertPrCommand.run(opts);
82
122
  }
83
123
  /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */
@@ -97,6 +137,10 @@ exports.PrGateApp = PrGateApp = tslib_1.__decorate([
97
137
  land_pr_command_1.LandPrCommand,
98
138
  check_pr_command_1.CheckPrCommand,
99
139
  review_upsert_pr_command_1.ReviewUpsertPrCommand,
100
- reap_worktree_command_1.ReapWorktreeCommand])
140
+ reap_worktree_command_1.ReapWorktreeCommand,
141
+ push_dev_command_1.PushDevCommand,
142
+ finish_push_dev_command_1.FinishPushDevCommand,
143
+ push_dev_state_1.PushDevStateStore,
144
+ rules_config_2.RepoRootFinder])
101
145
  ], PrGateApp);
102
146
  //# sourceMappingURL=pr-gate-app.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":";;;;AAAA,0DAAyD;AACzD,yCAA2D;AAC3D,0EAAqE;AACrE,4EAAuE;AACvE,gFAA0E;AAC1E,kFAA4E;AAC5E,gEAA4D;AAC5D,gEAA2D;AAC3D,kEAA6D;AAC7D,kFAAmG;AACnG,4EAAuE;AAEvE;;;;;GAKG;AAGI,IAAM,SAAS,GAAf,MAAM,SAAS;IAEG;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IATrB,YACqB,kBAAsC,EACtC,mBAAwC,EACxC,oBAA0C,EAC1C,qBAA4C,EAC5C,cAA8B,EAC9B,aAA4B,EAC5B,cAA8B,EAC9B,qBAA4C,EAC5C,mBAAwC;QARxC,uBAAkB,GAAlB,kBAAkB,CAAoB;QACtC,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,yBAAoB,GAApB,oBAAoB,CAAsB;QAC1C,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,mBAAc,GAAd,cAAc,CAAgB;QAC9B,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,wBAAmB,GAAnB,mBAAmB,CAAqB;IAC1D,CAAC;IAEJ;;;;OAIG;IACH,YAAY,CAAC,IAAc;QACvB,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED,kEAAkE;IAClE,WAAW;QACP,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC;IACzC,CAAC;IAED,gFAAgF;IAChF,YAAY;QACR,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED,4GAA4G;IAC5G,aAAa;QACT,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAC;IAC3C,CAAC;IAED,oGAAoG;IACpG,cAAc;QACV,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC;IAED,mGAAmG;IACnG,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;IAED,0FAA0F;IAC1F,MAAM;QACF,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC;IACpC,CAAC;IAED;;;;OAIG;IACH,cAAc,CAAC,OAA8B,IAAI,gDAAqB,EAAE;QACpE,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,+GAA+G;IAC/G,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;CACJ,CAAA;AAjEY,8BAAS;oBAAT,SAAS;IAFrB,IAAA,6BAAc,GAAE;IAChB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGI,yCAAkB;QACjB,2CAAmB;QAClB,8CAAoB;QACnB,gDAAqB;QAC5B,gCAAc;QACf,+BAAa;QACZ,iCAAc;QACP,gDAAqB;QACvB,2CAAmB;GAVpD,SAAS,CAiErB","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n ) {}\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n return this.finishUpsertPrCommand.run();\n }\n\n /** `wp-cleanup`: reap what is provably merged, and ASK about everything that merely looks dead. */\n cleanup(): Promise<void> {\n return this.cleanupCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(): Promise<void> {\n return this.landPrCommand.run();\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
1
+ {"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /** `wp-cleanup`: reap what is provably merged, and ASK about everything that merely looks dead. */\n cleanup(): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run();\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
@@ -0,0 +1,66 @@
1
+ import { DevDeployConfig } from '@webpieces/rules-config';
2
+ import { GitExec } from './git-exec';
3
+ /**
4
+ * One live copy in the dev namespace: the remote ref and the sha it points at. Data-only per CLAUDE.md —
5
+ * `--list` prints it and the clobber guard compares against `sha`, and neither should be re-parsing
6
+ * `ls-remote` output.
7
+ */
8
+ export declare class DevCopy {
9
+ /** The full ref name WITHOUT `refs/heads/`, e.g. `dev-include/dean/ONE-2275`. */
10
+ ref: string;
11
+ sha: string;
12
+ /** The feature branch this is a copy OF — `ref` with the namespace prefix stripped. */
13
+ sourceBranch: string;
14
+ constructor(ref: string, sha: string, sourceBranch: string);
15
+ }
16
+ /**
17
+ * Ref derivation and preconditions for the dev-deploy flow — the half of `wp-push-dev` that decides WHICH
18
+ * refs are in play, kept out of the commands so both `wp-push-dev` and `wp-finish-push-dev` answer those
19
+ * questions identically.
20
+ *
21
+ * THE INVARIANT THIS FILE EXISTS TO PROTECT: the feature branch is the PR head, so it must never acquire
22
+ * another developer's commits — landing that PR would ship their unreviewed work to production. Every
23
+ * write this flow performs therefore targets either the `<namespace>/<feature>` copy or a local throwaway
24
+ * branch, and NOTHING here ever moves `<feature>` itself. That is why a conflict resolution needs the copy
25
+ * to live in: it is the only place the resolution can be recorded without contaminating the PR.
26
+ */
27
+ export declare class DevDeployRefs {
28
+ private readonly gitExec;
29
+ constructor(gitExec: GitExec);
30
+ /** The `pr-gate.devDeploy` block — `dev-include` / `dev` unless the consumer overrode it. */
31
+ config(repoRoot: string): DevDeployConfig;
32
+ /** The checked-out branch name, or `'HEAD'` on a detached HEAD (which {@link assertPushable} refuses). */
33
+ currentBranch(repoRoot: string): string;
34
+ /**
35
+ * Refuse every branch that must never be the SOURCE of a dev copy, before anything is fetched or
36
+ * pushed.
37
+ *
38
+ * `main` and the composed dev branch are refused for the same reason from opposite ends: `main` is the
39
+ * trunk (whatever is on it already deploys, so copying it is meaningless), and the dev branch is a
40
+ * BUILD ARTIFACT that CI recomputes from `origin/main` on every run — copying it would publish a
41
+ * composition of everyone's work under one developer's name, and it would be wiped on the next CI run
42
+ * anyway. A branch already inside the namespace is refused because publishing a copy of a copy nests
43
+ * the prefix (`dev-include/dev-include/…`) and nothing would ever clean it up.
44
+ */
45
+ assertPushable(cfg: DevDeployConfig, branch: string): void;
46
+ /**
47
+ * Every live copy in the namespace, sorted by ref name.
48
+ *
49
+ * The sort is not cosmetic: `--resolve` with no argument queues these in exactly this order, and the
50
+ * consumer's CI composes `dev` in the same order, so what a developer resolves locally is the
51
+ * composition CI will actually build. A different order can produce a different (still valid) merge
52
+ * result, which would make the local resolution a guess about CI rather than a rehearsal of it.
53
+ */
54
+ liveCopies(repoRoot: string, cfg: DevDeployConfig): DevCopy[];
55
+ /** The remote sha of one copy, or '' when that copy does not exist yet. */
56
+ remoteSha(repoRoot: string, ref: string): string;
57
+ /**
58
+ * Fetch the named copy refs into `refs/remotes/origin/<ref>` so the local merges below run against
59
+ * fresh state. A no-op for an empty list, and a hard failure otherwise: a resolve queued against
60
+ * stale refs rehearses a composition CI will not build.
61
+ */
62
+ fetchCopies(repoRoot: string, refs: string[]): void;
63
+ /** How many commits `origin/<ref>` has that `HEAD` does not — the clobber signal. 0 = safe to publish. */
64
+ commitsOnlyOnRemote(repoRoot: string, ref: string): number;
65
+ private refusal;
66
+ }
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DevDeployRefs = exports.DevCopy = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const rules_config_1 = require("@webpieces/rules-config");
6
+ const inversify_1 = require("inversify");
7
+ const git_exec_1 = require("./git-exec");
8
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
9
+ const MAIN = 'main';
10
+ const DETACHED = 'HEAD';
11
+ /**
12
+ * One live copy in the dev namespace: the remote ref and the sha it points at. Data-only per CLAUDE.md —
13
+ * `--list` prints it and the clobber guard compares against `sha`, and neither should be re-parsing
14
+ * `ls-remote` output.
15
+ */
16
+ class DevCopy {
17
+ /** The full ref name WITHOUT `refs/heads/`, e.g. `dev-include/dean/ONE-2275`. */
18
+ ref;
19
+ sha;
20
+ /** The feature branch this is a copy OF — `ref` with the namespace prefix stripped. */
21
+ sourceBranch;
22
+ constructor(ref, sha, sourceBranch) {
23
+ this.ref = ref;
24
+ this.sha = sha;
25
+ this.sourceBranch = sourceBranch;
26
+ }
27
+ }
28
+ exports.DevCopy = DevCopy;
29
+ /**
30
+ * Ref derivation and preconditions for the dev-deploy flow — the half of `wp-push-dev` that decides WHICH
31
+ * refs are in play, kept out of the commands so both `wp-push-dev` and `wp-finish-push-dev` answer those
32
+ * questions identically.
33
+ *
34
+ * THE INVARIANT THIS FILE EXISTS TO PROTECT: the feature branch is the PR head, so it must never acquire
35
+ * another developer's commits — landing that PR would ship their unreviewed work to production. Every
36
+ * write this flow performs therefore targets either the `<namespace>/<feature>` copy or a local throwaway
37
+ * branch, and NOTHING here ever moves `<feature>` itself. That is why a conflict resolution needs the copy
38
+ * to live in: it is the only place the resolution can be recorded without contaminating the PR.
39
+ */
40
+ let DevDeployRefs = class DevDeployRefs {
41
+ gitExec;
42
+ constructor(gitExec) {
43
+ this.gitExec = gitExec;
44
+ }
45
+ /** The `pr-gate.devDeploy` block — `dev-include` / `dev` unless the consumer overrode it. */
46
+ config(repoRoot) {
47
+ return (0, rules_config_1.loadAndValidate)(repoRoot).prGate.devDeploy;
48
+ }
49
+ /** The checked-out branch name, or `'HEAD'` on a detached HEAD (which {@link assertPushable} refuses). */
50
+ currentBranch(repoRoot) {
51
+ return this.gitExec.gitQuery(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot, 'Failed to read the current branch (git rev-parse --abbrev-ref HEAD).');
52
+ }
53
+ /**
54
+ * Refuse every branch that must never be the SOURCE of a dev copy, before anything is fetched or
55
+ * pushed.
56
+ *
57
+ * `main` and the composed dev branch are refused for the same reason from opposite ends: `main` is the
58
+ * trunk (whatever is on it already deploys, so copying it is meaningless), and the dev branch is a
59
+ * BUILD ARTIFACT that CI recomputes from `origin/main` on every run — copying it would publish a
60
+ * composition of everyone's work under one developer's name, and it would be wiped on the next CI run
61
+ * anyway. A branch already inside the namespace is refused because publishing a copy of a copy nests
62
+ * the prefix (`dev-include/dev-include/…`) and nothing would ever clean it up.
63
+ */
64
+ assertPushable(cfg, branch) {
65
+ if (branch === DETACHED || branch === '') {
66
+ throw new rules_config_1.CliExitError(2, this.refusal('Detached HEAD', 'There is no branch name to derive the dev copy from. Check out your feature branch first:\n'
67
+ + ' git checkout <your-feature-branch>\n'));
68
+ }
69
+ if (branch === MAIN) {
70
+ throw new rules_config_1.CliExitError(2, this.refusal(`Refusing to publish a dev copy of \`${MAIN}\``, `\`${MAIN}\` is the trunk — whatever is on it already deploys, so a dev copy of it means nothing.\n`
71
+ + 'Check out the feature branch you actually want on the dev server and re-run.\n'));
72
+ }
73
+ if (branch === cfg.devBranch) {
74
+ throw new rules_config_1.CliExitError(2, this.refusal(`Refusing to publish a dev copy of \`${cfg.devBranch}\``, `\`${cfg.devBranch}\` is a BUILD ARTIFACT, not a source branch: your CI resets it to origin/${MAIN},\n`
75
+ + `merges every \`${cfg.copyRefGlob()}\` ref into it, and force-pushes — every run. Copying it would\n`
76
+ + "publish a composition of everybody's work under your branch's name, and the next CI run would\n"
77
+ + 'wipe it. Check out your own feature branch and re-run.\n'));
78
+ }
79
+ if (branch === cfg.branchNamespace || branch.startsWith(`${cfg.branchNamespace}/`)) {
80
+ throw new rules_config_1.CliExitError(2, this.refusal(`Refusing to publish a dev copy of a dev copy (\`${branch}\`)`, `\`${cfg.branchNamespace}/\` holds the disposable copies this command WRITES; you are standing on one.\n`
81
+ + `Publishing from here would nest the prefix (\`${cfg.branchNamespace}/${branch}\`) and nothing ever\n`
82
+ + 'cleans that up. Check out the real feature branch and re-run.\n'));
83
+ }
84
+ }
85
+ /**
86
+ * Every live copy in the namespace, sorted by ref name.
87
+ *
88
+ * The sort is not cosmetic: `--resolve` with no argument queues these in exactly this order, and the
89
+ * consumer's CI composes `dev` in the same order, so what a developer resolves locally is the
90
+ * composition CI will actually build. A different order can produce a different (still valid) merge
91
+ * result, which would make the local resolution a guess about CI rather than a rehearsal of it.
92
+ */
93
+ liveCopies(repoRoot, cfg) {
94
+ const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', cfg.copyRefGlob()], repoRoot, `Failed to list the dev copies (git ls-remote --heads origin '${cfg.copyRefGlob()}').`);
95
+ const copies = [];
96
+ for (const line of out.split('\n')) {
97
+ if (line.trim() === '')
98
+ continue;
99
+ const fields = line.split(/\s+/);
100
+ const sha = fields[0];
101
+ const ref = fields[1];
102
+ if (sha === undefined || ref === undefined || !ref.startsWith('refs/heads/'))
103
+ continue;
104
+ const shortRef = ref.slice('refs/heads/'.length);
105
+ copies.push(new DevCopy(shortRef, sha, shortRef.slice(cfg.branchNamespace.length + 1)));
106
+ }
107
+ return copies.sort((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));
108
+ }
109
+ /** The remote sha of one copy, or '' when that copy does not exist yet. */
110
+ remoteSha(repoRoot, ref) {
111
+ const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', ref], repoRoot, `Failed to look up origin/${ref} (git ls-remote).`);
112
+ const line = out.split('\n').find((l) => l.trim() !== '');
113
+ if (line === undefined)
114
+ return '';
115
+ return line.split(/\s+/)[0] ?? '';
116
+ }
117
+ /**
118
+ * Fetch the named copy refs into `refs/remotes/origin/<ref>` so the local merges below run against
119
+ * fresh state. A no-op for an empty list, and a hard failure otherwise: a resolve queued against
120
+ * stale refs rehearses a composition CI will not build.
121
+ */
122
+ fetchCopies(repoRoot, refs) {
123
+ if (refs.length === 0)
124
+ return;
125
+ const specs = refs.map((r) => `+refs/heads/${r}:refs/remotes/origin/${r}`);
126
+ this.gitExec.runGitChecked(['-C', repoRoot, 'fetch', 'origin', ...specs], 'Failed to fetch the dev copy refs from origin');
127
+ }
128
+ /** How many commits `origin/<ref>` has that `HEAD` does not — the clobber signal. 0 = safe to publish. */
129
+ commitsOnlyOnRemote(repoRoot, ref) {
130
+ const out = this.gitExec.gitQuery(['rev-list', '--count', `HEAD..refs/remotes/origin/${ref}`], repoRoot, `Failed to compare HEAD against origin/${ref} (git rev-list --count).`);
131
+ const count = Number(out);
132
+ return Number.isInteger(count) ? count : 0;
133
+ }
134
+ refusal(headline, body) {
135
+ return '\n' + SEP + `⛔ ${headline}\n` + SEP + '\n' + body + `\nRun \`${rules_config_1.WP_PUSH_DEV} --help\` for the full command.\n`;
136
+ }
137
+ };
138
+ exports.DevDeployRefs = DevDeployRefs;
139
+ exports.DevDeployRefs = DevDeployRefs = tslib_1.__decorate([
140
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
141
+ tslib_1.__metadata("design:paramtypes", [git_exec_1.GitExec])
142
+ ], DevDeployRefs);
143
+ //# sourceMappingURL=dev-deploy-refs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-deploy-refs.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/dev-deploy-refs.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, DevDeployConfig, loadAndValidate, WP_PUSH_DEV } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from './git-exec';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\nconst MAIN = 'main';\nconst DETACHED = 'HEAD';\n\n/**\n * One live copy in the dev namespace: the remote ref and the sha it points at. Data-only per CLAUDE.md —\n * `--list` prints it and the clobber guard compares against `sha`, and neither should be re-parsing\n * `ls-remote` output.\n */\nexport class DevCopy {\n /** The full ref name WITHOUT `refs/heads/`, e.g. `dev-include/dean/ONE-2275`. */\n ref: string;\n sha: string;\n /** The feature branch this is a copy OF — `ref` with the namespace prefix stripped. */\n sourceBranch: string;\n\n constructor(ref: string, sha: string, sourceBranch: string) {\n this.ref = ref;\n this.sha = sha;\n this.sourceBranch = sourceBranch;\n }\n}\n\n/**\n * Ref derivation and preconditions for the dev-deploy flow — the half of `wp-push-dev` that decides WHICH\n * refs are in play, kept out of the commands so both `wp-push-dev` and `wp-finish-push-dev` answer those\n * questions identically.\n *\n * THE INVARIANT THIS FILE EXISTS TO PROTECT: the feature branch is the PR head, so it must never acquire\n * another developer's commits — landing that PR would ship their unreviewed work to production. Every\n * write this flow performs therefore targets either the `<namespace>/<feature>` copy or a local throwaway\n * branch, and NOTHING here ever moves `<feature>` itself. That is why a conflict resolution needs the copy\n * to live in: it is the only place the resolution can be recorded without contaminating the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DevDeployRefs {\n constructor(private readonly gitExec: GitExec) {}\n\n /** The `pr-gate.devDeploy` block — `dev-include` / `dev` unless the consumer overrode it. */\n config(repoRoot: string): DevDeployConfig {\n return loadAndValidate(repoRoot).prGate.devDeploy;\n }\n\n /** The checked-out branch name, or `'HEAD'` on a detached HEAD (which {@link assertPushable} refuses). */\n currentBranch(repoRoot: string): string {\n return this.gitExec.gitQuery(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot,\n 'Failed to read the current branch (git rev-parse --abbrev-ref HEAD).');\n }\n\n /**\n * Refuse every branch that must never be the SOURCE of a dev copy, before anything is fetched or\n * pushed.\n *\n * `main` and the composed dev branch are refused for the same reason from opposite ends: `main` is the\n * trunk (whatever is on it already deploys, so copying it is meaningless), and the dev branch is a\n * BUILD ARTIFACT that CI recomputes from `origin/main` on every run — copying it would publish a\n * composition of everyone's work under one developer's name, and it would be wiped on the next CI run\n * anyway. A branch already inside the namespace is refused because publishing a copy of a copy nests\n * the prefix (`dev-include/dev-include/…`) and nothing would ever clean it up.\n */\n assertPushable(cfg: DevDeployConfig, branch: string): void {\n if (branch === DETACHED || branch === '') {\n throw new CliExitError(2, this.refusal(\n 'Detached HEAD',\n 'There is no branch name to derive the dev copy from. Check out your feature branch first:\\n'\n + ' git checkout <your-feature-branch>\\n'));\n }\n if (branch === MAIN) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of \\`${MAIN}\\``,\n `\\`${MAIN}\\` is the trunk — whatever is on it already deploys, so a dev copy of it means nothing.\\n`\n + 'Check out the feature branch you actually want on the dev server and re-run.\\n'));\n }\n if (branch === cfg.devBranch) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of \\`${cfg.devBranch}\\``,\n `\\`${cfg.devBranch}\\` is a BUILD ARTIFACT, not a source branch: your CI resets it to origin/${MAIN},\\n`\n + `merges every \\`${cfg.copyRefGlob()}\\` ref into it, and force-pushes — every run. Copying it would\\n`\n + \"publish a composition of everybody's work under your branch's name, and the next CI run would\\n\"\n + 'wipe it. Check out your own feature branch and re-run.\\n'));\n }\n if (branch === cfg.branchNamespace || branch.startsWith(`${cfg.branchNamespace}/`)) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of a dev copy (\\`${branch}\\`)`,\n `\\`${cfg.branchNamespace}/\\` holds the disposable copies this command WRITES; you are standing on one.\\n`\n + `Publishing from here would nest the prefix (\\`${cfg.branchNamespace}/${branch}\\`) and nothing ever\\n`\n + 'cleans that up. Check out the real feature branch and re-run.\\n'));\n }\n }\n\n /**\n * Every live copy in the namespace, sorted by ref name.\n *\n * The sort is not cosmetic: `--resolve` with no argument queues these in exactly this order, and the\n * consumer's CI composes `dev` in the same order, so what a developer resolves locally is the\n * composition CI will actually build. A different order can produce a different (still valid) merge\n * result, which would make the local resolution a guess about CI rather than a rehearsal of it.\n */\n liveCopies(repoRoot: string, cfg: DevDeployConfig): DevCopy[] {\n const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', cfg.copyRefGlob()], repoRoot,\n `Failed to list the dev copies (git ls-remote --heads origin '${cfg.copyRefGlob()}').`);\n const copies: DevCopy[] = [];\n for (const line of out.split('\\n')) {\n if (line.trim() === '') continue;\n const fields = line.split(/\\s+/);\n const sha = fields[0];\n const ref = fields[1];\n if (sha === undefined || ref === undefined || !ref.startsWith('refs/heads/')) continue;\n const shortRef = ref.slice('refs/heads/'.length);\n copies.push(new DevCopy(shortRef, sha, shortRef.slice(cfg.branchNamespace.length + 1)));\n }\n return copies.sort((a: DevCopy, b: DevCopy): number => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));\n }\n\n /** The remote sha of one copy, or '' when that copy does not exist yet. */\n remoteSha(repoRoot: string, ref: string): string {\n const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', ref], repoRoot,\n `Failed to look up origin/${ref} (git ls-remote).`);\n const line = out.split('\\n').find((l: string): boolean => l.trim() !== '');\n if (line === undefined) return '';\n return line.split(/\\s+/)[0] ?? '';\n }\n\n /**\n * Fetch the named copy refs into `refs/remotes/origin/<ref>` so the local merges below run against\n * fresh state. A no-op for an empty list, and a hard failure otherwise: a resolve queued against\n * stale refs rehearses a composition CI will not build.\n */\n fetchCopies(repoRoot: string, refs: string[]): void {\n if (refs.length === 0) return;\n const specs = refs.map((r: string): string => `+refs/heads/${r}:refs/remotes/origin/${r}`);\n this.gitExec.runGitChecked(['-C', repoRoot, 'fetch', 'origin', ...specs],\n 'Failed to fetch the dev copy refs from origin');\n }\n\n /** How many commits `origin/<ref>` has that `HEAD` does not — the clobber signal. 0 = safe to publish. */\n commitsOnlyOnRemote(repoRoot: string, ref: string): number {\n const out = this.gitExec.gitQuery(['rev-list', '--count', `HEAD..refs/remotes/origin/${ref}`], repoRoot,\n `Failed to compare HEAD against origin/${ref} (git rev-list --count).`);\n const count = Number(out);\n return Number.isInteger(count) ? count : 0;\n }\n\n private refusal(headline: string, body: string): string {\n return '\\n' + SEP + `⛔ ${headline}\\n` + SEP + '\\n' + body + `\\nRun \\`${WP_PUSH_DEV} --help\\` for the full command.\\n`;\n }\n}\n"]}
@@ -0,0 +1,72 @@
1
+ import { DevDeployConfig } from '@webpieces/rules-config';
2
+ import { GitExec } from './git-exec';
3
+ import { DevDeployRefs } from './dev-deploy-refs';
4
+ import { PushDevState, PushDevStateStore } from './push-dev-state';
5
+ /**
6
+ * The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.
7
+ *
8
+ * ONE class owns every git WRITE in the resolve flow, and that is a hard requirement rather than tidiness:
9
+ * `redirect-how-to-merge-main` blocks `git merge` in every form INCLUDING `--continue`, and
10
+ * `pr-creation-or-push-guard` blocks every `git push`. So an AI cannot be told "now run `git commit`" or
11
+ * "`git merge --continue`" — those instructions are unrunnable by the only party reading them. The command
12
+ * owns the merges, the commit and the push (as child processes the hooks never see); the AI's entire job is
13
+ * editing the conflicted files and `git add`-ing them, neither of which is blocked.
14
+ */
15
+ export declare class DevResolveRunner {
16
+ private readonly gitExec;
17
+ private readonly refs;
18
+ private readonly store;
19
+ constructor(gitExec: GitExec, refs: DevDeployRefs, store: PushDevStateStore);
20
+ /**
21
+ * Start a resolve: park on a throwaway branch at the current copy, then drain the queue.
22
+ *
23
+ * The checkout is `-B <tmp> <base>` and NEVER the feature branch, which is the cardinal constraint made
24
+ * mechanical — every merge below lands on a branch that is deleted at the end, so no other developer's
25
+ * commits can reach the PR head even if the resolve is abandoned halfway.
26
+ *
27
+ * `base` differs between the two callers and the difference IS the semantics. `--resolve` starts at the
28
+ * published copy and merges the other copies in (compose what CI will build). `--rebase-resolution`
29
+ * starts at the LOCAL HEAD and merges the published copy in (replay an existing resolution on top of
30
+ * the new commits, instead of force-pushing over it).
31
+ *
32
+ * `fetchRefs` is passed separately from the queue because `--rebase-resolution` merges a ref that is
33
+ * also its push destination, so "everything to fetch" and "everything to merge" are not the same list.
34
+ */
35
+ start(repoRoot: string, state: PushDevState, base: string, fetchRefs: string[]): void;
36
+ /**
37
+ * Resume after the human/AI has resolved the halted merge: commit it, then keep draining.
38
+ *
39
+ * REFETCHES the remaining queue first. A resolution session can run long, and queuing against refs
40
+ * fetched an hour ago rehearses a composition CI will not build — the one freshness gap the sibling
41
+ * update flow is still known to have (only its stage ① ever fetches).
42
+ */
43
+ resume(repoRoot: string, state: PushDevState): void;
44
+ /**
45
+ * Throw the whole resolve away: undo any halted merge, go back to the original branch, delete the
46
+ * throwaway branch and the state file. Nothing published, nothing on the feature branch.
47
+ */
48
+ abort(repoRoot: string, state: PushDevState): void;
49
+ /**
50
+ * Merge queued refs until one conflicts (HALT, hand the tree over) or the queue empties (PUBLISH).
51
+ *
52
+ * A LATER ref can conflict too, which is why this is a loop resumed from a state file rather than a
53
+ * single pass: `wp-finish-push-dev` can legitimately stop again on the next ref in the queue.
54
+ */
55
+ private drain;
56
+ /** Stop with the conflicted files listed and the ONE command that continues. Exit 2 = "your turn". */
57
+ private halt;
58
+ /** Queue empty: force-push the composed result over the copy, then restore the original checkout. */
59
+ private publish;
60
+ /** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */
61
+ private unmergedPaths;
62
+ private assertNoUnmergedPaths;
63
+ /**
64
+ * The name of the throwaway branch for a resolve of `branch`.
65
+ *
66
+ * Suffixed rather than prefixed, matching the existing `<feature>Squash` / `<feature>PreMerge<n>`
67
+ * convention — and deliberately NOT inside the dev namespace, which holds REMOTE copies only.
68
+ */
69
+ tmpBranchFor(branch: string): string;
70
+ /** The banner `--resolve` prints before the first merge, so the queue is visible before it halts. */
71
+ announce(cfg: DevDeployConfig, state: PushDevState): string;
72
+ }
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DevResolveRunner = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const rules_config_1 = require("@webpieces/rules-config");
6
+ const inversify_1 = require("inversify");
7
+ const git_exec_1 = require("./git-exec");
8
+ const dev_deploy_refs_1 = require("./dev-deploy-refs");
9
+ const push_dev_state_1 = require("./push-dev-state");
10
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
11
+ /**
12
+ * The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.
13
+ *
14
+ * ONE class owns every git WRITE in the resolve flow, and that is a hard requirement rather than tidiness:
15
+ * `redirect-how-to-merge-main` blocks `git merge` in every form INCLUDING `--continue`, and
16
+ * `pr-creation-or-push-guard` blocks every `git push`. So an AI cannot be told "now run `git commit`" or
17
+ * "`git merge --continue`" — those instructions are unrunnable by the only party reading them. The command
18
+ * owns the merges, the commit and the push (as child processes the hooks never see); the AI's entire job is
19
+ * editing the conflicted files and `git add`-ing them, neither of which is blocked.
20
+ */
21
+ let DevResolveRunner = class DevResolveRunner {
22
+ gitExec;
23
+ refs;
24
+ store;
25
+ constructor(gitExec, refs, store) {
26
+ this.gitExec = gitExec;
27
+ this.refs = refs;
28
+ this.store = store;
29
+ }
30
+ /**
31
+ * Start a resolve: park on a throwaway branch at the current copy, then drain the queue.
32
+ *
33
+ * The checkout is `-B <tmp> <base>` and NEVER the feature branch, which is the cardinal constraint made
34
+ * mechanical — every merge below lands on a branch that is deleted at the end, so no other developer's
35
+ * commits can reach the PR head even if the resolve is abandoned halfway.
36
+ *
37
+ * `base` differs between the two callers and the difference IS the semantics. `--resolve` starts at the
38
+ * published copy and merges the other copies in (compose what CI will build). `--rebase-resolution`
39
+ * starts at the LOCAL HEAD and merges the published copy in (replay an existing resolution on top of
40
+ * the new commits, instead of force-pushing over it).
41
+ *
42
+ * `fetchRefs` is passed separately from the queue because `--rebase-resolution` merges a ref that is
43
+ * also its push destination, so "everything to fetch" and "everything to merge" are not the same list.
44
+ */
45
+ start(repoRoot, state, base, fetchRefs) {
46
+ this.refs.fetchCopies(repoRoot, fetchRefs);
47
+ this.store.write(repoRoot, state);
48
+ this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', '-B', state.tmpBranch, base], `Failed to create the throwaway resolve branch ${state.tmpBranch}`);
49
+ this.drain(repoRoot, state);
50
+ }
51
+ /**
52
+ * Resume after the human/AI has resolved the halted merge: commit it, then keep draining.
53
+ *
54
+ * REFETCHES the remaining queue first. A resolution session can run long, and queuing against refs
55
+ * fetched an hour ago rehearses a composition CI will not build — the one freshness gap the sibling
56
+ * update flow is still known to have (only its stage ① ever fetches).
57
+ */
58
+ resume(repoRoot, state) {
59
+ this.assertNoUnmergedPaths(repoRoot, state);
60
+ this.gitExec.assertNoUntracked(repoRoot);
61
+ this.gitExec.runGitChecked(['-C', repoRoot, 'add', '-u'], 'Failed to stage the resolved files (git add -u)');
62
+ this.gitExec.runGitChecked(['-C', repoRoot, 'commit', '--no-edit'], `Failed to commit the resolution of ${state.current}`);
63
+ process.stdout.write(`✅ Committed the resolution of ${state.current}.\n`);
64
+ state.current = '';
65
+ this.refs.fetchCopies(repoRoot, state.queue);
66
+ this.drain(repoRoot, state);
67
+ }
68
+ /**
69
+ * Throw the whole resolve away: undo any halted merge, go back to the original branch, delete the
70
+ * throwaway branch and the state file. Nothing published, nothing on the feature branch.
71
+ */
72
+ abort(repoRoot, state) {
73
+ // Allowed to fail: there is no merge to abort when the resolve halted between merges, and that is
74
+ // not an error condition — the rest of the teardown must still run.
75
+ this.gitExec.tryGit(['-C', repoRoot, 'merge', '--abort'], repoRoot);
76
+ this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch], `Failed to return to ${state.originalBranch}`);
77
+ this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);
78
+ this.store.clear(repoRoot);
79
+ process.stdout.write('\n' + SEP + '🛑 Dev-deploy resolve aborted\n' + SEP + '\n'
80
+ + `You are back on \`${state.originalBranch}\`; \`${state.tmpBranch}\` and the state file are gone.\n`
81
+ + `The remote copy \`${state.targetRef}\` is untouched, and so is your feature branch.\n`);
82
+ }
83
+ /**
84
+ * Merge queued refs until one conflicts (HALT, hand the tree over) or the queue empties (PUBLISH).
85
+ *
86
+ * A LATER ref can conflict too, which is why this is a loop resumed from a state file rather than a
87
+ * single pass: `wp-finish-push-dev` can legitimately stop again on the next ref in the queue.
88
+ */
89
+ drain(repoRoot, state) {
90
+ while (state.queue.length > 0) {
91
+ const ref = state.queue[0];
92
+ state.queue = state.queue.slice(1);
93
+ const outcome = this.gitExec.tryGit(['-C', repoRoot, 'merge', '--no-ff', '--no-edit', '-m',
94
+ `Dev-deploy: merge ${ref} into ${state.targetRef}`, `refs/remotes/origin/${ref}`], repoRoot);
95
+ if (outcome.ok) {
96
+ process.stdout.write(` ✓ merged ${ref}\n`);
97
+ continue;
98
+ }
99
+ state.current = ref;
100
+ this.store.write(repoRoot, state);
101
+ this.halt(repoRoot, state);
102
+ return;
103
+ }
104
+ this.publish(repoRoot, state);
105
+ }
106
+ /** Stop with the conflicted files listed and the ONE command that continues. Exit 2 = "your turn". */
107
+ halt(repoRoot, state) {
108
+ const conflicted = this.unmergedPaths(repoRoot);
109
+ const remaining = state.queue.length > 0
110
+ ? `\nStill queued after this one: ${state.queue.join(', ')}\n`
111
+ + `\`${rules_config_1.WP_FINISH_PUSH_DEV}\` resumes the queue, so it may stop again on a later ref.\n`
112
+ : '';
113
+ throw new rules_config_1.CliExitError(2, '\n' + SEP + `⚠️ CONFLICT merging ${state.current}\n` + SEP + '\n'
114
+ + `You are on the throwaway branch \`${state.tmpBranch}\` — NOT \`${state.originalBranch}\`. Nothing here\n`
115
+ + 'can reach your feature branch or your PR, which is exactly why the resolution happens over here.\n\n'
116
+ + 'Resolve these files (edit until no conflict markers remain, then `git add` each one):\n'
117
+ + conflicted.map((f) => ` ${f}`).join('\n') + '\n'
118
+ + remaining
119
+ + '\nThen run ONE of:\n'
120
+ + ` ${rules_config_1.WP_FINISH_PUSH_DEV} ← commit, resume the queue, publish the copy\n`
121
+ + ` ${rules_config_1.WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \`${state.originalBranch}\`\n\n`
122
+ + 'Do NOT run `git merge --continue`, `git commit` or `git push` yourself — they are blocked, and\n'
123
+ + 'the finish command above does all three for you.\n');
124
+ }
125
+ /** Queue empty: force-push the composed result over the copy, then restore the original checkout. */
126
+ publish(repoRoot, state) {
127
+ this.gitExec.runGitChecked(['-C', repoRoot, 'push', '--force', 'origin', `${state.tmpBranch}:refs/heads/${state.targetRef}`], `Failed to publish the dev copy ${state.targetRef}`);
128
+ this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch], `Failed to return to ${state.originalBranch}`);
129
+ this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);
130
+ this.store.clear(repoRoot);
131
+ process.stdout.write('\n' + SEP + `✅ Published ${state.targetRef} with the resolutions\n` + SEP + '\n'
132
+ + `You are back on \`${state.originalBranch}\`, which was never modified — verify with:\n`
133
+ + ` git log --oneline ${state.originalBranch}\n`
134
+ + 'Your CI will pick the copy up on its next composition run.\n');
135
+ }
136
+ /** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */
137
+ unmergedPaths(repoRoot) {
138
+ const out = this.gitExec.gitQuery(['-C', repoRoot, 'diff', '--name-only', '--diff-filter=U'], repoRoot, 'Failed to list the conflicted files (git diff --name-only --diff-filter=U).');
139
+ return out.split('\n').filter((l) => l.trim() !== '');
140
+ }
141
+ // The gate on `wp-finish-push-dev`: refuse (and re-print the list) while anything is still unmerged.
142
+ assertNoUnmergedPaths(repoRoot, state) {
143
+ const conflicted = this.unmergedPaths(repoRoot);
144
+ if (conflicted.length === 0)
145
+ return;
146
+ throw new rules_config_1.CliExitError(2, '\n' + SEP + '⛔ Conflicts are NOT resolved yet\n' + SEP + '\n'
147
+ + `${rules_config_1.WP_FINISH_PUSH_DEV} commits the resolution — it cannot write one for you. These files are still\n`
148
+ + 'unmerged (edit until no conflict markers remain, then `git add` each one):\n'
149
+ + conflicted.map((f) => ` ${f}`).join('\n') + '\n\n'
150
+ + `Merging: ${state.current === '' ? '(none)' : state.current}\n`
151
+ + `Give up instead with: ${rules_config_1.WP_FINISH_PUSH_DEV} --abort\n`);
152
+ }
153
+ /**
154
+ * The name of the throwaway branch for a resolve of `branch`.
155
+ *
156
+ * Suffixed rather than prefixed, matching the existing `<feature>Squash` / `<feature>PreMerge<n>`
157
+ * convention — and deliberately NOT inside the dev namespace, which holds REMOTE copies only.
158
+ */
159
+ tmpBranchFor(branch) {
160
+ return `${branch}DevResolve`;
161
+ }
162
+ /** The banner `--resolve` prints before the first merge, so the queue is visible before it halts. */
163
+ announce(cfg, state) {
164
+ return '\n' + SEP + `🔀 Resolving the dev composition for ${state.targetRef}\n` + SEP + '\n'
165
+ + `Merging ${String(state.queue.length)} other cop${state.queue.length === 1 ? 'y' : 'ies'} from `
166
+ + `\`${cfg.copyRefGlob()}\`, in the order your CI composes\n\`${cfg.devBranch}\`, onto the throwaway branch `
167
+ + `\`${state.tmpBranch}\`:\n`
168
+ + state.queue.map((r) => ` ${r}`).join('\n') + '\n\n'
169
+ + `Your feature branch \`${state.originalBranch}\` is not touched by any of this.\n`
170
+ + `A clean run publishes the copy itself — ${rules_config_1.WP_FINISH_PUSH_DEV} is only needed if a merge stops.\n\n`;
171
+ }
172
+ };
173
+ exports.DevResolveRunner = DevResolveRunner;
174
+ exports.DevResolveRunner = DevResolveRunner = tslib_1.__decorate([
175
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
176
+ tslib_1.__metadata("design:paramtypes", [git_exec_1.GitExec,
177
+ dev_deploy_refs_1.DevDeployRefs,
178
+ push_dev_state_1.PushDevStateStore])
179
+ ], DevResolveRunner);
180
+ //# sourceMappingURL=dev-resolve-runner.js.map