@webpieces/pr-gate 0.4.590 → 0.4.591

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.
@@ -6,35 +6,33 @@ const fs = tslib_1.__importStar(require("fs"));
6
6
  const path = tslib_1.__importStar(require("path"));
7
7
  const rules_config_1 = require("@webpieces/rules-config");
8
8
  const inversify_1 = require("inversify");
9
- const CUTOFF_DAYS = 30;
10
9
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
11
- // Result of one sweep: how many files were reaped for age, and how many now-empty dirs were pruned.
12
- class SweepResult {
13
- files;
14
- dirs;
15
- constructor(files, dirs) {
16
- this.files = files;
17
- this.dirs = dirs;
18
- }
19
- }
20
10
  /**
21
- * 30-day garbage collection of the whole `.webpieces` tree.
11
+ * 30-day garbage collection of the whole `.webpieces` tree, AND of the machine-global PR-body store.
12
+ *
13
+ * Runs at the end of every merge/PR flow (see merge-end.ts). Both roots get the identical policy from
14
+ * the identical implementation (`AgedTreeSweeper`): any file older than the cutoff is deleted, and any
15
+ * directory left empty afterwards is pruned. Neither root itself is ever removed.
22
16
  *
23
- * Runs at the end of every merge/PR flow (see merge-end.ts). It walks `.webpieces` depth-first and:
24
- * 1. deletes ANY file whose mtime is older than the cutoff, anywhere in the tree, and
25
- * 2. removes ANY directory that is left empty afterwards (including dirs that were already empty).
17
+ * Everything the tooling still needs is rewritten on each run under a fresh mtime (instruct-ai/ docs,
18
+ * the active logs, the *-status.json state files, and an open PR's merge body), and every writer
19
+ * `mkdirSync(..., { recursive: true })`s its target first — so pruning a stale home dir is harmless;
20
+ * the next write recreates it.
26
21
  *
27
- * The `.webpieces` root itself is never removed. Everything the tooling still needs is rewritten on
28
- * each run under a fresh mtime (instruct-ai/ docs, the active hooks/ logs, the *-status.json state
29
- * files), and every writer `mkdirSync(..., { recursive: true })`s its target first — so pruning a
30
- * stale home dir is harmless; the next write recreates it. Stale per-feature merge-info/pr-review
31
- * subdirs and the retired legacy flat layout all fall out of this single pass with no special-casing.
22
+ * The SECOND root is the one with no other owner. `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/` survives
23
+ * `rm -rf <clone>`, so if this sweep did not reap it nothing ever would (`decisions/0001` § O3). It is
24
+ * swept from here — the one command that already runs at the end of every flow — rather than from a new
25
+ * mechanism, because a retention policy nobody runs is not a retention policy.
32
26
  */
