@webpieces/pr-gate 0.4.565 → 0.4.567
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 +91 -0
- package/src/scripts/commands/push-dev-command.js +256 -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 +74 -0
- package/src/scripts/workflow/dev-deploy-refs.js +153 -0
- package/src/scripts/workflow/dev-deploy-refs.js.map +1 -0
- package/src/scripts/workflow/dev-deploy-watch-hints.d.ts +35 -0
- package/src/scripts/workflow/dev-deploy-watch-hints.js +76 -0
- package/src/scripts/workflow/dev-deploy-watch-hints.js.map +1 -0
- package/src/scripts/workflow/dev-resolve-runner.d.ts +74 -0
- package/src/scripts/workflow/dev-resolve-runner.js +188 -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
|
@@ -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
|
-
|
|
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;
|
|
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,74 @@
|
|
|
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
|
+
/**
|
|
56
|
+
* The sha of a local ref (default HEAD).
|
|
57
|
+
*
|
|
58
|
+
* Read AFTER a push so the watch hints quote the sha that is provably published. Quoting the copy
|
|
59
|
+
* REF instead would be a moving target — it advances the next time anybody publishes — and the whole
|
|
60
|
+
* value of the "did my code land in dev" check is that it is a claim about one specific commit.
|
|
61
|
+
*/
|
|
62
|
+
headSha(repoRoot: string, ref?: string): string;
|
|
63
|
+
/** The remote sha of one copy, or '' when that copy does not exist yet. */
|
|
64
|
+
remoteSha(repoRoot: string, ref: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Fetch the named copy refs into `refs/remotes/origin/<ref>` so the local merges below run against
|
|
67
|
+
* fresh state. A no-op for an empty list, and a hard failure otherwise: a resolve queued against
|
|
68
|
+
* stale refs rehearses a composition CI will not build.
|
|
69
|
+
*/
|
|
70
|
+
fetchCopies(repoRoot: string, refs: string[]): void;
|
|
71
|
+
/** How many commits `origin/<ref>` has that `HEAD` does not — the clobber signal. 0 = safe to publish. */
|
|
72
|
+
commitsOnlyOnRemote(repoRoot: string, ref: string): number;
|
|
73
|
+
private refusal;
|
|
74
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
/**
|
|
110
|
+
* The sha of a local ref (default HEAD).
|
|
111
|
+
*
|
|
112
|
+
* Read AFTER a push so the watch hints quote the sha that is provably published. Quoting the copy
|
|
113
|
+
* REF instead would be a moving target — it advances the next time anybody publishes — and the whole
|
|
114
|
+
* value of the "did my code land in dev" check is that it is a claim about one specific commit.
|
|
115
|
+
*/
|
|
116
|
+
headSha(repoRoot, ref = 'HEAD') {
|
|
117
|
+
return this.gitExec.gitQuery(['-C', repoRoot, 'rev-parse', ref], repoRoot, `Failed to read the sha of ${ref} (git rev-parse).`);
|
|
118
|
+
}
|
|
119
|
+
/** The remote sha of one copy, or '' when that copy does not exist yet. */
|
|
120
|
+
remoteSha(repoRoot, ref) {
|
|
121
|
+
const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', ref], repoRoot, `Failed to look up origin/${ref} (git ls-remote).`);
|
|
122
|
+
const line = out.split('\n').find((l) => l.trim() !== '');
|
|
123
|
+
if (line === undefined)
|
|
124
|
+
return '';
|
|
125
|
+
return line.split(/\s+/)[0] ?? '';
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Fetch the named copy refs into `refs/remotes/origin/<ref>` so the local merges below run against
|
|
129
|
+
* fresh state. A no-op for an empty list, and a hard failure otherwise: a resolve queued against
|
|
130
|
+
* stale refs rehearses a composition CI will not build.
|
|
131
|
+
*/
|
|
132
|
+
fetchCopies(repoRoot, refs) {
|
|
133
|
+
if (refs.length === 0)
|
|
134
|
+
return;
|
|
135
|
+
const specs = refs.map((r) => `+refs/heads/${r}:refs/remotes/origin/${r}`);
|
|
136
|
+
this.gitExec.runGitChecked(['-C', repoRoot, 'fetch', 'origin', ...specs], 'Failed to fetch the dev copy refs from origin');
|
|
137
|
+
}
|
|
138
|
+
/** How many commits `origin/<ref>` has that `HEAD` does not — the clobber signal. 0 = safe to publish. */
|
|
139
|
+
commitsOnlyOnRemote(repoRoot, ref) {
|
|
140
|
+
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).`);
|
|
141
|
+
const count = Number(out);
|
|
142
|
+
return Number.isInteger(count) ? count : 0;
|
|
143
|
+
}
|
|
144
|
+
refusal(headline, body) {
|
|
145
|
+
return '\n' + SEP + `⛔ ${headline}\n` + SEP + '\n' + body + `\nRun \`${rules_config_1.WP_PUSH_DEV} --help\` for the full command.\n`;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
exports.DevDeployRefs = DevDeployRefs;
|
|
149
|
+
exports.DevDeployRefs = DevDeployRefs = tslib_1.__decorate([
|
|
150
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
151
|
+
tslib_1.__metadata("design:paramtypes", [git_exec_1.GitExec])
|
|
152
|
+
], DevDeployRefs);
|
|
153
|
+
//# 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 /**\n * The sha of a local ref (default HEAD).\n *\n * Read AFTER a push so the watch hints quote the sha that is provably published. Quoting the copy\n * REF instead would be a moving target — it advances the next time anybody publishes — and the whole\n * value of the \"did my code land in dev\" check is that it is a claim about one specific commit.\n */\n headSha(repoRoot: string, ref = 'HEAD'): string {\n return this.gitExec.gitQuery(['-C', repoRoot, 'rev-parse', ref], repoRoot,\n `Failed to read the sha of ${ref} (git rev-parse).`);\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,35 @@
|
|
|
1
|
+
import { DevDeployConfig } from '@webpieces/rules-config';
|
|
2
|
+
/**
|
|
3
|
+
* The "now go watch it deploy" block printed after a copy is published.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS TEXT AND NOT A FEATURE: webpieces cannot know the consumer's CD. The composed branch might
|
|
6
|
+
* be built by GitHub Actions, by Jenkins, by a cron job on a box — so polling for "the run" would mean
|
|
7
|
+
* either a config key naming their workflow, or a heuristic that is wrong for somebody. Handing the AI
|
|
8
|
+
* the exact commands costs nothing, degrades gracefully (an empty `gh run list` is itself an answer), and
|
|
9
|
+
* keeps webpieces out of the business of modelling someone else's pipeline.
|
|
10
|
+
*
|
|
11
|
+
* The commands are pre-substituted with the real sha and refs — an AI given a template with blanks to
|
|
12
|
+
* fill in gets one of them wrong eventually, and a wrong `git merge-base` argument answers the question
|
|
13
|
+
* confidently and incorrectly.
|
|
14
|
+
*
|
|
15
|
+
* All of them are READ-ONLY and none is blocked: `git fetch`/`merge-base` are queries, and
|
|
16
|
+
* `gh run list|view|watch` is on the guards' allowed list.
|
|
17
|
+
*/
|
|
18
|
+
export declare class DevDeployWatchHints {
|
|
19
|
+
/**
|
|
20
|
+
* @param sha the commit that was just published — the ONLY thing that makes step 1 a proof rather
|
|
21
|
+
* than a guess, since `<copyRef>` itself moves the next time anyone publishes.
|
|
22
|
+
*/
|
|
23
|
+
render(cfg: DevDeployConfig, copyRef: string, sha: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Step 1 — pure git, works with ANY CI, and is the only claim provably about YOUR code.
|
|
26
|
+
*
|
|
27
|
+
* `<devBranch>` is recomposed from `origin/main` plus every copy on each CI run, so your published
|
|
28
|
+
* sha is an ancestor of it if and only if the composition included you.
|
|
29
|
+
*/
|
|
30
|
+
private landedSection;
|
|
31
|
+
/** Step 2 — GitHub Actions, if that is where this repo's dev deploy lives. Empty output is an answer. */
|
|
32
|
+
private runSection;
|
|
33
|
+
/** The trap worth one sentence: a green run on a SHARED branch is not a statement about your code. */
|
|
34
|
+
private orderingWarning;
|
|
35
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DevDeployWatchHints = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
7
|
+
/**
|
|
8
|
+
* The "now go watch it deploy" block printed after a copy is published.
|
|
9
|
+
*
|
|
10
|
+
* WHY THIS IS TEXT AND NOT A FEATURE: webpieces cannot know the consumer's CD. The composed branch might
|
|
11
|
+
* be built by GitHub Actions, by Jenkins, by a cron job on a box — so polling for "the run" would mean
|
|
12
|
+
* either a config key naming their workflow, or a heuristic that is wrong for somebody. Handing the AI
|
|
13
|
+
* the exact commands costs nothing, degrades gracefully (an empty `gh run list` is itself an answer), and
|
|
14
|
+
* keeps webpieces out of the business of modelling someone else's pipeline.
|
|
15
|
+
*
|
|
16
|
+
* The commands are pre-substituted with the real sha and refs — an AI given a template with blanks to
|
|
17
|
+
* fill in gets one of them wrong eventually, and a wrong `git merge-base` argument answers the question
|
|
18
|
+
* confidently and incorrectly.
|
|
19
|
+
*
|
|
20
|
+
* All of them are READ-ONLY and none is blocked: `git fetch`/`merge-base` are queries, and
|
|
21
|
+
* `gh run list|view|watch` is on the guards' allowed list.
|
|
22
|
+
*/
|
|
23
|
+
let DevDeployWatchHints = class DevDeployWatchHints {
|
|
24
|
+
/**
|
|
25
|
+
* @param sha the commit that was just published — the ONLY thing that makes step 1 a proof rather
|
|
26
|
+
* than a guess, since `<copyRef>` itself moves the next time anyone publishes.
|
|
27
|
+
*/
|
|
28
|
+
render(cfg, copyRef, sha) {
|
|
29
|
+
return '\n' + SEP + '👀 Is it on the dev server yet? — two questions, in this order\n' + SEP + '\n'
|
|
30
|
+
+ this.landedSection(cfg, sha)
|
|
31
|
+
+ '\n'
|
|
32
|
+
+ this.runSection(cfg, copyRef)
|
|
33
|
+
+ '\n'
|
|
34
|
+
+ this.orderingWarning(cfg);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Step 1 — pure git, works with ANY CI, and is the only claim provably about YOUR code.
|
|
38
|
+
*
|
|
39
|
+
* `<devBranch>` is recomposed from `origin/main` plus every copy on each CI run, so your published
|
|
40
|
+
* sha is an ancestor of it if and only if the composition included you.
|
|
41
|
+
*/
|
|
42
|
+
landedSection(cfg, sha) {
|
|
43
|
+
return `1. Did YOUR code land in \`${cfg.devBranch}\`? (pure git — works whatever your CI is)\n`
|
|
44
|
+
+ ` git fetch origin ${cfg.devBranch}\n`
|
|
45
|
+
+ ` git merge-base --is-ancestor ${sha} origin/${cfg.devBranch} && echo IN || echo "not yet"\n`
|
|
46
|
+
+ ` \`${cfg.devBranch}\` is rebuilt from origin/main + every \`${cfg.copyRefGlob()}\` ref on each run, so\n`
|
|
47
|
+
+ ' ancestry IS inclusion. Re-run after the next composition if it says "not yet".\n';
|
|
48
|
+
}
|
|
49
|
+
/** Step 2 — GitHub Actions, if that is where this repo's dev deploy lives. Empty output is an answer. */
|
|
50
|
+
runSection(cfg, copyRef) {
|
|
51
|
+
const rows = [
|
|
52
|
+
[`gh run list --branch ${copyRef} --limit 5`, 'the run your push triggered, if any'],
|
|
53
|
+
[`gh run list --branch ${cfg.devBranch} --limit 5`, 'the composition / deploy run'],
|
|
54
|
+
['gh run watch <run-id>', 'follow one to completion'],
|
|
55
|
+
['gh run view <run-id> --log-failed', 'read why it failed'],
|
|
56
|
+
];
|
|
57
|
+
// Aligned off the longest command, because the ref names are consumer-configurable and any
|
|
58
|
+
// hand-counted padding would go crooked the moment somebody renames the namespace.
|
|
59
|
+
const width = Math.max(...rows.map((row) => row[0].length));
|
|
60
|
+
return '2. Is the CI/CD run finished? (GitHub Actions — all read-only, none of it is blocked)\n'
|
|
61
|
+
+ rows.map((row) => ` ${row[0].padEnd(width)} ← ${row[1]}\n`).join('')
|
|
62
|
+
+ ' Empty output is not a bug: it means this repo deploys from somewhere other than Actions.\n'
|
|
63
|
+
+ ' Ask the human where the dev deploy runs, and do not guess.\n';
|
|
64
|
+
}
|
|
65
|
+
/** The trap worth one sentence: a green run on a SHARED branch is not a statement about your code. */
|
|
66
|
+
orderingWarning(cfg) {
|
|
67
|
+
return `A green run on \`${cfg.devBranch}\` is NOT proof your change deployed — that branch carries everyone's\n`
|
|
68
|
+
+ 'copies, and the composition may have excluded yours. Question 1 is the one that answers for YOUR\n'
|
|
69
|
+
+ 'code; question 2 only tells you whether the machinery finished.\n';
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
exports.DevDeployWatchHints = DevDeployWatchHints;
|
|
73
|
+
exports.DevDeployWatchHints = DevDeployWatchHints = tslib_1.__decorate([
|
|
74
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
|
|
75
|
+
], DevDeployWatchHints);
|
|
76
|
+
//# sourceMappingURL=dev-deploy-watch-hints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-deploy-watch-hints.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/dev-deploy-watch-hints.ts"],"names":[],"mappings":";;;;AACA,yCAA2D;AAE3D,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;;;;;;;;;;;;GAeG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC5B;;;OAGG;IACH,MAAM,CAAC,GAAoB,EAAE,OAAe,EAAE,GAAW;QACrD,OAAO,IAAI,GAAG,GAAG,GAAG,kEAAkE,GAAG,GAAG,GAAG,IAAI;cAC7F,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC;cAC5B,IAAI;cACJ,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;cAC7B,IAAI;cACJ,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACK,aAAa,CAAC,GAAoB,EAAE,GAAW;QACnD,OAAO,8BAA8B,GAAG,CAAC,SAAS,+CAA+C;cAC3F,yBAAyB,GAAG,CAAC,SAAS,IAAI;cAC1C,qCAAqC,GAAG,WAAW,GAAG,CAAC,SAAS,iCAAiC;cACjG,QAAQ,GAAG,CAAC,SAAS,4CAA4C,GAAG,CAAC,WAAW,EAAE,0BAA0B;cAC5G,qFAAqF,CAAC;IAChG,CAAC;IAED,yGAAyG;IACjG,UAAU,CAAC,GAAoB,EAAE,OAAe;QACpD,MAAM,IAAI,GAAG;YACT,CAAC,wBAAwB,OAAO,YAAY,EAAE,qCAAqC,CAAC;YACpF,CAAC,wBAAwB,GAAG,CAAC,SAAS,YAAY,EAAE,8BAA8B,CAAC;YACnF,CAAC,uBAAuB,EAAE,0BAA0B,CAAC;YACrD,CAAC,mCAAmC,EAAE,oBAAoB,CAAC;SAC9D,CAAC;QACF,2FAA2F;QAC3F,mFAAmF;QACnF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAa,EAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QAC9E,OAAO,0FAA0F;cAC3F,IAAI,CAAC,GAAG,CAAC,CAAC,GAAa,EAAU,EAAE,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;cAC5F,+FAA+F;cAC/F,iEAAiE,CAAC;IAC5E,CAAC;IAED,sGAAsG;IAC9F,eAAe,CAAC,GAAoB;QACxC,OAAO,oBAAoB,GAAG,CAAC,SAAS,yEAAyE;cAC3G,oGAAoG;cACpG,mEAAmE,CAAC;IAC9E,CAAC;CACJ,CAAA;AAnDY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,mBAAmB,CAmD/B","sourcesContent":["import { DevDeployConfig } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The \"now go watch it deploy\" block printed after a copy is published.\n *\n * WHY THIS IS TEXT AND NOT A FEATURE: webpieces cannot know the consumer's CD. The composed branch might\n * be built by GitHub Actions, by Jenkins, by a cron job on a box — so polling for \"the run\" would mean\n * either a config key naming their workflow, or a heuristic that is wrong for somebody. Handing the AI\n * the exact commands costs nothing, degrades gracefully (an empty `gh run list` is itself an answer), and\n * keeps webpieces out of the business of modelling someone else's pipeline.\n *\n * The commands are pre-substituted with the real sha and refs — an AI given a template with blanks to\n * fill in gets one of them wrong eventually, and a wrong `git merge-base` argument answers the question\n * confidently and incorrectly.\n *\n * All of them are READ-ONLY and none is blocked: `git fetch`/`merge-base` are queries, and\n * `gh run list|view|watch` is on the guards' allowed list.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DevDeployWatchHints {\n /**\n * @param sha the commit that was just published — the ONLY thing that makes step 1 a proof rather\n * than a guess, since `<copyRef>` itself moves the next time anyone publishes.\n */\n render(cfg: DevDeployConfig, copyRef: string, sha: string): string {\n return '\\n' + SEP + '👀 Is it on the dev server yet? — two questions, in this order\\n' + SEP + '\\n'\n + this.landedSection(cfg, sha)\n + '\\n'\n + this.runSection(cfg, copyRef)\n + '\\n'\n + this.orderingWarning(cfg);\n }\n\n /**\n * Step 1 — pure git, works with ANY CI, and is the only claim provably about YOUR code.\n *\n * `<devBranch>` is recomposed from `origin/main` plus every copy on each CI run, so your published\n * sha is an ancestor of it if and only if the composition included you.\n */\n private landedSection(cfg: DevDeployConfig, sha: string): string {\n return `1. Did YOUR code land in \\`${cfg.devBranch}\\`? (pure git — works whatever your CI is)\\n`\n + ` git fetch origin ${cfg.devBranch}\\n`\n + ` git merge-base --is-ancestor ${sha} origin/${cfg.devBranch} && echo IN || echo \"not yet\"\\n`\n + ` \\`${cfg.devBranch}\\` is rebuilt from origin/main + every \\`${cfg.copyRefGlob()}\\` ref on each run, so\\n`\n + ' ancestry IS inclusion. Re-run after the next composition if it says \"not yet\".\\n';\n }\n\n /** Step 2 — GitHub Actions, if that is where this repo's dev deploy lives. Empty output is an answer. */\n private runSection(cfg: DevDeployConfig, copyRef: string): string {\n const rows = [\n [`gh run list --branch ${copyRef} --limit 5`, 'the run your push triggered, if any'],\n [`gh run list --branch ${cfg.devBranch} --limit 5`, 'the composition / deploy run'],\n ['gh run watch <run-id>', 'follow one to completion'],\n ['gh run view <run-id> --log-failed', 'read why it failed'],\n ];\n // Aligned off the longest command, because the ref names are consumer-configurable and any\n // hand-counted padding would go crooked the moment somebody renames the namespace.\n const width = Math.max(...rows.map((row: string[]): number => row[0].length));\n return '2. Is the CI/CD run finished? (GitHub Actions — all read-only, none of it is blocked)\\n'\n + rows.map((row: string[]): string => ` ${row[0].padEnd(width)} ← ${row[1]}\\n`).join('')\n + ' Empty output is not a bug: it means this repo deploys from somewhere other than Actions.\\n'\n + ' Ask the human where the dev deploy runs, and do not guess.\\n';\n }\n\n /** The trap worth one sentence: a green run on a SHARED branch is not a statement about your code. */\n private orderingWarning(cfg: DevDeployConfig): string {\n return `A green run on \\`${cfg.devBranch}\\` is NOT proof your change deployed — that branch carries everyone's\\n`\n + 'copies, and the composition may have excluded yours. Question 1 is the one that answers for YOUR\\n'\n + 'code; question 2 only tells you whether the machinery finished.\\n';\n }\n}\n"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
import { DevDeployWatchHints } from './dev-deploy-watch-hints';
|
|
6
|
+
/**
|
|
7
|
+
* The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.
|
|
8
|
+
*
|
|
9
|
+
* ONE class owns every git WRITE in the resolve flow, and that is a hard requirement rather than tidiness:
|
|
10
|
+
* `redirect-how-to-merge-main` blocks `git merge` in every form INCLUDING `--continue`, and
|
|
11
|
+
* `pr-creation-or-push-guard` blocks every `git push`. So an AI cannot be told "now run `git commit`" or
|
|
12
|
+
* "`git merge --continue`" — those instructions are unrunnable by the only party reading them. The command
|
|
13
|
+
* owns the merges, the commit and the push (as child processes the hooks never see); the AI's entire job is
|
|
14
|
+
* editing the conflicted files and `git add`-ing them, neither of which is blocked.
|
|
15
|
+
*/
|
|
16
|
+
export declare class DevResolveRunner {
|
|
17
|
+
private readonly gitExec;
|
|
18
|
+
private readonly refs;
|
|
19
|
+
private readonly store;
|
|
20
|
+
private readonly watchHints;
|
|
21
|
+
constructor(gitExec: GitExec, refs: DevDeployRefs, store: PushDevStateStore, watchHints: DevDeployWatchHints);
|
|
22
|
+
/**
|
|
23
|
+
* Start a resolve: park on a throwaway branch at the current copy, then drain the queue.
|
|
24
|
+
*
|
|
25
|
+
* The checkout is `-B <tmp> <base>` and NEVER the feature branch, which is the cardinal constraint made
|
|
26
|
+
* mechanical — every merge below lands on a branch that is deleted at the end, so no other developer's
|
|
27
|
+
* commits can reach the PR head even if the resolve is abandoned halfway.
|
|
28
|
+
*
|
|
29
|
+
* `base` differs between the two callers and the difference IS the semantics. `--resolve` starts at the
|
|
30
|
+
* published copy and merges the other copies in (compose what CI will build). `--rebase-resolution`
|
|
31
|
+
* starts at the LOCAL HEAD and merges the published copy in (replay an existing resolution on top of
|
|
32
|
+
* the new commits, instead of force-pushing over it).
|
|
33
|
+
*
|
|
34
|
+
* `fetchRefs` is passed separately from the queue because `--rebase-resolution` merges a ref that is
|
|
35
|
+
* also its push destination, so "everything to fetch" and "everything to merge" are not the same list.
|
|
36
|
+
*/
|
|
37
|
+
start(repoRoot: string, state: PushDevState, base: string, fetchRefs: string[]): void;
|
|
38
|
+
/**
|
|
39
|
+
* Resume after the human/AI has resolved the halted merge: commit it, then keep draining.
|
|
40
|
+
*
|
|
41
|
+
* REFETCHES the remaining queue first. A resolution session can run long, and queuing against refs
|
|
42
|
+
* fetched an hour ago rehearses a composition CI will not build — the one freshness gap the sibling
|
|
43
|
+
* update flow is still known to have (only its stage ① ever fetches).
|
|
44
|
+
*/
|
|
45
|
+
resume(repoRoot: string, state: PushDevState): void;
|
|
46
|
+
/**
|
|
47
|
+
* Throw the whole resolve away: undo any halted merge, go back to the original branch, delete the
|
|
48
|
+
* throwaway branch and the state file. Nothing published, nothing on the feature branch.
|
|
49
|
+
*/
|
|
50
|
+
abort(repoRoot: string, state: PushDevState): void;
|
|
51
|
+
/**
|
|
52
|
+
* Merge queued refs until one conflicts (HALT, hand the tree over) or the queue empties (PUBLISH).
|
|
53
|
+
*
|
|
54
|
+
* A LATER ref can conflict too, which is why this is a loop resumed from a state file rather than a
|
|
55
|
+
* single pass: `wp-finish-push-dev` can legitimately stop again on the next ref in the queue.
|
|
56
|
+
*/
|
|
57
|
+
private drain;
|
|
58
|
+
/** Stop with the conflicted files listed and the ONE command that continues. Exit 2 = "your turn". */
|
|
59
|
+
private halt;
|
|
60
|
+
/** Queue empty: force-push the composed result over the copy, then restore the original checkout. */
|
|
61
|
+
private publish;
|
|
62
|
+
/** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */
|
|
63
|
+
private unmergedPaths;
|
|
64
|
+
private assertNoUnmergedPaths;
|
|
65
|
+
/**
|
|
66
|
+
* The name of the throwaway branch for a resolve of `branch`.
|
|
67
|
+
*
|
|
68
|
+
* Suffixed rather than prefixed, matching the existing `<feature>Squash` / `<feature>PreMerge<n>`
|
|
69
|
+
* convention — and deliberately NOT inside the dev namespace, which holds REMOTE copies only.
|
|
70
|
+
*/
|
|
71
|
+
tmpBranchFor(branch: string): string;
|
|
72
|
+
/** The banner `--resolve` prints before the first merge, so the queue is visible before it halts. */
|
|
73
|
+
announce(cfg: DevDeployConfig, state: PushDevState): string;
|
|
74
|
+
}
|