@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.
- package/bin/wp-finish-push-dev.js +27 -0
- package/bin/wp-push-dev.js +27 -0
- package/package.json +5 -3
- package/src/scripts/commands/finish-push-dev-command.d.ts +28 -0
- package/src/scripts/commands/finish-push-dev-command.js +62 -0
- package/src/scripts/commands/finish-push-dev-command.js.map +1 -0
- package/src/scripts/commands/push-dev-command.d.ts +89 -0
- package/src/scripts/commands/push-dev-command.js +249 -0
- package/src/scripts/commands/push-dev-command.js.map +1 -0
- package/src/scripts/pr-gate-app.d.ts +22 -1
- package/src/scripts/pr-gate-app.js +46 -2
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/dev-deploy-refs.d.ts +66 -0
- package/src/scripts/workflow/dev-deploy-refs.js +143 -0
- package/src/scripts/workflow/dev-deploy-refs.js.map +1 -0
- package/src/scripts/workflow/dev-resolve-runner.d.ts +72 -0
- package/src/scripts/workflow/dev-resolve-runner.js +180 -0
- package/src/scripts/workflow/dev-resolve-runner.js.map +1 -0
- package/src/scripts/workflow/git-exec.d.ts +20 -1
- package/src/scripts/workflow/git-exec.js +30 -3
- package/src/scripts/workflow/git-exec.js.map +1 -1
- package/src/scripts/workflow/push-dev-state.d.ts +52 -0
- package/src/scripts/workflow/push-dev-state.js +147 -0
- package/src/scripts/workflow/push-dev-state.js.map +1 -0
- package/src/scripts/wp-finish-push-dev.d.ts +2 -0
- package/src/scripts/wp-finish-push-dev.js +20 -0
- package/src/scripts/wp-finish-push-dev.js.map +1 -0
- package/src/scripts/wp-push-dev.d.ts +2 -0
- package/src/scripts/wp-push-dev.js +44 -0
- package/src/scripts/wp-push-dev.js.map +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Plain JS shim -> delegates to the compiled TypeScript entry point.
|
|
3
|
+
//
|
|
4
|
+
// Must NOT be converted to TypeScript. pnpm chmods every `bin` target while it links a
|
|
5
|
+
// package, and in THIS workspace a `workspace:*` sibling is linked from its SOURCE dir,
|
|
6
|
+
// long before any build has produced `src/scripts/wp-finish-push-dev.js`. Pointing `bin` straight at the
|
|
7
|
+
// compiled path therefore makes every `pnpm install` print
|
|
8
|
+
// `WARN Failed to create bin ... ENOENT ... chmod` — noise indistinguishable from a real
|
|
9
|
+
// bin-link failure. This file exists in git, so the chmod always succeeds.
|
|
10
|
+
//
|
|
11
|
+
// See setupDebugging.md (Attempt 8) and bin-targets-exist.spec.ts, which fails if any
|
|
12
|
+
// `bin` target in the workspace does not exist on disk.
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
|
|
18
|
+
const compiled = path.join(__dirname, '..', 'src', 'scripts', 'wp-finish-push-dev.js');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(compiled)) {
|
|
21
|
+
console.error(
|
|
22
|
+
' [pr-gate] wp-finish-push-dev: package not built yet. Run the build first, or install from npm.',
|
|
23
|
+
);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
require(compiled);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Plain JS shim -> delegates to the compiled TypeScript entry point.
|
|
3
|
+
//
|
|
4
|
+
// Must NOT be converted to TypeScript. pnpm chmods every `bin` target while it links a
|
|
5
|
+
// package, and in THIS workspace a `workspace:*` sibling is linked from its SOURCE dir,
|
|
6
|
+
// long before any build has produced `src/scripts/wp-push-dev.js`. Pointing `bin` straight at the
|
|
7
|
+
// compiled path therefore makes every `pnpm install` print
|
|
8
|
+
// `WARN Failed to create bin ... ENOENT ... chmod` — noise indistinguishable from a real
|
|
9
|
+
// bin-link failure. This file exists in git, so the chmod always succeeds.
|
|
10
|
+
//
|
|
11
|
+
// See setupDebugging.md (Attempt 8) and bin-targets-exist.spec.ts, which fails if any
|
|
12
|
+
// `bin` target in the workspace does not exist on disk.
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
|
|
18
|
+
const compiled = path.join(__dirname, '..', 'src', 'scripts', 'wp-push-dev.js');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(compiled)) {
|
|
21
|
+
console.error(
|
|
22
|
+
' [pr-gate] wp-push-dev: package not built yet. Run the build first, or install from npm.',
|
|
23
|
+
);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
require(compiled);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.566",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"wp-cleanup": "./bin/wp-cleanup.js",
|
|
13
13
|
"wp-land-pr": "./bin/wp-land-pr.js",
|
|
14
14
|
"wp-check-pr": "./bin/wp-check-pr.js",
|
|
15
|
-
"wp-review-upsert-pr": "./bin/wp-review-upsert-pr.js"
|
|
15
|
+
"wp-review-upsert-pr": "./bin/wp-review-upsert-pr.js",
|
|
16
|
+
"wp-push-dev": "./bin/wp-push-dev.js",
|
|
17
|
+
"wp-finish-push-dev": "./bin/wp-finish-push-dev.js"
|
|
16
18
|
},
|
|
17
19
|
"files": [
|
|
18
20
|
"src/**/*",
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
"directory": "packages/tooling/pr-gate"
|
|
27
29
|
},
|
|
28
30
|
"dependencies": {
|
|
29
|
-
"@webpieces/rules-config": "0.4.
|
|
31
|
+
"@webpieces/rules-config": "0.4.566",
|
|
30
32
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
31
33
|
"inversify": "7.10.4",
|
|
32
34
|
"reflect-metadata": "0.2.2"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RepoRootFinder } from '@webpieces/rules-config';
|
|
2
|
+
import { PushDevStateStore } from '../workflow/push-dev-state';
|
|
3
|
+
import { DevResolveRunner } from '../workflow/dev-resolve-runner';
|
|
4
|
+
/** What `wp-finish-push-dev` was asked to do. Data-only (per CLAUDE.md). */
|
|
5
|
+
export declare class FinishPushDevOptions {
|
|
6
|
+
/** Throw the whole resolve away and restore the original checkout. */
|
|
7
|
+
abort: boolean;
|
|
8
|
+
constructor(abort?: boolean);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `wp-finish-push-dev` — stage ② of the dev-deploy flow, needed ONLY when a merge stopped.
|
|
12
|
+
*
|
|
13
|
+
* It mirrors `wp-start-update` → `wp-finish-update` on purpose: the repo already has a two-phase
|
|
14
|
+
* start/finish idiom for exactly this shape ("the tool merges, you resolve, the tool commits"), and a
|
|
15
|
+
* `--continue` idiom invented here would be a second vocabulary for one concept. A clean `wp-push-dev
|
|
16
|
+
* --resolve` finalizes itself and this command is never needed.
|
|
17
|
+
*
|
|
18
|
+
* The command owns the commit because the AI cannot make it: `redirect-how-to-merge-main` blocks
|
|
19
|
+
* `git merge --continue` and `merge-in-progress-guard` blocks `git commit`. Editing the conflicted files
|
|
20
|
+
* and `git add`-ing them is the AI's whole job, and neither is blocked.
|
|
21
|
+
*/
|
|
22
|
+
export declare class FinishPushDevCommand {
|
|
23
|
+
private readonly repoRootFinder;
|
|
24
|
+
private readonly store;
|
|
25
|
+
private readonly runner;
|
|
26
|
+
constructor(repoRootFinder: RepoRootFinder, store: PushDevStateStore, runner: DevResolveRunner);
|
|
27
|
+
run(opts: FinishPushDevOptions): Promise<void>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FinishPushDevCommand = exports.FinishPushDevOptions = 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 push_dev_state_1 = require("../workflow/push-dev-state");
|
|
8
|
+
const dev_resolve_runner_1 = require("../workflow/dev-resolve-runner");
|
|
9
|
+
/** What `wp-finish-push-dev` was asked to do. Data-only (per CLAUDE.md). */
|
|
10
|
+
class FinishPushDevOptions {
|
|
11
|
+
/** Throw the whole resolve away and restore the original checkout. */
|
|
12
|
+
abort = false;
|
|
13
|
+
constructor(abort = false) {
|
|
14
|
+
this.abort = abort;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.FinishPushDevOptions = FinishPushDevOptions;
|
|
18
|
+
/**
|
|
19
|
+
* `wp-finish-push-dev` — stage ② of the dev-deploy flow, needed ONLY when a merge stopped.
|
|
20
|
+
*
|
|
21
|
+
* It mirrors `wp-start-update` → `wp-finish-update` on purpose: the repo already has a two-phase
|
|
22
|
+
* start/finish idiom for exactly this shape ("the tool merges, you resolve, the tool commits"), and a
|
|
23
|
+
* `--continue` idiom invented here would be a second vocabulary for one concept. A clean `wp-push-dev
|
|
24
|
+
* --resolve` finalizes itself and this command is never needed.
|
|
25
|
+
*
|
|
26
|
+
* The command owns the commit because the AI cannot make it: `redirect-how-to-merge-main` blocks
|
|
27
|
+
* `git merge --continue` and `merge-in-progress-guard` blocks `git commit`. Editing the conflicted files
|
|
28
|
+
* and `git add`-ing them is the AI's whole job, and neither is blocked.
|
|
29
|
+
*/
|
|
30
|
+
let FinishPushDevCommand = class FinishPushDevCommand {
|
|
31
|
+
repoRootFinder;
|
|
32
|
+
store;
|
|
33
|
+
runner;
|
|
34
|
+
constructor(repoRootFinder, store, runner) {
|
|
35
|
+
this.repoRootFinder = repoRootFinder;
|
|
36
|
+
this.store = store;
|
|
37
|
+
this.runner = runner;
|
|
38
|
+
}
|
|
39
|
+
async run(opts) {
|
|
40
|
+
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
41
|
+
(0, rules_config_1.writeTemplate)(repoRoot, 'webpieces.git-workflow.md');
|
|
42
|
+
// Refuses with the one-command publish form when nothing is in flight, so a mistaken call here
|
|
43
|
+
// does not read as "the flow is broken".
|
|
44
|
+
const state = this.store.require(repoRoot);
|
|
45
|
+
if (opts.abort) {
|
|
46
|
+
this.runner.abort(repoRoot, state);
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
}
|
|
49
|
+
// Refuses (and re-prints the unresolved files) before committing anything, then resumes the
|
|
50
|
+
// queue — a LATER ref can conflict too, so this may legitimately stop again.
|
|
51
|
+
this.runner.resume(repoRoot, state);
|
|
52
|
+
return Promise.resolve();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
exports.FinishPushDevCommand = FinishPushDevCommand;
|
|
56
|
+
exports.FinishPushDevCommand = FinishPushDevCommand = tslib_1.__decorate([
|
|
57
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
58
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
|
|
59
|
+
push_dev_state_1.PushDevStateStore,
|
|
60
|
+
dev_resolve_runner_1.DevResolveRunner])
|
|
61
|
+
], FinishPushDevCommand);
|
|
62
|
+
//# sourceMappingURL=finish-push-dev-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finish-push-dev-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/finish-push-dev-command.ts"],"names":[],"mappings":";;;;AAAA,0DAAwE;AACxE,yCAA2D;AAE3D,+DAA+D;AAC/D,uEAAkE;AAElE,4EAA4E;AAC5E,MAAa,oBAAoB;IAC7B,sEAAsE;IACtE,KAAK,GAAG,KAAK,CAAC;IAEd,YAAY,KAAK,GAAG,KAAK;QACrB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AAPD,oDAOC;AAED;;;;;;;;;;;GAWG;AAEI,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAER;IACA;IACA;IAHrB,YACqB,cAA8B,EAC9B,KAAwB,EACxB,MAAwB;QAFxB,mBAAc,GAAd,cAAc,CAAgB;QAC9B,UAAK,GAAL,KAAK,CAAmB;QACxB,WAAM,GAAN,MAAM,CAAkB;IAC1C,CAAC;IAEJ,KAAK,CAAC,GAAG,CAAC,IAA0B;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,IAAA,4BAAa,EAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAC;QACrD,+FAA+F;QAC/F,yCAAyC;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAE3C,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YACnC,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC7B,CAAC;QACD,4FAA4F;QAC5F,6EAA6E;QAC7E,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACpC,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;CACJ,CAAA;AAvBY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,6BAAc;QACvB,kCAAiB;QAChB,qCAAgB;GAJpC,oBAAoB,CAuBhC","sourcesContent":["import { RepoRootFinder, writeTemplate } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { PushDevStateStore } from '../workflow/push-dev-state';\nimport { DevResolveRunner } from '../workflow/dev-resolve-runner';\n\n/** What `wp-finish-push-dev` was asked to do. Data-only (per CLAUDE.md). */\nexport class FinishPushDevOptions {\n /** Throw the whole resolve away and restore the original checkout. */\n abort = false;\n\n constructor(abort = false) {\n this.abort = abort;\n }\n}\n\n/**\n * `wp-finish-push-dev` — stage ② of the dev-deploy flow, needed ONLY when a merge stopped.\n *\n * It mirrors `wp-start-update` → `wp-finish-update` on purpose: the repo already has a two-phase\n * start/finish idiom for exactly this shape (\"the tool merges, you resolve, the tool commits\"), and a\n * `--continue` idiom invented here would be a second vocabulary for one concept. A clean `wp-push-dev\n * --resolve` finalizes itself and this command is never needed.\n *\n * The command owns the commit because the AI cannot make it: `redirect-how-to-merge-main` blocks\n * `git merge --continue` and `merge-in-progress-guard` blocks `git commit`. Editing the conflicted files\n * and `git add`-ing them is the AI's whole job, and neither is blocked.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class FinishPushDevCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly store: PushDevStateStore,\n private readonly runner: DevResolveRunner,\n ) {}\n\n async run(opts: FinishPushDevOptions): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n // Refuses with the one-command publish form when nothing is in flight, so a mistaken call here\n // does not read as \"the flow is broken\".\n const state = this.store.require(repoRoot);\n\n if (opts.abort) {\n this.runner.abort(repoRoot, state);\n return Promise.resolve();\n }\n // Refuses (and re-prints the unresolved files) before committing anything, then resumes the\n // queue — a LATER ref can conflict too, so this may legitimately stop again.\n this.runner.resume(repoRoot, state);\n return Promise.resolve();\n }\n}\n"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { RepoRootFinder } from '@webpieces/rules-config';
|
|
2
|
+
import { GitExec } from '../workflow/git-exec';
|
|
3
|
+
import { DevDeployRefs } from '../workflow/dev-deploy-refs';
|
|
4
|
+
import { PushDevStateStore } from '../workflow/push-dev-state';
|
|
5
|
+
import { DevResolveRunner } from '../workflow/dev-resolve-runner';
|
|
6
|
+
/**
|
|
7
|
+
* Which of `wp-push-dev`'s modes was asked for. Data-only (per CLAUDE.md) with plain assigned fields
|
|
8
|
+
* rather than a seven-argument constructor — the bin sets what argv carried and nothing else.
|
|
9
|
+
*/
|
|
10
|
+
export declare class PushDevOptions {
|
|
11
|
+
/** Delete the remote copy — the rollback. */
|
|
12
|
+
remove: boolean;
|
|
13
|
+
/** List the live copies and stop. The one mode that needs no branch preconditions. */
|
|
14
|
+
list: boolean;
|
|
15
|
+
/** Discard a conflict resolution already published on the copy, and overwrite it. */
|
|
16
|
+
force: boolean;
|
|
17
|
+
/** Replay that resolution on top of the new local commits instead of discarding it. */
|
|
18
|
+
rebaseResolution: boolean;
|
|
19
|
+
/** Compose the other copies onto this one so the shared environment can build them together. */
|
|
20
|
+
resolve: boolean;
|
|
21
|
+
/** With `--resolve`: merge ONLY this developer's branch. Empty ⇒ every other copy. */
|
|
22
|
+
resolveTarget: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `wp-push-dev` — publish a DISPOSABLE copy of this branch so a shared dev environment can build it,
|
|
26
|
+
* without landing anything on `main` and without opening a PR.
|
|
27
|
+
*
|
|
28
|
+
* WHY THIS COMMAND HAS TO EXIST AT ALL: `pr-creation-or-push-guard` blocks every `git push`, no
|
|
29
|
+
* exceptions, by design. Its own fix-hint offers "ask a human to run the push" for out-of-band cases —
|
|
30
|
+
* which is the right answer for a genuine exception and the wrong one for a routine daily action. Without
|
|
31
|
+
* a gated command there is simply no legal way for an AI to get a branch onto a dev server, so the
|
|
32
|
+
* feature was undoable rather than merely awkward.
|
|
33
|
+
*
|
|
34
|
+
* WHY THERE IS NO BUILD, deliberately unlike `wp-start-upsert-pr`: the shared environment's CI builds the
|
|
35
|
+
* COMPOSED tree (main + every listed branch). A local build here proves the branch compiles against
|
|
36
|
+
* `main` alone, which is not the question — it would be both slower and reassuring about the wrong thing.
|
|
37
|
+
* The gate that matters runs on the composition, in CI, where it belongs.
|
|
38
|
+
*
|
|
39
|
+
* WHAT IT NEVER DOES: touch the feature branch. See DevDeployRefs for the invariant and why the copy
|
|
40
|
+
* exists to hold conflict resolutions that must not reach the PR.
|
|
41
|
+
*/
|
|
42
|
+
export declare class PushDevCommand {
|
|
43
|
+
private readonly repoRootFinder;
|
|
44
|
+
private readonly gitExec;
|
|
45
|
+
private readonly refs;
|
|
46
|
+
private readonly store;
|
|
47
|
+
private readonly runner;
|
|
48
|
+
constructor(repoRootFinder: RepoRootFinder, gitExec: GitExec, refs: DevDeployRefs, store: PushDevStateStore, runner: DevResolveRunner);
|
|
49
|
+
run(opts: PushDevOptions): Promise<void>;
|
|
50
|
+
/** `--list`: exactly the live copies, straight from the remote — never a cached local view. */
|
|
51
|
+
private list;
|
|
52
|
+
/** `--remove`: delete the remote copy. This IS the rollback — nothing else has to be undone. */
|
|
53
|
+
private remove;
|
|
54
|
+
/**
|
|
55
|
+
* The ordinary path: publish HEAD as `<namespace>/<branch>`.
|
|
56
|
+
*
|
|
57
|
+
* THE CLOBBER GUARD is the whole of the interesting logic here. If the remote copy holds commits the
|
|
58
|
+
* local branch does not, somebody (quite possibly this developer, days ago) resolved a real conflict
|
|
59
|
+
* over there. A silent force-push destroys work that cost a human a genuine merge resolution, so the
|
|
60
|
+
* default is refusal with two named ways forward — never a "use --force" one-liner, which is how a
|
|
61
|
+
* warning becomes a reflex.
|
|
62
|
+
*/
|
|
63
|
+
private publish;
|
|
64
|
+
/**
|
|
65
|
+
* `--rebase-resolution`: keep the published resolution, replay it on top of the new commits.
|
|
66
|
+
*
|
|
67
|
+
* Implemented as a resolve whose BASE is the local HEAD and whose queue is the copy itself, so a
|
|
68
|
+
* replay that conflicts again lands in exactly the same halt/`wp-finish-push-dev` state machine as
|
|
69
|
+
* any other conflict, rather than inventing a second way to be stuck.
|
|
70
|
+
*/
|
|
71
|
+
private rebaseResolution;
|
|
72
|
+
/**
|
|
73
|
+
* `--resolve`: compose the other copies onto this one, so what is published is what CI will build.
|
|
74
|
+
*
|
|
75
|
+
* With no argument the queue is EVERY other copy, in the same sorted order CI composes them — the
|
|
76
|
+
* local resolution is then a rehearsal of the real composition, not a guess at it. With an argument
|
|
77
|
+
* (the branch a CI conflict message names) it is just that one.
|
|
78
|
+
*/
|
|
79
|
+
private startResolve;
|
|
80
|
+
private resolveQueue;
|
|
81
|
+
/**
|
|
82
|
+
* `--force-with-lease` pinned to the sha we just inspected, so the clobber guard's verdict cannot go
|
|
83
|
+
* stale between the check and the push. Plain `--force` only when the caller explicitly asked to
|
|
84
|
+
* discard, and no force at all for a brand-new copy.
|
|
85
|
+
*/
|
|
86
|
+
private pushSafety;
|
|
87
|
+
private refuseClobber;
|
|
88
|
+
private assertNoResolveInProgress;
|
|
89
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PushDevCommand = exports.PushDevOptions = 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("../workflow/git-exec");
|
|
8
|
+
const dev_deploy_refs_1 = require("../workflow/dev-deploy-refs");
|
|
9
|
+
const push_dev_state_1 = require("../workflow/push-dev-state");
|
|
10
|
+
const dev_resolve_runner_1 = require("../workflow/dev-resolve-runner");
|
|
11
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
12
|
+
/**
|
|
13
|
+
* Which of `wp-push-dev`'s modes was asked for. Data-only (per CLAUDE.md) with plain assigned fields
|
|
14
|
+
* rather than a seven-argument constructor — the bin sets what argv carried and nothing else.
|
|
15
|
+
*/
|
|
16
|
+
class PushDevOptions {
|
|
17
|
+
/** Delete the remote copy — the rollback. */
|
|
18
|
+
remove = false;
|
|
19
|
+
/** List the live copies and stop. The one mode that needs no branch preconditions. */
|
|
20
|
+
list = false;
|
|
21
|
+
/** Discard a conflict resolution already published on the copy, and overwrite it. */
|
|
22
|
+
force = false;
|
|
23
|
+
/** Replay that resolution on top of the new local commits instead of discarding it. */
|
|
24
|
+
rebaseResolution = false;
|
|
25
|
+
/** Compose the other copies onto this one so the shared environment can build them together. */
|
|
26
|
+
resolve = false;
|
|
27
|
+
/** With `--resolve`: merge ONLY this developer's branch. Empty ⇒ every other copy. */
|
|
28
|
+
resolveTarget = '';
|
|
29
|
+
}
|
|
30
|
+
exports.PushDevOptions = PushDevOptions;
|
|
31
|
+
/**
|
|
32
|
+
* `wp-push-dev` — publish a DISPOSABLE copy of this branch so a shared dev environment can build it,
|
|
33
|
+
* without landing anything on `main` and without opening a PR.
|
|
34
|
+
*
|
|
35
|
+
* WHY THIS COMMAND HAS TO EXIST AT ALL: `pr-creation-or-push-guard` blocks every `git push`, no
|
|
36
|
+
* exceptions, by design. Its own fix-hint offers "ask a human to run the push" for out-of-band cases —
|
|
37
|
+
* which is the right answer for a genuine exception and the wrong one for a routine daily action. Without
|
|
38
|
+
* a gated command there is simply no legal way for an AI to get a branch onto a dev server, so the
|
|
39
|
+
* feature was undoable rather than merely awkward.
|
|
40
|
+
*
|
|
41
|
+
* WHY THERE IS NO BUILD, deliberately unlike `wp-start-upsert-pr`: the shared environment's CI builds the
|
|
42
|
+
* COMPOSED tree (main + every listed branch). A local build here proves the branch compiles against
|
|
43
|
+
* `main` alone, which is not the question — it would be both slower and reassuring about the wrong thing.
|
|
44
|
+
* The gate that matters runs on the composition, in CI, where it belongs.
|
|
45
|
+
*
|
|
46
|
+
* WHAT IT NEVER DOES: touch the feature branch. See DevDeployRefs for the invariant and why the copy
|
|
47
|
+
* exists to hold conflict resolutions that must not reach the PR.
|
|
48
|
+
*/
|
|
49
|
+
let PushDevCommand = class PushDevCommand {
|
|
50
|
+
repoRootFinder;
|
|
51
|
+
gitExec;
|
|
52
|
+
refs;
|
|
53
|
+
store;
|
|
54
|
+
runner;
|
|
55
|
+
constructor(repoRootFinder, gitExec, refs, store, runner) {
|
|
56
|
+
this.repoRootFinder = repoRootFinder;
|
|
57
|
+
this.gitExec = gitExec;
|
|
58
|
+
this.refs = refs;
|
|
59
|
+
this.store = store;
|
|
60
|
+
this.runner = runner;
|
|
61
|
+
}
|
|
62
|
+
async run(opts) {
|
|
63
|
+
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
64
|
+
// Refresh the AI-facing workflow doc so it is present + current for any failure message to cite —
|
|
65
|
+
// same reason every other wp-* command opens this way.
|
|
66
|
+
(0, rules_config_1.writeTemplate)(repoRoot, 'webpieces.git-workflow.md');
|
|
67
|
+
const cfg = this.refs.config(repoRoot);
|
|
68
|
+
// A resolve already in flight owns the checkout; starting a second one would strand the first.
|
|
69
|
+
this.assertNoResolveInProgress(repoRoot);
|
|
70
|
+
if (opts.list) {
|
|
71
|
+
this.list(repoRoot, cfg);
|
|
72
|
+
return Promise.resolve();
|
|
73
|
+
}
|
|
74
|
+
const branch = this.refs.currentBranch(repoRoot);
|
|
75
|
+
this.refs.assertPushable(cfg, branch);
|
|
76
|
+
const targetRef = cfg.copyRefFor(branch);
|
|
77
|
+
if (opts.remove) {
|
|
78
|
+
this.remove(repoRoot, cfg, branch, targetRef);
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
}
|
|
81
|
+
// Every remaining mode commits or merges, so the tree must be the AI's committed work and nothing
|
|
82
|
+
// else. The tooling never commits for you.
|
|
83
|
+
this.gitExec.assertCleanTree(repoRoot);
|
|
84
|
+
if (opts.resolve) {
|
|
85
|
+
this.startResolve(repoRoot, cfg, branch, targetRef, opts.resolveTarget);
|
|
86
|
+
return Promise.resolve();
|
|
87
|
+
}
|
|
88
|
+
this.publish(repoRoot, cfg, branch, targetRef, opts);
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
}
|
|
91
|
+
/** `--list`: exactly the live copies, straight from the remote — never a cached local view. */
|
|
92
|
+
list(repoRoot, cfg) {
|
|
93
|
+
const copies = this.refs.liveCopies(repoRoot, cfg);
|
|
94
|
+
if (copies.length === 0) {
|
|
95
|
+
process.stdout.write(`\nNo dev copies published — nothing matches \`${cfg.copyRefGlob()}\` on origin.\n`
|
|
96
|
+
+ `Publish this branch's with: ${rules_config_1.WP_PUSH_DEV}\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let out = '\n' + SEP + `📋 ${String(copies.length)} dev cop${copies.length === 1 ? 'y' : 'ies'} on origin\n` + SEP + '\n';
|
|
100
|
+
for (const copy of copies)
|
|
101
|
+
out += ` ${copy.sha.slice(0, 8)} ${copy.ref}\n`;
|
|
102
|
+
out += `\nYour CI composes \`${cfg.devBranch}\` from origin/main plus every ref above, on every run.\n`
|
|
103
|
+
+ `Remove yours with: ${rules_config_1.WP_PUSH_DEV} --remove\n`;
|
|
104
|
+
process.stdout.write(out);
|
|
105
|
+
}
|
|
106
|
+
/** `--remove`: delete the remote copy. This IS the rollback — nothing else has to be undone. */
|
|
107
|
+
remove(repoRoot, cfg, branch, targetRef) {
|
|
108
|
+
if (this.refs.remoteSha(repoRoot, targetRef) === '') {
|
|
109
|
+
process.stdout.write(`\nNothing to remove — \`${targetRef}\` is not published.\n`
|
|
110
|
+
+ `List what IS published with: ${rules_config_1.WP_PUSH_DEV} --list\n`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.gitExec.runGitChecked(['-C', repoRoot, 'push', 'origin', '--delete', `refs/heads/${targetRef}`], `Failed to delete the dev copy ${targetRef}`);
|
|
114
|
+
process.stdout.write('\n' + SEP + `🗑️ Removed ${targetRef}\n` + SEP + '\n'
|
|
115
|
+
+ `\`${branch}\` will drop out of \`${cfg.devBranch}\` on its next composition run. Your branch and any\n`
|
|
116
|
+
+ 'PR on it are untouched.\n');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The ordinary path: publish HEAD as `<namespace>/<branch>`.
|
|
120
|
+
*
|
|
121
|
+
* THE CLOBBER GUARD is the whole of the interesting logic here. If the remote copy holds commits the
|
|
122
|
+
* local branch does not, somebody (quite possibly this developer, days ago) resolved a real conflict
|
|
123
|
+
* over there. A silent force-push destroys work that cost a human a genuine merge resolution, so the
|
|
124
|
+
* default is refusal with two named ways forward — never a "use --force" one-liner, which is how a
|
|
125
|
+
* warning becomes a reflex.
|
|
126
|
+
*/
|
|
127
|
+
publish(repoRoot, cfg, branch, targetRef, opts) {
|
|
128
|
+
const remoteSha = this.refs.remoteSha(repoRoot, targetRef);
|
|
129
|
+
if (remoteSha !== '') {
|
|
130
|
+
this.refs.fetchCopies(repoRoot, [targetRef]);
|
|
131
|
+
const ahead = this.refs.commitsOnlyOnRemote(repoRoot, targetRef);
|
|
132
|
+
if (ahead > 0 && opts.rebaseResolution) {
|
|
133
|
+
this.rebaseResolution(repoRoot, branch, targetRef);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (ahead > 0 && !opts.force)
|
|
137
|
+
this.refuseClobber(cfg, branch, targetRef, ahead);
|
|
138
|
+
}
|
|
139
|
+
this.gitExec.runGitChecked(['-C', repoRoot, 'push', ...this.pushSafety(targetRef, remoteSha, opts.force),
|
|
140
|
+
'origin', `HEAD:refs/heads/${targetRef}`], `Failed to publish the dev copy ${targetRef}`);
|
|
141
|
+
process.stdout.write('\n' + SEP + `✅ Published ${targetRef}\n` + SEP + '\n'
|
|
142
|
+
+ `\`${branch}\` is now in the pool your CI composes \`${cfg.devBranch}\` from — it will be picked up on\n`
|
|
143
|
+
+ 'the next composition run. No PR was opened and nothing landed on main.\n\n'
|
|
144
|
+
+ ` ${rules_config_1.WP_PUSH_DEV} --list ← what else is in the pool\n`
|
|
145
|
+
+ ` ${rules_config_1.WP_PUSH_DEV} --remove ← take this branch back out\n`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `--rebase-resolution`: keep the published resolution, replay it on top of the new commits.
|
|
149
|
+
*
|
|
150
|
+
* Implemented as a resolve whose BASE is the local HEAD and whose queue is the copy itself, so a
|
|
151
|
+
* replay that conflicts again lands in exactly the same halt/`wp-finish-push-dev` state machine as
|
|
152
|
+
* any other conflict, rather than inventing a second way to be stuck.
|
|
153
|
+
*/
|
|
154
|
+
rebaseResolution(repoRoot, branch, targetRef) {
|
|
155
|
+
const state = new push_dev_state_1.PushDevState(branch, this.runner.tmpBranchFor(branch), targetRef, [targetRef]);
|
|
156
|
+
process.stdout.write('\n' + SEP + `♻️ Replaying the published resolution onto \`${branch}\`\n` + SEP + '\n'
|
|
157
|
+
+ `Merging \`${targetRef}\` (which holds the resolution) into your new commits on a throwaway branch.\n`
|
|
158
|
+
+ `Your feature branch \`${branch}\` is not touched.\n\n`);
|
|
159
|
+
this.runner.start(repoRoot, state, 'HEAD', [targetRef]);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* `--resolve`: compose the other copies onto this one, so what is published is what CI will build.
|
|
163
|
+
*
|
|
164
|
+
* With no argument the queue is EVERY other copy, in the same sorted order CI composes them — the
|
|
165
|
+
* local resolution is then a rehearsal of the real composition, not a guess at it. With an argument
|
|
166
|
+
* (the branch a CI conflict message names) it is just that one.
|
|
167
|
+
*/
|
|
168
|
+
startResolve(repoRoot, cfg, branch, targetRef, only) {
|
|
169
|
+
if (this.refs.remoteSha(repoRoot, targetRef) === '') {
|
|
170
|
+
throw new rules_config_1.CliExitError(2, '\n' + SEP + `⛔ \`${targetRef}\` is not published yet\n` + SEP + '\n'
|
|
171
|
+
+ 'There is nothing to compose against. Publish your copy first, then resolve:\n'
|
|
172
|
+
+ ` ${rules_config_1.WP_PUSH_DEV}\n ${rules_config_1.WP_PUSH_DEV} --resolve\n`);
|
|
173
|
+
}
|
|
174
|
+
const queue = this.resolveQueue(repoRoot, cfg, targetRef, only);
|
|
175
|
+
if (queue.length === 0) {
|
|
176
|
+
process.stdout.write(`\nNothing to compose — \`${targetRef}\` is the only copy in \`${cfg.copyRefGlob()}\`.\n`
|
|
177
|
+
+ `Your CI will merge it onto origin/main by itself; there is no other branch to conflict with.\n`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const state = new push_dev_state_1.PushDevState(branch, this.runner.tmpBranchFor(branch), targetRef, queue);
|
|
181
|
+
process.stdout.write(this.runner.announce(cfg, state));
|
|
182
|
+
this.runner.start(repoRoot, state, `refs/remotes/origin/${targetRef}`, [targetRef, ...queue]);
|
|
183
|
+
}
|
|
184
|
+
// The refs to merge: one named copy, or every copy that is not mine. A named branch is accepted with
|
|
185
|
+
// or without the namespace prefix, because the CI message that names it may print either.
|
|
186
|
+
resolveQueue(repoRoot, cfg, targetRef, only) {
|
|
187
|
+
const copies = this.refs.liveCopies(repoRoot, cfg);
|
|
188
|
+
if (only === '') {
|
|
189
|
+
return copies.filter((c) => c.ref !== targetRef).map((c) => c.ref);
|
|
190
|
+
}
|
|
191
|
+
const wanted = only.startsWith(`${cfg.branchNamespace}/`) ? only : cfg.copyRefFor(only);
|
|
192
|
+
if (wanted === targetRef) {
|
|
193
|
+
throw new rules_config_1.CliExitError(2, `\n⛔ \`${only}\` is your own copy — there is nothing to compose it with.\n`);
|
|
194
|
+
}
|
|
195
|
+
if (!copies.some((c) => c.ref === wanted)) {
|
|
196
|
+
throw new rules_config_1.CliExitError(2, '\n' + SEP + `⛔ No published copy \`${wanted}\`\n` + SEP + '\n'
|
|
197
|
+
+ `Live copies (${rules_config_1.WP_PUSH_DEV} --list):\n`
|
|
198
|
+
+ copies.map((c) => ` ${c.ref}`).join('\n') + '\n');
|
|
199
|
+
}
|
|
200
|
+
return [wanted];
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* `--force-with-lease` pinned to the sha we just inspected, so the clobber guard's verdict cannot go
|
|
204
|
+
* stale between the check and the push. Plain `--force` only when the caller explicitly asked to
|
|
205
|
+
* discard, and no force at all for a brand-new copy.
|
|
206
|
+
*/
|
|
207
|
+
pushSafety(targetRef, remoteSha, force) {
|
|
208
|
+
if (remoteSha === '')
|
|
209
|
+
return [];
|
|
210
|
+
if (force)
|
|
211
|
+
return ['--force'];
|
|
212
|
+
return [`--force-with-lease=refs/heads/${targetRef}:${remoteSha}`];
|
|
213
|
+
}
|
|
214
|
+
refuseClobber(cfg, branch, targetRef, ahead) {
|
|
215
|
+
throw new rules_config_1.CliExitError(2, '\n' + SEP + `⛔ \`${targetRef}\` holds ${String(ahead)} commit(s) your branch does not\n` + SEP + '\n'
|
|
216
|
+
+ 'That is what a CONFLICT RESOLUTION looks like: somebody composed this copy against another\n'
|
|
217
|
+
+ `developer's branch so \`${cfg.devBranch}\` could build, and that work exists ONLY on the copy —\n`
|
|
218
|
+
+ 'by design, because it must never reach your feature branch or your PR.\n\n'
|
|
219
|
+
+ 'Publishing now would force-push over it and destroy it, so this command refuses. Pick one:\n\n'
|
|
220
|
+
+ ` ${rules_config_1.WP_PUSH_DEV} --rebase-resolution\n`
|
|
221
|
+
+ ' Replay that resolution on top of your new commits. Keeps the work; may ask you to\n'
|
|
222
|
+
+ ' re-resolve the parts your new commits actually changed.\n\n'
|
|
223
|
+
+ ` ${rules_config_1.WP_PUSH_DEV} --force\n`
|
|
224
|
+
+ ' Discard it and publish your branch as-is. The composition breaks again until someone\n'
|
|
225
|
+
+ ` re-resolves it with \`${rules_config_1.WP_PUSH_DEV} --resolve\`.\n\n`
|
|
226
|
+
+ `See what is on the copy: git log --oneline ${branch}..refs/remotes/origin/${targetRef}\n`);
|
|
227
|
+
}
|
|
228
|
+
// A half-finished resolve owns the checkout — it is parked on a throwaway branch, so "the current
|
|
229
|
+
// branch" is not the feature branch this command would otherwise act on.
|
|
230
|
+
assertNoResolveInProgress(repoRoot) {
|
|
231
|
+
const state = this.store.read(repoRoot);
|
|
232
|
+
if (state === null)
|
|
233
|
+
return;
|
|
234
|
+
throw new rules_config_1.CliExitError(2, '\n' + SEP + '⛔ A dev-deploy resolve is already in progress\n' + SEP + '\n'
|
|
235
|
+
+ `You are on the throwaway branch \`${state.tmpBranch}\`, mid-composition. Finish it or bail out:\n`
|
|
236
|
+
+ ` ${rules_config_1.WP_FINISH_PUSH_DEV} ← commit the resolution, resume the queue, publish\n`
|
|
237
|
+
+ ` ${rules_config_1.WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \`${state.originalBranch}\`\n`);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
exports.PushDevCommand = PushDevCommand;
|
|
241
|
+
exports.PushDevCommand = PushDevCommand = tslib_1.__decorate([
|
|
242
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
243
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
|
|
244
|
+
git_exec_1.GitExec,
|
|
245
|
+
dev_deploy_refs_1.DevDeployRefs,
|
|
246
|
+
push_dev_state_1.PushDevStateStore,
|
|
247
|
+
dev_resolve_runner_1.DevResolveRunner])
|
|
248
|
+
], PushDevCommand);
|
|
249
|
+
//# sourceMappingURL=push-dev-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-dev-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/push-dev-command.ts"],"names":[],"mappings":";;;;AAAA,0DAOiC;AACjC,yCAA2D;AAE3D,mDAA+C;AAC/C,iEAAqE;AACrE,+DAA6E;AAC7E,uEAAkE;AAElE,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;GAGG;AACH,MAAa,cAAc;IACvB,6CAA6C;IAC7C,MAAM,GAAG,KAAK,CAAC;IACf,sFAAsF;IACtF,IAAI,GAAG,KAAK,CAAC;IACb,qFAAqF;IACrF,KAAK,GAAG,KAAK,CAAC;IACd,uFAAuF;IACvF,gBAAgB,GAAG,KAAK,CAAC;IACzB,gGAAgG;IAChG,OAAO,GAAG,KAAK,CAAC;IAChB,sFAAsF;IACtF,aAAa,GAAG,EAAE,CAAC;CACtB;AAbD,wCAaC;AAED;;;;;;;;;;;;;;;;;GAiBG;AAEI,IAAM,cAAc,GAApB,MAAM,cAAc;IAEF;IACA;IACA;IACA;IACA;IALrB,YACqB,cAA8B,EAC9B,OAAgB,EAChB,IAAmB,EACnB,KAAwB,EACxB,MAAwB;QAJxB,mBAAc,GAAd,cAAc,CAAgB;QAC9B,YAAO,GAAP,OAAO,CAAS;QAChB,SAAI,GAAJ,IAAI,CAAe;QACnB,UAAK,GAAL,KAAK,CAAmB;QACxB,WAAM,GAAN,MAAM,CAAkB;IAC1C,CAAC;IAEJ,KAAK,CAAC,GAAG,CAAC,IAAoB;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,kGAAkG;QAClG,uDAAuD;QACvD,IAAA,4BAAa,EAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAC;QACrD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAEvC,+FAA+F;QAC/F,IAAI,CAAC,yBAAyB,CAAC,QAAQ,CAAC,CAAC;QAEzC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YACzB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC7B,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACtC,MAAM,SAAS,GAAG,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAEzC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;YAC9C,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC7B,CAAC;QACD,kGAAkG;QAClG,2CAA2C;QAC3C,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QAEvC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;YACxE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;QACrD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;IAED,+FAA+F;IACvF,IAAI,CAAC,QAAgB,EAAE,GAAoB;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACnD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,iDAAiD,GAAG,CAAC,WAAW,EAAE,iBAAiB;kBACjF,+BAA+B,0BAAW,IAAI,CAAC,CAAC;YACtD,OAAO;QACX,CAAC;QACD,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,cAAc,GAAG,GAAG,GAAG,IAAI,CAAC;QAC1H,KAAK,MAAM,IAAI,IAAI,MAAM;YAAE,GAAG,IAAI,KAAK,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;QAC7E,GAAG,IAAI,wBAAwB,GAAG,CAAC,SAAS,2DAA2D;cACjG,sBAAsB,0BAAW,aAAa,CAAC;QACrD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;IAED,gGAAgG;IACxF,MAAM,CAAC,QAAgB,EAAE,GAAoB,EAAE,MAAc,EAAE,SAAiB;QACpF,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,EAAE,EAAE,CAAC;YAClD,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,2BAA2B,SAAS,wBAAwB;kBAC1D,gCAAgC,0BAAW,WAAW,CAAC,CAAC;YAC9D,OAAO;QACX,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,SAAS,EAAE,CAAC,EAChG,iCAAiC,SAAS,EAAE,CAAC,CAAC;QAClD,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,IAAI,GAAG,GAAG,GAAG,gBAAgB,SAAS,IAAI,GAAG,GAAG,GAAG,IAAI;cACrD,KAAK,MAAM,yBAAyB,GAAG,CAAC,SAAS,uDAAuD;cACxG,2BAA2B,CAAC,CAAC;IACvC,CAAC;IAED;;;;;;;;OAQG;IACK,OAAO,CAAC,QAAgB,EAAE,GAAoB,EAAE,MAAc,EAAE,SAAiB,EAAE,IAAoB;QAC3G,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;QAC3D,IAAI,SAAS,KAAK,EAAE,EAAE,CAAC;YACnB,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;YAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;YACjE,IAAI,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBACrC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;gBACnD,OAAO;YACX,CAAC;YACD,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;gBAAE,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QACpF,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,aAAa,CACtB,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC;YACzE,QAAQ,EAAE,mBAAmB,SAAS,EAAE,CAAC,EAC7C,kCAAkC,SAAS,EAAE,CAAC,CAAC;QACnD,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,IAAI,GAAG,GAAG,GAAG,eAAe,SAAS,IAAI,GAAG,GAAG,GAAG,IAAI;cACpD,KAAK,MAAM,4CAA4C,GAAG,CAAC,SAAS,qCAAqC;cACzG,4EAA4E;cAC5E,KAAK,0BAAW,2CAA2C;cAC3D,KAAK,0BAAW,4CAA4C,CAAC,CAAC;IACxE,CAAC;IAED;;;;;;OAMG;IACK,gBAAgB,CAAC,QAAgB,EAAE,MAAc,EAAE,SAAiB;QACxE,MAAM,KAAK,GAAG,IAAI,6BAAY,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;QACjG,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,IAAI,GAAG,GAAG,GAAG,iDAAiD,MAAM,MAAM,GAAG,GAAG,GAAG,IAAI;cACrF,aAAa,SAAS,gFAAgF;cACtG,yBAAyB,MAAM,wBAAwB,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED;;;;;;OAMG;IACK,YAAY,CAAC,QAAgB,EAAE,GAAoB,EAAE,MAAc,EAAE,SAAiB,EAAE,IAAY;QACxG,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,EAAE,EAAE,CAAC;YAClD,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,OAAO,SAAS,2BAA2B,GAAG,GAAG,GAAG,IAAI;kBACnE,+EAA+E;kBAC/E,KAAK,0BAAW,OAAO,0BAAW,cAAc,CAAC,CAAC;QAC5D,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;QAChE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,4BAA4B,SAAS,4BAA4B,GAAG,CAAC,WAAW,EAAE,OAAO;kBACvF,gGAAgG,CAAC,CAAC;YACxG,OAAO;QACX,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,6BAAY,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QAC3F,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,uBAAuB,SAAS,EAAE,EAAE,CAAC,SAAS,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;IAClG,CAAC;IAED,qGAAqG;IACrG,0FAA0F;IAClF,YAAY,CAAC,QAAgB,EAAE,GAAoB,EAAE,SAAiB,EAAE,IAAY;QACxF,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACnD,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;YACd,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAU,EAAW,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAU,EAAU,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC1G,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACxF,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,2BAAY,CAAC,CAAC,EAAE,SAAS,IAAI,8DAA8D,CAAC,CAAC;QAC3G,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAU,EAAW,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,MAAM,CAAC,EAAE,CAAC;YAC1D,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,yBAAyB,MAAM,MAAM,GAAG,GAAG,GAAG,IAAI;kBAC7D,gBAAgB,0BAAW,aAAa;kBACxC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAU,EAAU,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;IAED;;;;OAIG;IACK,UAAU,CAAC,SAAiB,EAAE,SAAiB,EAAE,KAAc;QACnE,IAAI,SAAS,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAChC,IAAI,KAAK;YAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAC9B,OAAO,CAAC,iCAAiC,SAAS,IAAI,SAAS,EAAE,CAAC,CAAC;IACvE,CAAC;IAEO,aAAa,CAAC,GAAoB,EAAE,MAAc,EAAE,SAAiB,EAAE,KAAa;QACxF,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,OAAO,SAAS,YAAY,MAAM,CAAC,KAAK,CAAC,mCAAmC,GAAG,GAAG,GAAG,IAAI;cACpG,8FAA8F;cAC9F,2BAA2B,GAAG,CAAC,SAAS,2DAA2D;cACnG,4EAA4E;cAC5E,gGAAgG;cAChG,KAAK,0BAAW,wBAAwB;cACxC,2FAA2F;cAC3F,mEAAmE;cACnE,KAAK,0BAAW,YAAY;cAC5B,8FAA8F;cAC9F,+BAA+B,0BAAW,mBAAmB;cAC7D,+CAA+C,MAAM,yBAAyB,SAAS,IAAI,CAAC,CAAC;IACvG,CAAC;IAED,kGAAkG;IAClG,yEAAyE;IACjE,yBAAyB,CAAC,QAAgB;QAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO;QAC3B,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,iDAAiD,GAAG,GAAG,GAAG,IAAI;cACzE,qCAAqC,KAAK,CAAC,SAAS,+CAA+C;cACnG,KAAK,iCAAkB,kEAAkE;cACzF,KAAK,iCAAkB,gDAAgD,KAAK,CAAC,cAAc,MAAM,CAAC,CAAC;IAC7G,CAAC;CACJ,CAAA;AAhNY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,6BAAc;QACrB,kBAAO;QACV,+BAAa;QACZ,kCAAiB;QAChB,qCAAgB;GANpC,cAAc,CAgN1B","sourcesContent":["import {\n CliExitError,\n DevDeployConfig,\n RepoRootFinder,\n WP_FINISH_PUSH_DEV,\n WP_PUSH_DEV,\n writeTemplate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from '../workflow/git-exec';\nimport { DevCopy, DevDeployRefs } from '../workflow/dev-deploy-refs';\nimport { PushDevState, PushDevStateStore } from '../workflow/push-dev-state';\nimport { DevResolveRunner } from '../workflow/dev-resolve-runner';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * Which of `wp-push-dev`'s modes was asked for. Data-only (per CLAUDE.md) with plain assigned fields\n * rather than a seven-argument constructor — the bin sets what argv carried and nothing else.\n */\nexport class PushDevOptions {\n /** Delete the remote copy — the rollback. */\n remove = false;\n /** List the live copies and stop. The one mode that needs no branch preconditions. */\n list = false;\n /** Discard a conflict resolution already published on the copy, and overwrite it. */\n force = false;\n /** Replay that resolution on top of the new local commits instead of discarding it. */\n rebaseResolution = false;\n /** Compose the other copies onto this one so the shared environment can build them together. */\n resolve = false;\n /** With `--resolve`: merge ONLY this developer's branch. Empty ⇒ every other copy. */\n resolveTarget = '';\n}\n\n/**\n * `wp-push-dev` — publish a DISPOSABLE copy of this branch so a shared dev environment can build it,\n * without landing anything on `main` and without opening a PR.\n *\n * WHY THIS COMMAND HAS TO EXIST AT ALL: `pr-creation-or-push-guard` blocks every `git push`, no\n * exceptions, by design. Its own fix-hint offers \"ask a human to run the push\" for out-of-band cases —\n * which is the right answer for a genuine exception and the wrong one for a routine daily action. Without\n * a gated command there is simply no legal way for an AI to get a branch onto a dev server, so the\n * feature was undoable rather than merely awkward.\n *\n * WHY THERE IS NO BUILD, deliberately unlike `wp-start-upsert-pr`: the shared environment's CI builds the\n * COMPOSED tree (main + every listed branch). A local build here proves the branch compiles against\n * `main` alone, which is not the question — it would be both slower and reassuring about the wrong thing.\n * The gate that matters runs on the composition, in CI, where it belongs.\n *\n * WHAT IT NEVER DOES: touch the feature branch. See DevDeployRefs for the invariant and why the copy\n * exists to hold conflict resolutions that must not reach the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PushDevCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly gitExec: GitExec,\n private readonly refs: DevDeployRefs,\n private readonly store: PushDevStateStore,\n private readonly runner: DevResolveRunner,\n ) {}\n\n async run(opts: PushDevOptions): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it is present + current for any failure message to cite —\n // same reason every other wp-* command opens this way.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n const cfg = this.refs.config(repoRoot);\n\n // A resolve already in flight owns the checkout; starting a second one would strand the first.\n this.assertNoResolveInProgress(repoRoot);\n\n if (opts.list) {\n this.list(repoRoot, cfg);\n return Promise.resolve();\n }\n\n const branch = this.refs.currentBranch(repoRoot);\n this.refs.assertPushable(cfg, branch);\n const targetRef = cfg.copyRefFor(branch);\n\n if (opts.remove) {\n this.remove(repoRoot, cfg, branch, targetRef);\n return Promise.resolve();\n }\n // Every remaining mode commits or merges, so the tree must be the AI's committed work and nothing\n // else. The tooling never commits for you.\n this.gitExec.assertCleanTree(repoRoot);\n\n if (opts.resolve) {\n this.startResolve(repoRoot, cfg, branch, targetRef, opts.resolveTarget);\n return Promise.resolve();\n }\n this.publish(repoRoot, cfg, branch, targetRef, opts);\n return Promise.resolve();\n }\n\n /** `--list`: exactly the live copies, straight from the remote — never a cached local view. */\n private list(repoRoot: string, cfg: DevDeployConfig): void {\n const copies = this.refs.liveCopies(repoRoot, cfg);\n if (copies.length === 0) {\n process.stdout.write(\n `\\nNo dev copies published — nothing matches \\`${cfg.copyRefGlob()}\\` on origin.\\n`\n + `Publish this branch's with: ${WP_PUSH_DEV}\\n`);\n return;\n }\n let out = '\\n' + SEP + `📋 ${String(copies.length)} dev cop${copies.length === 1 ? 'y' : 'ies'} on origin\\n` + SEP + '\\n';\n for (const copy of copies) out += ` ${copy.sha.slice(0, 8)} ${copy.ref}\\n`;\n out += `\\nYour CI composes \\`${cfg.devBranch}\\` from origin/main plus every ref above, on every run.\\n`\n + `Remove yours with: ${WP_PUSH_DEV} --remove\\n`;\n process.stdout.write(out);\n }\n\n /** `--remove`: delete the remote copy. This IS the rollback — nothing else has to be undone. */\n private remove(repoRoot: string, cfg: DevDeployConfig, branch: string, targetRef: string): void {\n if (this.refs.remoteSha(repoRoot, targetRef) === '') {\n process.stdout.write(\n `\\nNothing to remove — \\`${targetRef}\\` is not published.\\n`\n + `List what IS published with: ${WP_PUSH_DEV} --list\\n`);\n return;\n }\n this.gitExec.runGitChecked(['-C', repoRoot, 'push', 'origin', '--delete', `refs/heads/${targetRef}`],\n `Failed to delete the dev copy ${targetRef}`);\n process.stdout.write(\n '\\n' + SEP + `🗑️ Removed ${targetRef}\\n` + SEP + '\\n'\n + `\\`${branch}\\` will drop out of \\`${cfg.devBranch}\\` on its next composition run. Your branch and any\\n`\n + 'PR on it are untouched.\\n');\n }\n\n /**\n * The ordinary path: publish HEAD as `<namespace>/<branch>`.\n *\n * THE CLOBBER GUARD is the whole of the interesting logic here. If the remote copy holds commits the\n * local branch does not, somebody (quite possibly this developer, days ago) resolved a real conflict\n * over there. A silent force-push destroys work that cost a human a genuine merge resolution, so the\n * default is refusal with two named ways forward — never a \"use --force\" one-liner, which is how a\n * warning becomes a reflex.\n */\n private publish(repoRoot: string, cfg: DevDeployConfig, branch: string, targetRef: string, opts: PushDevOptions): void {\n const remoteSha = this.refs.remoteSha(repoRoot, targetRef);\n if (remoteSha !== '') {\n this.refs.fetchCopies(repoRoot, [targetRef]);\n const ahead = this.refs.commitsOnlyOnRemote(repoRoot, targetRef);\n if (ahead > 0 && opts.rebaseResolution) {\n this.rebaseResolution(repoRoot, branch, targetRef);\n return;\n }\n if (ahead > 0 && !opts.force) this.refuseClobber(cfg, branch, targetRef, ahead);\n }\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'push', ...this.pushSafety(targetRef, remoteSha, opts.force),\n 'origin', `HEAD:refs/heads/${targetRef}`],\n `Failed to publish the dev copy ${targetRef}`);\n process.stdout.write(\n '\\n' + SEP + `✅ Published ${targetRef}\\n` + SEP + '\\n'\n + `\\`${branch}\\` is now in the pool your CI composes \\`${cfg.devBranch}\\` from — it will be picked up on\\n`\n + 'the next composition run. No PR was opened and nothing landed on main.\\n\\n'\n + ` ${WP_PUSH_DEV} --list ← what else is in the pool\\n`\n + ` ${WP_PUSH_DEV} --remove ← take this branch back out\\n`);\n }\n\n /**\n * `--rebase-resolution`: keep the published resolution, replay it on top of the new commits.\n *\n * Implemented as a resolve whose BASE is the local HEAD and whose queue is the copy itself, so a\n * replay that conflicts again lands in exactly the same halt/`wp-finish-push-dev` state machine as\n * any other conflict, rather than inventing a second way to be stuck.\n */\n private rebaseResolution(repoRoot: string, branch: string, targetRef: string): void {\n const state = new PushDevState(branch, this.runner.tmpBranchFor(branch), targetRef, [targetRef]);\n process.stdout.write(\n '\\n' + SEP + `♻️ Replaying the published resolution onto \\`${branch}\\`\\n` + SEP + '\\n'\n + `Merging \\`${targetRef}\\` (which holds the resolution) into your new commits on a throwaway branch.\\n`\n + `Your feature branch \\`${branch}\\` is not touched.\\n\\n`);\n this.runner.start(repoRoot, state, 'HEAD', [targetRef]);\n }\n\n /**\n * `--resolve`: compose the other copies onto this one, so what is published is what CI will build.\n *\n * With no argument the queue is EVERY other copy, in the same sorted order CI composes them — the\n * local resolution is then a rehearsal of the real composition, not a guess at it. With an argument\n * (the branch a CI conflict message names) it is just that one.\n */\n private startResolve(repoRoot: string, cfg: DevDeployConfig, branch: string, targetRef: string, only: string): void {\n if (this.refs.remoteSha(repoRoot, targetRef) === '') {\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ \\`${targetRef}\\` is not published yet\\n` + SEP + '\\n'\n + 'There is nothing to compose against. Publish your copy first, then resolve:\\n'\n + ` ${WP_PUSH_DEV}\\n ${WP_PUSH_DEV} --resolve\\n`);\n }\n const queue = this.resolveQueue(repoRoot, cfg, targetRef, only);\n if (queue.length === 0) {\n process.stdout.write(\n `\\nNothing to compose — \\`${targetRef}\\` is the only copy in \\`${cfg.copyRefGlob()}\\`.\\n`\n + `Your CI will merge it onto origin/main by itself; there is no other branch to conflict with.\\n`);\n return;\n }\n const state = new PushDevState(branch, this.runner.tmpBranchFor(branch), targetRef, queue);\n process.stdout.write(this.runner.announce(cfg, state));\n this.runner.start(repoRoot, state, `refs/remotes/origin/${targetRef}`, [targetRef, ...queue]);\n }\n\n // The refs to merge: one named copy, or every copy that is not mine. A named branch is accepted with\n // or without the namespace prefix, because the CI message that names it may print either.\n private resolveQueue(repoRoot: string, cfg: DevDeployConfig, targetRef: string, only: string): string[] {\n const copies = this.refs.liveCopies(repoRoot, cfg);\n if (only === '') {\n return copies.filter((c: DevCopy): boolean => c.ref !== targetRef).map((c: DevCopy): string => c.ref);\n }\n const wanted = only.startsWith(`${cfg.branchNamespace}/`) ? only : cfg.copyRefFor(only);\n if (wanted === targetRef) {\n throw new CliExitError(2, `\\n⛔ \\`${only}\\` is your own copy — there is nothing to compose it with.\\n`);\n }\n if (!copies.some((c: DevCopy): boolean => c.ref === wanted)) {\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ No published copy \\`${wanted}\\`\\n` + SEP + '\\n'\n + `Live copies (${WP_PUSH_DEV} --list):\\n`\n + copies.map((c: DevCopy): string => ` ${c.ref}`).join('\\n') + '\\n');\n }\n return [wanted];\n }\n\n /**\n * `--force-with-lease` pinned to the sha we just inspected, so the clobber guard's verdict cannot go\n * stale between the check and the push. Plain `--force` only when the caller explicitly asked to\n * discard, and no force at all for a brand-new copy.\n */\n private pushSafety(targetRef: string, remoteSha: string, force: boolean): string[] {\n if (remoteSha === '') return [];\n if (force) return ['--force'];\n return [`--force-with-lease=refs/heads/${targetRef}:${remoteSha}`];\n }\n\n private refuseClobber(cfg: DevDeployConfig, branch: string, targetRef: string, ahead: number): never {\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ \\`${targetRef}\\` holds ${String(ahead)} commit(s) your branch does not\\n` + SEP + '\\n'\n + 'That is what a CONFLICT RESOLUTION looks like: somebody composed this copy against another\\n'\n + `developer's branch so \\`${cfg.devBranch}\\` could build, and that work exists ONLY on the copy —\\n`\n + 'by design, because it must never reach your feature branch or your PR.\\n\\n'\n + 'Publishing now would force-push over it and destroy it, so this command refuses. Pick one:\\n\\n'\n + ` ${WP_PUSH_DEV} --rebase-resolution\\n`\n + ' Replay that resolution on top of your new commits. Keeps the work; may ask you to\\n'\n + ' re-resolve the parts your new commits actually changed.\\n\\n'\n + ` ${WP_PUSH_DEV} --force\\n`\n + ' Discard it and publish your branch as-is. The composition breaks again until someone\\n'\n + ` re-resolves it with \\`${WP_PUSH_DEV} --resolve\\`.\\n\\n`\n + `See what is on the copy: git log --oneline ${branch}..refs/remotes/origin/${targetRef}\\n`);\n }\n\n // A half-finished resolve owns the checkout — it is parked on a throwaway branch, so \"the current\n // branch\" is not the feature branch this command would otherwise act on.\n private assertNoResolveInProgress(repoRoot: string): void {\n const state = this.store.read(repoRoot);\n if (state === null) return;\n throw new CliExitError(2,\n '\\n' + SEP + '⛔ A dev-deploy resolve is already in progress\\n' + SEP + '\\n'\n + `You are on the throwaway branch \\`${state.tmpBranch}\\`, mid-composition. Finish it or bail out:\\n`\n + ` ${WP_FINISH_PUSH_DEV} ← commit the resolution, resume the queue, publish\\n`\n + ` ${WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \\`${state.originalBranch}\\`\\n`);\n }\n}\n"]}
|
|
@@ -7,6 +7,10 @@ import { LandPrCommand } from './commands/land-pr-command';
|
|
|
7
7
|
import { CheckPrCommand } from './commands/check-pr-command';
|
|
8
8
|
import { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';
|
|
9
9
|
import { ReapWorktreeCommand } from './commands/reap-worktree-command';
|
|
10
|
+
import { PushDevCommand, PushDevOptions } from './commands/push-dev-command';
|
|
11
|
+
import { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';
|
|
12
|
+
import { PushDevStateStore } from './workflow/push-dev-state';
|
|
13
|
+
import { RepoRootFinder } from '@webpieces/rules-config';
|
|
10
14
|
/**
|
|
11
15
|
* The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command
|
|
12
16
|
* classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the
|
|
@@ -23,7 +27,24 @@ export declare class PrGateApp {
|
|
|
23
27
|
private readonly checkPrCommand;
|
|
24
28
|
private readonly reviewUpsertPrCommand;
|
|
25
29
|
private readonly reapWorktreeCommand;
|
|
26
|
-
|
|
30
|
+
private readonly pushDevCommand;
|
|
31
|
+
private readonly finishPushDevCommand;
|
|
32
|
+
private readonly pushDevStateStore;
|
|
33
|
+
private readonly repoRootFinder;
|
|
34
|
+
constructor(startUpdateCommand: StartUpdateCommand, finishUpdateCommand: FinishUpdateCommand, startUpsertPrCommand: StartUpsertPrCommand, finishUpsertPrCommand: FinishUpsertPrCommand, cleanupCommand: CleanupCommand, landPrCommand: LandPrCommand, checkPrCommand: CheckPrCommand, reviewUpsertPrCommand: ReviewUpsertPrCommand, reapWorktreeCommand: ReapWorktreeCommand, pushDevCommand: PushDevCommand, finishPushDevCommand: FinishPushDevCommand, pushDevStateStore: PushDevStateStore, repoRootFinder: RepoRootFinder);
|
|
35
|
+
/**
|
|
36
|
+
* Refuse `command` while a `wp-push-dev --resolve` is half-finished.
|
|
37
|
+
*
|
|
38
|
+
* Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve
|
|
39
|
+
* parks the checkout on a throwaway branch, so every command below would act on a branch that is not
|
|
40
|
+
* the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the
|
|
41
|
+
* tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.
|
|
42
|
+
*/
|
|
43
|
+
private assertNoResolveInProgress;
|
|
44
|
+
/** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */
|
|
45
|
+
pushDev(opts?: PushDevOptions): Promise<void>;
|
|
46
|
+
/** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */
|
|
47
|
+
finishPushDev(opts?: FinishPushDevOptions): Promise<void>;
|
|
27
48
|
/**
|
|
28
49
|
* INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.
|
|
29
50
|
* `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be
|