33
27
  let CleanTmp = class CleanTmp {
34
28
  repoRootFinder;
29
+ prBodies;
30
+ sweeper;
35
31
  dotDir;
36
- constructor(repoRootFinder, dotDir = rules_config_1.dotWebpieces) {
32
+ constructor(repoRootFinder, prBodies, sweeper, dotDir = rules_config_1.dotWebpieces) {
37
33
  this.repoRootFinder = repoRootFinder;
34
+ this.prBodies = prBodies;
35
+ this.sweeper = sweeper;
38
36
  this.dotDir = dotDir;
39
37
  }
40
38
  async cleanTmp() {
@@ -43,72 +41,41 @@ let CleanTmp = class CleanTmp {
43
41
  // never sweep the repo-wide dir, whose entries (merged-branches.json, the main-sync status and
44
42
  // its lock) belong to every worktree at once.
45
43
  const tmpBase = this.dotDir.local(repoRoot);
46
- if (!fs.existsSync(tmpBase)) {
44
+ const prsRoot = this.prBodies.prsRoot(repoRoot);
45
+ if (!fs.existsSync(tmpBase) && !fs.existsSync(prsRoot))
47
46
  return;
48
- }
49
- process.stdout.write('\n');
50
- process.stdout.write(SEP);
51
- process.stdout.write('🧹 Garbage-Collecting .webpieces\n');
52
- process.stdout.write(SEP);
53
- process.stdout.write('\n');
47
+ process.stdout.write('\n' + SEP + '🧹 Garbage-Collecting .webpieces\n' + SEP + '\n');
54
48
  process.stdout.write(`Location: ${tmpBase}\n`);
55
- process.stdout.write(`Retention: ${CUTOFF_DAYS} days (older files reaped; empty dirs pruned)\n`);
56
- process.stdout.write('\n');
57
- const cutoffMs = CUTOFF_DAYS * 24 * 60 * 60 * 1000;
58
- const now = Date.now();
59
- // `true` => this is the root, which is swept into but never itself removed. This generic
60
- // depth-first sweep needs NO knowledge of the merge-info layout — including the new
61
- // staged/<feature> and merged/<feature> split, which sits one level deeper than the old
62
- // per-feature dirs. That is precisely why it replaced the per-home special-casing.
63
- const result = this.sweep(tmpBase, tmpBase, now, cutoffMs, true);
64
- if (result.files === 0 && result.dirs === 0) {
65
- process.stdout.write(` ✅ Nothing older than ${CUTOFF_DAYS} days; no empty directories\n`);
49
+ process.stdout.write(` ${prsRoot} (machine-global PR merge bodies)\n`);
50
+ process.stdout.write(`Retention: ${rules_config_1.RETENTION_DAYS} days (older files reaped; empty dirs pruned)\n\n`);
51
+ const cutoffMs = rules_config_1.RETENTION_DAYS * 24 * 60 * 60 * 1000;
52
+ const total = new rules_config_1.SweepCount();
53
+ total.add(this.sweeper.sweep(tmpBase, cutoffMs, this.reporter(tmpBase)));
54
+ total.add(this.prBodies.sweep(repoRoot, this.reporter(prsRoot)));
55
+ if (total.empty) {
56
+ process.stdout.write(` ✅ Nothing older than ${rules_config_1.RETENTION_DAYS} days; no empty directories\n`);
66
57
  }
67
58
  else {
68
- const fileWord = result.files === 1 ? 'file' : 'files';
69
- const dirWord = result.dirs === 1 ? 'directory' : 'directories';
70
- process.stdout.write('\n');
71
- process.stdout.write(` ✅ Reaped ${result.files} old ${fileWord} and pruned ${result.dirs} empty ${dirWord}\n`);
59
+ const fileWord = total.files === 1 ? 'file' : 'files';
60
+ const dirWord = total.dirs === 1 ? 'directory' : 'directories';
61
+ process.stdout.write(`\n ✅ Reaped ${total.files} old ${fileWord} and pruned ${total.dirs} empty ${dirWord}\n`);
72
62
  }
73
- process.stdout.write('\n');
74
- process.stdout.write(SEP);
75
- process.stdout.write('\n');
63
+ process.stdout.write('\n' + SEP + '\n');
76
64
  }
77
- // Depth-first, post-order sweep of `dir` (rooted at `tmpBase`, only used for tidy relative logging).
78
- // Files older than the cutoff are deleted; a directory left empty once its children are processed is
79
- // pruned — except the root, which is kept even when empty so `.webpieces/` itself survives.
80
- sweep(dir, tmpBase, now, cutoffMs, isRoot) {
81
- let files = 0;
82
- let dirs = 0;
83
- for (const entry of fs.readdirSync(dir)) {
84
- const fullPath = path.join(dir, entry);
85
- // lstat, not stat: a symlink is treated as a leaf (aged out like a file), never followed —
86
- // so we can never wander outside `.webpieces` or delete a link target elsewhere.
87
- const stat = fs.lstatSync(fullPath);
88
- if (stat.isDirectory()) {
89
- const nested = this.sweep(fullPath, tmpBase, now, cutoffMs, false);
90
- files += nested.files;
91
- dirs += nested.dirs;
92
- }
93
- else if (now - stat.mtimeMs >= cutoffMs) {
94
- process.stdout.write(` 🗑️ file: ${path.relative(tmpBase, fullPath)}\n`);
95
- fs.rmSync(fullPath, { force: true });
96
- files += 1;
97
- }
98
- }
99
- // Post-order: prune this dir if reaping its children (or nothing) left it empty.
100
- if (!isRoot && fs.readdirSync(dir).length === 0) {
101
- process.stdout.write(` 🗑️ dir: ${path.relative(tmpBase, dir)}/\n`);
102
- fs.rmdirSync(dir);
103
- dirs += 1;
104
- }
105
- return new SweepResult(files, dirs);
65
+ // One line per removal, relative to the root it came from, so the two roots read as one report.
66
+ reporter(base) {
67
+ return (removed, isDir) => {
68
+ const label = isDir ? 'dir: ' : 'file: ';
69
+ process.stdout.write(` 🗑️ ${label} ${path.relative(base, removed)}${isDir ? '/' : ''}\n`);
70
+ };
106
71
  }
107
72
  };
108
73
  exports.CleanTmp = CleanTmp;
109
74
  exports.CleanTmp = CleanTmp = tslib_1.__decorate([
110
75
  (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
111
76
  tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
77
+ rules_config_1.PrBodyStore,
78
+ rules_config_1.AgedTreeSweeper,
112
79
  rules_config_1.DotWebpieces])
113
80
  ], CleanTmp);
114
81
  //# sourceMappingURL=cleanTmp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cleanTmp.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/cleanTmp.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { DotWebpieces, dotWebpieces, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst CUTOFF_DAYS = 30;\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Result of one sweep: how many files were reaped for age, and how many now-empty dirs were pruned.\nclass SweepResult {\n constructor(\n public files: number,\n public dirs: number,\n ) {}\n}\n\n/**\n * 30-day garbage collection of the whole `.webpieces` tree.\n *\n * Runs at the end of every merge/PR flow (see merge-end.ts). It walks `.webpieces` depth-first and:\n * 1. deletes ANY file whose mtime is older than the cutoff, anywhere in the tree, and\n * 2. removes ANY directory that is left empty afterwards (including dirs that were already empty).\n *\n * The `.webpieces` root itself is never removed. Everything the tooling still needs is rewritten on\n * each run under a fresh mtime (instruct-ai/ docs, the active hooks/ logs, the *-status.json state\n * files), and every writer `mkdirSync(..., { recursive: true })`s its target first — so pruning a\n * stale home dir is harmless; the next write recreates it. Stale per-feature merge-info/pr-review\n * subdirs and the retired legacy flat layout all fall out of this single pass with no special-casing.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CleanTmp {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly dotDir: DotWebpieces = dotWebpieces,\n ) {}\n\n async cleanTmp(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // LOCAL scope: a worktree garbage-collects its OWN merge-info/pr-review scratch dirs. It must\n // never sweep the repo-wide dir, whose entries (merged-branches.json, the main-sync status and\n // its lock) belong to every worktree at once.\n const tmpBase = this.dotDir.local(repoRoot);\n\n if (!fs.existsSync(tmpBase)) {\n return;\n }\n\n process.stdout.write('\\n');\n process.stdout.write(SEP);\n process.stdout.write('🧹 Garbage-Collecting .webpieces\\n');\n process.stdout.write(SEP);\n process.stdout.write('\\n');\n process.stdout.write(`Location: ${tmpBase}\\n`);\n process.stdout.write(`Retention: ${CUTOFF_DAYS} days (older files reaped; empty dirs pruned)\\n`);\n process.stdout.write('\\n');\n\n const cutoffMs = CUTOFF_DAYS * 24 * 60 * 60 * 1000;\n const now = Date.now();\n\n // `true` => this is the root, which is swept into but never itself removed. This generic\n // depth-first sweep needs NO knowledge of the merge-info layout — including the new\n // staged/<feature> and merged/<feature> split, which sits one level deeper than the old\n // per-feature dirs. That is precisely why it replaced the per-home special-casing.\n const result = this.sweep(tmpBase, tmpBase, now, cutoffMs, true);\n\n if (result.files === 0 && result.dirs === 0) {\n process.stdout.write(` ✅ Nothing older than ${CUTOFF_DAYS} days; no empty directories\\n`);\n } else {\n const fileWord = result.files === 1 ? 'file' : 'files';\n const dirWord = result.dirs === 1 ? 'directory' : 'directories';\n process.stdout.write('\\n');\n process.stdout.write(` ✅ Reaped ${result.files} old ${fileWord} and pruned ${result.dirs} empty ${dirWord}\\n`);\n }\n\n process.stdout.write('\\n');\n process.stdout.write(SEP);\n process.stdout.write('\\n');\n }\n\n // Depth-first, post-order sweep of `dir` (rooted at `tmpBase`, only used for tidy relative logging).\n // Files older than the cutoff are deleted; a directory left empty once its children are processed is\n // pruned — except the root, which is kept even when empty so `.webpieces/` itself survives.\n private sweep(dir: string, tmpBase: string, now: number, cutoffMs: number, isRoot: boolean): SweepResult {\n let files = 0;\n let dirs = 0;\n\n for (const entry of fs.readdirSync(dir)) {\n const fullPath = path.join(dir, entry);\n // lstat, not stat: a symlink is treated as a leaf (aged out like a file), never followed —\n // so we can never wander outside `.webpieces` or delete a link target elsewhere.\n const stat = fs.lstatSync(fullPath);\n if (stat.isDirectory()) {\n const nested = this.sweep(fullPath, tmpBase, now, cutoffMs, false);\n files += nested.files;\n dirs += nested.dirs;\n } else if (now - stat.mtimeMs >= cutoffMs) {\n process.stdout.write(` 🗑️ file: ${path.relative(tmpBase, fullPath)}\\n`);\n fs.rmSync(fullPath, { force: true });\n files += 1;\n }\n }\n\n // Post-order: prune this dir if reaping its children (or nothing) left it empty.\n if (!isRoot && fs.readdirSync(dir).length === 0) {\n process.stdout.write(` 🗑️ dir: ${path.relative(tmpBase, dir)}/\\n`);\n fs.rmdirSync(dir);\n dirs += 1;\n }\n\n return new SweepResult(files, dirs);\n }\n}\n"]}
1
+ {"version":3,"file":"cleanTmp.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/cleanTmp.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n AgedTreeSweeper, DotWebpieces, PrBodyStore, RETENTION_DAYS, RepoRootFinder, SweepCount, dotWebpieces,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * 30-day garbage collection of the whole `.webpieces` tree, AND of the machine-global PR-body store.\n *\n * Runs at the end of every merge/PR flow (see merge-end.ts). Both roots get the identical policy from\n * the identical implementation (`AgedTreeSweeper`): any file older than the cutoff is deleted, and any\n * directory left empty afterwards is pruned. Neither root itself is ever removed.\n *\n * Everything the tooling still needs is rewritten on each run under a fresh mtime (instruct-ai/ docs,\n * the active logs, the *-status.json state files, and an open PR's merge body), and every writer\n * `mkdirSync(..., { recursive: true })`s its target first — so pruning a stale home dir is harmless;\n * the next write recreates it.\n *\n * The SECOND root is the one with no other owner. `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/` survives\n * `rm -rf <clone>`, so if this sweep did not reap it nothing ever would (`decisions/0001` § O3). It is\n * swept from here — the one command that already runs at the end of every flow — rather than from a new\n * mechanism, because a retention policy nobody runs is not a retention policy.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CleanTmp {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly prBodies: PrBodyStore,\n private readonly sweeper: AgedTreeSweeper,\n private readonly dotDir: DotWebpieces = dotWebpieces,\n ) {}\n\n async cleanTmp(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // LOCAL scope: a worktree garbage-collects its OWN merge-info/pr-review scratch dirs. It must\n // never sweep the repo-wide dir, whose entries (merged-branches.json, the main-sync status and\n // its lock) belong to every worktree at once.\n const tmpBase = this.dotDir.local(repoRoot);\n const prsRoot = this.prBodies.prsRoot(repoRoot);\n if (!fs.existsSync(tmpBase) && !fs.existsSync(prsRoot)) return;\n\n process.stdout.write('\\n' + SEP + '🧹 Garbage-Collecting .webpieces\\n' + SEP + '\\n');\n process.stdout.write(`Location: ${tmpBase}\\n`);\n process.stdout.write(` ${prsRoot} (machine-global PR merge bodies)\\n`);\n process.stdout.write(`Retention: ${RETENTION_DAYS} days (older files reaped; empty dirs pruned)\\n\\n`);\n\n const cutoffMs = RETENTION_DAYS * 24 * 60 * 60 * 1000;\n const total = new SweepCount();\n total.add(this.sweeper.sweep(tmpBase, cutoffMs, this.reporter(tmpBase)));\n total.add(this.prBodies.sweep(repoRoot, this.reporter(prsRoot)));\n\n if (total.empty) {\n process.stdout.write(` ✅ Nothing older than ${RETENTION_DAYS} days; no empty directories\\n`);\n } else {\n const fileWord = total.files === 1 ? 'file' : 'files';\n const dirWord = total.dirs === 1 ? 'directory' : 'directories';\n process.stdout.write(`\\n ✅ Reaped ${total.files} old ${fileWord} and pruned ${total.dirs} empty ${dirWord}\\n`);\n }\n\n process.stdout.write('\\n' + SEP + '\\n');\n }\n\n // One line per removal, relative to the root it came from, so the two roots read as one report.\n private reporter(base: string): (removed: string, isDir: boolean) => void {\n return (removed: string, isDir: boolean): void => {\n const label = isDir ? 'dir: ' : 'file: ';\n process.stdout.write(` 🗑️ ${label} ${path.relative(base, removed)}${isDir ? '/' : ''}\\n`);\n };\n }\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import { DotWebpieces, PrBodyStore } from '@webpieces/rules-config';
2
+ /** Everything needed to file one gated squash body. Data-only, per CLAUDE.md. */
3
+ export declare class MergeBodyRequest {
4
+ treeRoot: string;
5
+ branch: string;
6
+ feature: string;
7
+ prNumber: string;
8
+ prUrl: string;
9
+ body: string;
10
+ }
11
+ /**
12
+ * Files the gated squash-commit body where LANDING can find it, and returns the path to hand `gh`.
13
+ *
14
+ * MACHINE-GLOBAL, keyed by the PR's own identity —
15
+ * `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/merge-commit-body.md`. It used to be written into the
16
+ * rendering worktree's `pr-review/<branch>/`, which made landing work only while the branch never moved
17
+ * trees: the gated flow ran in the primary clone, landing happened from a linked worktree, and
18
+ * `wp-land-pr` found nothing and said "Nothing to land" at a perfectly good PR. See PrBodyStore for why
19
+ * the PR's identity — not the tree, not the branch — is the right key.
20
+ *
21
+ * There is deliberately NO second write to the old in-repo path. Per CLAUDE.md this is a hard cut: two
22
+ * homes for the receipt is two answers to "which bytes land", and the stale one wins in exactly the
23
+ * situation that broke. `wp-land-pr` prints a LOUD one-time signpost if it finds a body left by an older
24
+ * release, and never reads it.
25
+ *
26
+ * Separate from FinishUpsertPrCommand because it is the WRITE half of a two-command contract whose read
27
+ * half lives in LandPrCommand — the pair is the thing that has to stay true, and a private method inside
28
+ * a 600-line command is not something the read half's spec can hold still next to.
29
+ */
30
+ export declare class MergeBodyFiler {
31
+ private readonly prBodies;
32
+ private readonly dotDir;
33
+ constructor(prBodies: PrBodyStore, dotDir?: DotWebpieces);
34
+ /**
35
+ * @returns the file to pass as `gh pr merge --body-file`. Never ''.
36
+ *
37
+ * The temp file is the ONE case the store cannot serve: `gh pr view` gave no number back (or the
38
+ * remote could not be parsed), so there is no key to file under. This process still holds the bytes
39
+ * and can merge with them right now; a later `wp-land-pr` correctly reports the PR as not found on
40
+ * this machine, because no durable receipt was ever filed. Saying so beats pretending otherwise.
41
+ */
42
+ file(request: MergeBodyRequest): string;
43
+ private warnIfDegraded;
44
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MergeBodyFiler = exports.MergeBodyRequest = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const os = tslib_1.__importStar(require("os"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const rules_config_1 = require("@webpieces/rules-config");
9
+ const inversify_1 = require("inversify");
10
+ /** Everything needed to file one gated squash body. Data-only, per CLAUDE.md. */
11
+ class MergeBodyRequest {
12
+ treeRoot = '';
13
+ branch = '';
14
+ feature = '';
15
+ prNumber = '';
16
+ prUrl = '';
17
+ body = '';
18
+ }
19
+ exports.MergeBodyRequest = MergeBodyRequest;
20
+ /**
21
+ * Files the gated squash-commit body where LANDING can find it, and returns the path to hand `gh`.
22
+ *
23
+ * MACHINE-GLOBAL, keyed by the PR's own identity —
24
+ * `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/merge-commit-body.md`. It used to be written into the
25
+ * rendering worktree's `pr-review/<branch>/`, which made landing work only while the branch never moved
26
+ * trees: the gated flow ran in the primary clone, landing happened from a linked worktree, and
27
+ * `wp-land-pr` found nothing and said "Nothing to land" at a perfectly good PR. See PrBodyStore for why
28
+ * the PR's identity — not the tree, not the branch — is the right key.
29
+ *
30
+ * There is deliberately NO second write to the old in-repo path. Per CLAUDE.md this is a hard cut: two
31
+ * homes for the receipt is two answers to "which bytes land", and the stale one wins in exactly the
32
+ * situation that broke. `wp-land-pr` prints a LOUD one-time signpost if it finds a body left by an older
33
+ * release, and never reads it.
34
+ *
35
+ * Separate from FinishUpsertPrCommand because it is the WRITE half of a two-command contract whose read
36
+ * half lives in LandPrCommand — the pair is the thing that has to stay true, and a private method inside
37
+ * a 600-line command is not something the read half's spec can hold still next to.
38
+ */
39
+ let MergeBodyFiler = class MergeBodyFiler {
40
+ prBodies;
41
+ dotDir;
42
+ constructor(prBodies, dotDir = rules_config_1.dotWebpieces) {
43
+ this.prBodies = prBodies;
44
+ this.dotDir = dotDir;
45
+ }
46
+ /**
47
+ * @returns the file to pass as `gh pr merge --body-file`. Never ''.
48
+ *
49
+ * The temp file is the ONE case the store cannot serve: `gh pr view` gave no number back (or the
50
+ * remote could not be parsed), so there is no key to file under. This process still holds the bytes
51
+ * and can merge with them right now; a later `wp-land-pr` correctly reports the PR as not found on
52
+ * this machine, because no durable receipt was ever filed. Saying so beats pretending otherwise.
53
+ */
54
+ file(request) {
55
+ const origin = new rules_config_1.PrBodyOrigin();
56
+ origin.treeRoot = request.treeRoot;
57
+ origin.primaryRoot = this.dotDir.primaryRoot(request.treeRoot);
58
+ origin.branch = request.branch;
59
+ origin.feature = request.feature;
60
+ origin.prNumber = request.prNumber;
61
+ origin.prUrl = request.prUrl;
62
+ origin.writtenAt = new Date().toISOString();
63
+ const stored = this.prBodies.write(request.treeRoot, request.prNumber, request.body, origin);
64
+ if (stored !== null) {
65
+ process.stdout.write(` merge body → ${stored.bodyFile}\n`);
66
+ this.warnIfDegraded(request.treeRoot);
67
+ return stored.bodyFile;
68
+ }
69
+ const tmp = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'wp-merge-body-')), rules_config_1.MERGE_BODY_FILE);
70
+ fs.writeFileSync(tmp, request.body);
71
+ process.stderr.write(' ⚠️ could not file the merge body under this PR\'s identity (no PR number read back, or the\n' +
72
+ ' git remote could not be parsed). It was written to a temp file for THIS run only, so\n' +
73
+ ' `pnpm wp-land-pr` will report the PR as not found on this machine.\n');
74
+ return tmp;
75
+ }
76
+ // A degraded home is a receipt written INSIDE the clone — which is the thing that was broken. It
77
+ // still works from here, so it is a warning, not a failure; it is never silent.
78
+ warnIfDegraded(treeRoot) {
79
+ const home = this.prBodies.home(treeRoot);
80
+ if (!home.degraded)
81
+ return;
82
+ process.stderr.write(` ⚠️ that path is INSIDE this clone (${home.reason}), so \`pnpm wp-land-pr\` will only find\n` +
83
+ ` it from this clone. Set ${rules_config_1.WEBPIECES_STATE_HOME_ENV} to a writable directory.\n`);
84
+ }
85
+ };
86
+ exports.MergeBodyFiler = MergeBodyFiler;
87
+ exports.MergeBodyFiler = MergeBodyFiler = tslib_1.__decorate([
88
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
89
+ tslib_1.__metadata("design:paramtypes", [rules_config_1.PrBodyStore,
90
+ rules_config_1.DotWebpieces])
91
+ ], MergeBodyFiler);
92
+ //# sourceMappingURL=merge-body-filer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-body-filer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-body-filer.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,+CAAyB;AACzB,mDAA6B;AAC7B,0DAEiC;AACjC,yCAA2D;AAE3D,iFAAiF;AACjF,MAAa,gBAAgB;IACzB,QAAQ,GAAG,EAAE,CAAC;IACd,MAAM,GAAG,EAAE,CAAC;IACZ,OAAO,GAAG,EAAE,CAAC;IACb,QAAQ,GAAG,EAAE,CAAC;IACd,KAAK,GAAG,EAAE,CAAC;IACX,IAAI,GAAG,EAAE,CAAC;CACb;AAPD,4CAOC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AAEI,IAAM,cAAc,GAApB,MAAM,cAAc;IAEF;IACA;IAFrB,YACqB,QAAqB,EACrB,SAAuB,2BAAY;QADnC,aAAQ,GAAR,QAAQ,CAAa;QACrB,WAAM,GAAN,MAAM,CAA6B;IACrD,CAAC;IAEJ;;;;;;;OAOG;IACH,IAAI,CAAC,OAAyB;QAC1B,MAAM,MAAM,GAAG,IAAI,2BAAY,EAAE,CAAC;QAClC,MAAM,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACnC,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC/D,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC/B,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QACjC,MAAM,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACnC,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC7B,MAAM,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAE5C,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7F,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YAClB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;YAC7D,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACtC,OAAO,MAAM,CAAC,QAAQ,CAAC;QAC3B,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAC,EAAE,8BAAe,CAAC,CAAC;QACjG,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,kGAAkG;YAClG,+FAA+F;YAC/F,6EAA6E,CAAC,CAAC;QACnF,OAAO,GAAG,CAAC;IACf,CAAC;IAED,iGAAiG;IACjG,gFAAgF;IACxE,cAAc,CAAC,QAAgB;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,0CAA0C,IAAI,CAAC,MAAM,4CAA4C;YACjG,kCAAkC,uCAAwB,6BAA6B,CAAC,CAAC;IACjG,CAAC;CACJ,CAAA;AAjDY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGN,0BAAW;QACb,2BAAY;GAHhC,cAAc,CAiD1B","sourcesContent":["import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport {\n DotWebpieces, MERGE_BODY_FILE, PrBodyOrigin, PrBodyStore, WEBPIECES_STATE_HOME_ENV, dotWebpieces,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/** Everything needed to file one gated squash body. Data-only, per CLAUDE.md. */\nexport class MergeBodyRequest {\n treeRoot = '';\n branch = '';\n feature = '';\n prNumber = '';\n prUrl = '';\n body = '';\n}\n\n/**\n * Files the gated squash-commit body where LANDING can find it, and returns the path to hand `gh`.\n *\n * MACHINE-GLOBAL, keyed by the PR's own identity —\n * `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/merge-commit-body.md`. It used to be written into the\n * rendering worktree's `pr-review/<branch>/`, which made landing work only while the branch never moved\n * trees: the gated flow ran in the primary clone, landing happened from a linked worktree, and\n * `wp-land-pr` found nothing and said \"Nothing to land\" at a perfectly good PR. See PrBodyStore for why\n * the PR's identity — not the tree, not the branch — is the right key.\n *\n * There is deliberately NO second write to the old in-repo path. Per CLAUDE.md this is a hard cut: two\n * homes for the receipt is two answers to \"which bytes land\", and the stale one wins in exactly the\n * situation that broke. `wp-land-pr` prints a LOUD one-time signpost if it finds a body left by an older\n * release, and never reads it.\n *\n * Separate from FinishUpsertPrCommand because it is the WRITE half of a two-command contract whose read\n * half lives in LandPrCommand — the pair is the thing that has to stay true, and a private method inside\n * a 600-line command is not something the read half's spec can hold still next to.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class MergeBodyFiler {\n constructor(\n private readonly prBodies: PrBodyStore,\n private readonly dotDir: DotWebpieces = dotWebpieces,\n ) {}\n\n /**\n * @returns the file to pass as `gh pr merge --body-file`. Never ''.\n *\n * The temp file is the ONE case the store cannot serve: `gh pr view` gave no number back (or the\n * remote could not be parsed), so there is no key to file under. This process still holds the bytes\n * and can merge with them right now; a later `wp-land-pr` correctly reports the PR as not found on\n * this machine, because no durable receipt was ever filed. Saying so beats pretending otherwise.\n */\n file(request: MergeBodyRequest): string {\n const origin = new PrBodyOrigin();\n origin.treeRoot = request.treeRoot;\n origin.primaryRoot = this.dotDir.primaryRoot(request.treeRoot);\n origin.branch = request.branch;\n origin.feature = request.feature;\n origin.prNumber = request.prNumber;\n origin.prUrl = request.prUrl;\n origin.writtenAt = new Date().toISOString();\n\n const stored = this.prBodies.write(request.treeRoot, request.prNumber, request.body, origin);\n if (stored !== null) {\n process.stdout.write(` merge body → ${stored.bodyFile}\\n`);\n this.warnIfDegraded(request.treeRoot);\n return stored.bodyFile;\n }\n\n const tmp = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'wp-merge-body-')), MERGE_BODY_FILE);\n fs.writeFileSync(tmp, request.body);\n process.stderr.write(\n ' ⚠️ could not file the merge body under this PR\\'s identity (no PR number read back, or the\\n' +\n ' git remote could not be parsed). It was written to a temp file for THIS run only, so\\n' +\n ' `pnpm wp-land-pr` will report the PR as not found on this machine.\\n');\n return tmp;\n }\n\n // A degraded home is a receipt written INSIDE the clone — which is the thing that was broken. It\n // still works from here, so it is a warning, not a failure; it is never silent.\n private warnIfDegraded(treeRoot: string): void {\n const home = this.prBodies.home(treeRoot);\n if (!home.degraded) return;\n process.stderr.write(\n ` ⚠️ that path is INSIDE this clone (${home.reason}), so \\`pnpm wp-land-pr\\` will only find\\n` +\n ` it from this clone. Set ${WEBPIECES_STATE_HOME_ENV} to a writable directory.\\n`);\n }\n}\n"]}
@@ -5,12 +5,26 @@ require("reflect-metadata");
5
5
  const inversify_1 = require("inversify");
6
6
  const rules_config_1 = require("@webpieces/rules-config");
7
7
  const pr_gate_app_1 = require("./pr-gate-app");
8
+ const land_pr_command_1 = require("./commands/land-pr-command");
9
+ const FALLBACK_TITLE_ONLY = '--fallback-title-only';
8
10
  // Composition root: build the container and resolve the app so inversify constructs the whole DAG.
9
11
  (0, rules_config_1.runMain)(async () => {
10
12
  // autobind self-binds every @injectable(Singleton) tooling class (replaces the buildProviderModule registry scan)
11
13
  const container = new inversify_1.Container({ autobind: true });
12
14
  // Reject `--help`/bogus flags BEFORE the app touches git — an ignored flag must never start the flow.
13
- container.get(rules_config_1.CliArgs).assertNoArgs(new rules_config_1.CliUsage('wp-land-pr', "Squash-merge this branch's PR into main with the compact risk/flags commit body."));
14
- await container.get(pr_gate_app_1.PrGateApp).landPr();
15
+ // A mistyped `--fallback-title-onl` that was silently dropped would turn a deliberate degraded land
16
+ // into a refusal, which is the safe direction — but a dropped flag is never acceptable on a command
17
+ // that writes main's history.
18
+ const args = container.get(rules_config_1.CliArgs).parse(new rules_config_1.CliUsage('wp-land-pr', "Squash-merge this branch's PR into main with the compact risk/flags commit body.", [
19
+ new rules_config_1.CliFlag(FALLBACK_TITLE_ONLY, 'A HUMAN DECISION. Land even though the gated commit body rendered by\n'
20
+ + ' `wp-finish-upsert-pr` is not on this machine: the commit gets the PR\n'
21
+ + ' TITLE + LINK and a line saying the gated body was unavailable. The PR\n'
22
+ + ' DESCRIPTION is never used — dumping a PR Gate Dashboard into main is\n'
23
+ + ' the ugly git log this whole mechanism exists to prevent. Do not pass\n'
24
+ + ' this on your own initiative; ask.'),
25
+ ]));
26
+ const opts = new land_pr_command_1.LandPrOptions();
27
+ opts.fallbackTitleOnly = args.has(FALLBACK_TITLE_ONLY);
28
+ await container.get(pr_gate_app_1.PrGateApp).landPr(opts);
15
29
  });
16
30
  //# sourceMappingURL=wp-land-pr.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"wp-land-pr.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-land-pr.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,0DAAqE;AACrE,+CAA0C;AAE1C,mGAAmG;AACnG,IAAA,sBAAO,EAAC,KAAK,IAAmB,EAAE;IAC9B,kHAAkH;IAClH,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,sGAAsG;IACtG,SAAS,CAAC,GAAG,CAAC,sBAAO,CAAC,CAAC,YAAY,CAAC,IAAI,uBAAQ,CAC5C,YAAY,EAAE,kFAAkF,CAAC,CAAC,CAAC;IACvG,MAAM,SAAS,CAAC,GAAG,CAAC,uBAAS,CAAC,CAAC,MAAM,EAAE,CAAC;AAC5C,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\nimport { runMain, CliArgs, CliUsage } from '@webpieces/rules-config';\nimport { PrGateApp } from './pr-gate-app';\n\n// Composition root: build the container and resolve the app so inversify constructs the whole DAG.\nrunMain(async (): Promise<void> => {\n // autobind self-binds every @injectable(Singleton) tooling class (replaces the buildProviderModule registry scan)\n const container = new Container({ autobind: true });\n // Reject `--help`/bogus flags BEFORE the app touches git — an ignored flag must never start the flow.\n container.get(CliArgs).assertNoArgs(new CliUsage(\n 'wp-land-pr', \"Squash-merge this branch's PR into main with the compact risk/flags commit body.\"));\n await container.get(PrGateApp).landPr();\n});\n"]}
1
+ {"version":3,"file":"wp-land-pr.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-land-pr.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,0DAA8E;AAC9E,+CAA0C;AAC1C,gEAA2D;AAE3D,MAAM,mBAAmB,GAAG,uBAAuB,CAAC;AAEpD,mGAAmG;AACnG,IAAA,sBAAO,EAAC,KAAK,IAAmB,EAAE;IAC9B,kHAAkH;IAClH,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,sGAAsG;IACtG,oGAAoG;IACpG,oGAAoG;IACpG,8BAA8B;IAC9B,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,sBAAO,CAAC,CAAC,KAAK,CAAC,IAAI,uBAAQ,CAClD,YAAY,EACZ,kFAAkF,EAClF;QACI,IAAI,sBAAO,CAAC,mBAAmB,EAC3B,wEAAwE;cACtE,kGAAkG;cAClG,mGAAmG;cACnG,kGAAkG;cAClG,kGAAkG;cAClG,6DAA6D,CAAC;KACvE,CACJ,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,IAAI,+BAAa,EAAE,CAAC;IACjC,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IACvD,MAAM,SAAS,CAAC,GAAG,CAAC,uBAAS,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAChD,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\nimport { runMain, CliArgs, CliFlag, CliUsage } from '@webpieces/rules-config';\nimport { PrGateApp } from './pr-gate-app';\nimport { LandPrOptions } from './commands/land-pr-command';\n\nconst FALLBACK_TITLE_ONLY = '--fallback-title-only';\n\n// Composition root: build the container and resolve the app so inversify constructs the whole DAG.\nrunMain(async (): Promise<void> => {\n // autobind self-binds every @injectable(Singleton) tooling class (replaces the buildProviderModule registry scan)\n const container = new Container({ autobind: true });\n // Reject `--help`/bogus flags BEFORE the app touches git — an ignored flag must never start the flow.\n // A mistyped `--fallback-title-onl` that was silently dropped would turn a deliberate degraded land\n // into a refusal, which is the safe direction — but a dropped flag is never acceptable on a command\n // that writes main's history.\n const args = container.get(CliArgs).parse(new CliUsage(\n 'wp-land-pr',\n \"Squash-merge this branch's PR into main with the compact risk/flags commit body.\",\n [\n new CliFlag(FALLBACK_TITLE_ONLY,\n 'A HUMAN DECISION. Land even though the gated commit body rendered by\\n'\n + ' `wp-finish-upsert-pr` is not on this machine: the commit gets the PR\\n'\n + ' TITLE + LINK and a line saying the gated body was unavailable. The PR\\n'\n + ' DESCRIPTION is never used — dumping a PR Gate Dashboard into main is\\n'\n + ' the ugly git log this whole mechanism exists to prevent. Do not pass\\n'\n + ' this on your own initiative; ask.'),\n ],\n ));\n const opts = new LandPrOptions();\n opts.fallbackTitleOnly = args.has(FALLBACK_TITLE_ONLY);\n await container.get(PrGateApp).landPr(opts);\n});\n"]}