@webpieces/pr-gate 0.4.564 → 0.4.566
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/wp-finish-push-dev.js +27 -0
- package/bin/wp-push-dev.js +27 -0
- package/package.json +5 -3
- package/src/dashboard/checklist-comment-row.d.ts +42 -0
- package/src/dashboard/checklist-comment-row.js +59 -0
- package/src/dashboard/checklist-comment-row.js.map +1 -0
- package/src/dashboard/dashboard.d.ts +19 -29
- package/src/dashboard/dashboard.js +57 -53
- package/src/dashboard/dashboard.js.map +1 -1
- 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/finish-upsert-pr-command.d.ts +9 -0
- package/src/scripts/commands/finish-upsert-pr-command.js +24 -4
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/push-dev-command.d.ts +89 -0
- package/src/scripts/commands/push-dev-command.js +249 -0
- package/src/scripts/commands/push-dev-command.js.map +1 -0
- package/src/scripts/commands/review-upsert-pr-command.d.ts +12 -1
- package/src/scripts/commands/review-upsert-pr-command.js +20 -4
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
- package/src/scripts/pr-gate-app.d.ts +24 -3
- package/src/scripts/pr-gate-app.js +48 -4
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/checklist-detector.js +1 -1
- package/src/scripts/workflow/checklist-detector.js.map +1 -1
- package/src/scripts/workflow/checklist-scanner.d.ts +21 -1
- package/src/scripts/workflow/checklist-scanner.js +31 -3
- package/src/scripts/workflow/checklist-scanner.js.map +1 -1
- package/src/scripts/workflow/dev-deploy-refs.d.ts +66 -0
- package/src/scripts/workflow/dev-deploy-refs.js +143 -0
- package/src/scripts/workflow/dev-deploy-refs.js.map +1 -0
- package/src/scripts/workflow/dev-resolve-runner.d.ts +72 -0
- package/src/scripts/workflow/dev-resolve-runner.js +180 -0
- package/src/scripts/workflow/dev-resolve-runner.js.map +1 -0
- package/src/scripts/workflow/git-exec.d.ts +20 -1
- package/src/scripts/workflow/git-exec.js +30 -3
- package/src/scripts/workflow/git-exec.js.map +1 -1
- package/src/scripts/workflow/push-dev-state.d.ts +52 -0
- package/src/scripts/workflow/push-dev-state.js +147 -0
- package/src/scripts/workflow/push-dev-state.js.map +1 -0
- package/src/scripts/workflow/review-report.d.ts +62 -1
- package/src/scripts/workflow/review-report.js +165 -34
- package/src/scripts/workflow/review-report.js.map +1 -1
- package/src/scripts/workflow/reviewer-briefing-builder.js +1 -0
- package/src/scripts/workflow/reviewer-briefing-builder.js.map +1 -1
- package/src/scripts/workflow/reviewer-verdict-gate.d.ts +10 -0
- package/src/scripts/workflow/reviewer-verdict-gate.js +17 -1
- package/src/scripts/workflow/reviewer-verdict-gate.js.map +1 -1
- 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
- package/src/scripts/wp-review-upsert-pr.js +9 -2
- package/src/scripts/wp-review-upsert-pr.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checklist-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/checklist-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangedFilesOptions, ChecklistDefinition, ChecklistResult, ChecklistReviewContext, DiffScope,\n RequiredChecklist, ReviewJsonService, reviewJsonPath,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { ChecklistDetector, ChecklistRoster } from './checklist-detector';\nimport { DiffBasis, DiffBasisResolver } from './diff-basis';\nimport { PrContextWriter } from './pr-context-writer';\n\n/** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */\nexport class ChecklistScanOptions {\n /**\n * false — `outstanding` is every applicable checklist (what `wp-review-upsert-pr` LISTS).\n * true — `outstanding` drops the ones already carrying a passing/overridden verdict, leaving only what\n * still owes review (what `wp-finish-upsert-pr` BLOCKS on).\n */\n filterAlreadyReviewed: boolean;\n /**\n * Which per-stage snapshot this scan should persist ('stage3-finish', …), or '' for \"do not write\n * pr-context.json here\".\n *\n * '' exists for stage ②, which cannot write yet: the context records `diffDir`, and the diff has not\n * been materialized at scan time. Letting the scan write anyway meant stage ② wrote the file twice,\n * ~60 lines apart, the first time with an empty diffDir. Now it writes once, after materializing.\n *\n * The DEFAULT is deliberately non-empty, so writing is what you get by NOT thinking about it. A\n * reviewer block that lost its diff command because nobody had written pr-context.json is a bug this\n * codebase has already shipped once (see PrContextWriter's docstring); opting out must be an explicit,\n * visible act by a caller that takes responsibility for writing it later — not an omission.\n */\n contextStage: string;\n\n constructor(filterAlreadyReviewed = false, contextStage = 'stage-scan') {\n this.filterAlreadyReviewed = filterAlreadyReviewed;\n this.contextStage = contextStage;\n }\n}\n\n/**\n * The answer to \"what review does this branch owe?\", in the X → N → Z terms the commands report:\n * X = `defined` — every checklist in pr-gate.checklists\n * N = `applicable` — those whose patterns matched (or that have no patterns, so always run)\n * Z = `outstanding` — of N, those still owing a verdict (only when filterAlreadyReviewed)\n * Data-only.\n */\nexport class ChecklistScan {\n defined: ChecklistDefinition[]; // X\n applicable: RequiredChecklist[]; // N\n reviewed: RequiredChecklist[]; // N − Z: already have a passing/warned/overridden review-<id>.json\n outstanding: RequiredChecklist[]; // Z (== applicable when not filtering)\n context: ChecklistReviewContext; // fork-point sha + pr-context.json path\n reviewPath: string; // the branch's review.json; verdict files sit beside it\n forkPoint: string; // '' when no fork point resolved\n // ALL X, matched or not, with why — what the PR comment publishes as its roster. Skipped checklists are\n // absent from `applicable` by construction, and recovering them downstream would mean a second\n // changed-file computation with different semantics (see the class comment).\n roster: ChecklistRoster;\n // Verdict files that exist but cannot be read as a verdict (e.g. still using the removed `success`).\n // Carried on the SCAN because wp-finish-upsert-pr refuses on missing reviewers before it ever parses\n // review.json — a complaint raised only in there would never reach the AI.\n formatErrors: string[];\n /**\n * The basis the matching ACTUALLY ran against. Carried out so a caller that materializes the diff\n * (stage ②) reuses the identical range instead of resolving its own — two independent resolutions is\n * exactly how the changed-file set and the printed `git diff` command came to disagree.\n */\n basis: DiffBasis;\n changedFiles: string[]; // the full changed-file set the matching ran against\n /**\n * The verdict files the scan ALREADY read, carried out rather than dropped.\n *\n * `outstanding` answers \"who still owes a verdict?\" but not \"why\" — and the two reasons demand opposite\n * actions from the reader: a reviewer that never ran must be SPAWNED, a reviewer that ran and REFUSED\n * must not be (it will refuse again; the finding has to be fixed first). Telling them apart means\n * resolving each checklist's verdict, and without the results here every caller either re-reads the same\n * files off disk — a second read that can disagree with this one — or merges the two cases into one\n * message, which is exactly the loop this field exists to break.\n */\n results: ChecklistResult[];\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n defined: ChecklistDefinition[],\n applicable: RequiredChecklist[],\n reviewed: RequiredChecklist[],\n outstanding: RequiredChecklist[],\n context: ChecklistReviewContext,\n reviewPath: string,\n forkPoint: string,\n roster: ChecklistRoster,\n formatErrors: string[],\n basis: DiffBasis = new DiffBasis(),\n changedFiles: string[] = [],\n // Defaulted so a caller that only cares about the X/N/Z counts (and every existing test construction)\n // stays a one-liner; the scanner itself always passes the real set.\n results: ChecklistResult[] = [],\n ) {\n this.defined = defined;\n this.applicable = applicable;\n this.reviewed = reviewed;\n this.outstanding = outstanding;\n this.context = context;\n this.reviewPath = reviewPath;\n this.forkPoint = forkPoint;\n this.roster = roster;\n this.formatErrors = formatErrors;\n this.basis = basis;\n this.changedFiles = changedFiles;\n this.results = results;\n }\n}\n\n/**\n * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and\n * `wp-finish-upsert-pr` (which blocks). They previously each assembled this from the same parts in slightly\n * different ways, and any divergence means the command that reports and the command that gates disagree.\n *\n * Two properties are deliberate and load-bearing:\n *\n * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which\n * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.\n * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.\n * 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that\n * diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and\n * silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files\n * would never fire and its reviewer would never be listed.\n * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set\n * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and\n * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing\n * caller cannot re-resolve and drift.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistScanner {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly checklistDetector: ChecklistDetector,\n private readonly diffScope: DiffScope,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly prContextWriter: PrContextWriter,\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n /**\n * `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load\n * config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array\n * `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and\n * reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of\n * here leaves this a function of its inputs rather than of the filesystem.\n */\n scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan {\n const featureName = this.aiBranchName.getFeatureName();\n const reviewPath = reviewJsonPath(repoRoot, featureName);\n // ONE basis for the file set, the reproduce command and any downstream materialization. The fork\n // point still comes from ForkPoint (never DiffScope.resolveBase) — DiffBasisResolver injects it.\n const basis = this.diffBasisResolver.resolve(repoRoot);\n const base = basis.base;\n // ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is\n // pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.\n const changedFiles = this.changedFiles(repoRoot, base);\n const roster = new ChecklistRoster(\n this.checklistDetector.roster(defined, changedFiles), changedFiles.length, base !== '');\n const applicable = this.checklistDetector.toRequired(this.checklistDetector.detect(defined, changedFiles));\n const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);\n const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);\n const owedIds = new Set(stillOwed.map((r: RequiredChecklist): string => r.id));\n const reviewed = applicable.filter((r: RequiredChecklist): boolean => !owedIds.has(r.id));\n return new ChecklistScan(\n defined,\n applicable,\n reviewed,\n opts.filterAlreadyReviewed ? stillOwed : applicable,\n opts.contextStage === ''\n ? this.prContextWriter.contextFor(repoRoot, featureName, basis)\n : this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles),\n reviewPath,\n base,\n roster,\n this.reviewJsonService.checklistFormatErrors(applicable, results),\n basis,\n changedFiles,\n results,\n );\n }\n\n /**\n * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default\n * options, both load-bearing:\n *\n * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants\n * to key on, which would silently shrink the set a reviewer is pointed at.\n * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that\n * deletes a migration, an auth check or a terraform rule changed exactly what a\n * checklist exists to catch, and under the default no checklist fires at all.\n */\n private changedFiles(repoRoot: string, base: string): string[] {\n if (base === '') return [];\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n // No head argument — see the class comment. This is what includes the working tree.\n return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"checklist-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/checklist-scanner.ts"],"names":[],"mappings":";;;;AAAA,0DAGiC;AACjC,yCAA2D;AAC3D,iEAAsD;AACtD,6DAA0E;AAC1E,6CAA4D;AAC5D,2DAAsD;AAEtD,uEAAuE;AACvE,MAAa,oBAAoB;IAC7B;;;;OAIG;IACH,qBAAqB,CAAU;IAC/B;;;;;;;;;;;;OAYG;IACH,YAAY,CAAS;IAErB,YAAY,qBAAqB,GAAG,KAAK,EAAE,YAAY,GAAG,YAAY;QAClE,IAAI,CAAC,qBAAqB,GAAG,qBAAqB,CAAC;QACnD,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;CACJ;AA1BD,oDA0BC;AAED;;;;;;GAMG;AACH,MAAa,aAAa;IACtB,OAAO,CAAwB,CAAM,IAAI;IACzC,UAAU,CAAsB,CAAK,IAAI;IACzC,QAAQ,CAAsB,CAAO,mEAAmE;IACxG,WAAW,CAAsB,CAAI,uCAAuC;IAC5E,OAAO,CAAyB,CAAK,wCAAwC;IAC7E,UAAU,CAAS,CAAkB,wDAAwD;IAC7F,SAAS,CAAS,CAAmB,iCAAiC;IACtE,wGAAwG;IACxG,+FAA+F;IAC/F,6EAA6E;IAC7E,MAAM,CAAkB;IACxB,qGAAqG;IACrG,qGAAqG;IACrG,2EAA2E;IAC3E,YAAY,CAAW;IACvB;;;;OAIG;IACH,KAAK,CAAY;IACjB,YAAY,CAAW,CAAc,qDAAqD;IAC1F;;;;;;;;;OASG;IACH,OAAO,CAAoB;IAC3B;;;;;;;;OAQG;IACH,cAAc,CAAsB;IAEpC,yDAAyD;IACzD,YACI,OAA8B,EAC9B,UAA+B,EAC/B,QAA6B,EAC7B,WAAgC,EAChC,OAA+B,EAC/B,UAAkB,EAClB,SAAiB,EACjB,MAAuB,EACvB,YAAsB,EACtB,QAAmB,IAAI,sBAAS,EAAE,EAClC,eAAyB,EAAE;IAC3B,sGAAsG;IACtG,oEAAoE;IACpE,UAA6B,EAAE,EAC/B,iBAAsC,EAAE;QAExC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;IACzC,CAAC;CACJ;AA7ED,sCA6EC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AAEI,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IAEJ;IACA;IACA;IACA;IACA;IACA;IANrB,YACqB,YAA0B,EAC1B,iBAAoC,EACpC,SAAoB,EACpB,iBAAoC,EACpC,eAAgC,EAChC,iBAAoC;QALpC,iBAAY,GAAZ,YAAY,CAAc;QAC1B,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,cAAS,GAAT,SAAS,CAAW;QACpB,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,oBAAe,GAAf,eAAe,CAAiB;QAChC,sBAAiB,GAAjB,iBAAiB,CAAmB;IACtD,CAAC;IAEJ;;;;;;OAMG;IACH,IAAI,CAAC,QAAgB,EAAE,OAA8B,EAAE,IAA0B;QAC7E,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;QACvD,MAAM,UAAU,GAAG,IAAA,6BAAc,EAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACzD,iGAAiG;QACjG,iGAAiG;QACjG,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACxB,qGAAqG;QACrG,oGAAoG;QACpG,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,oCAAe,CAC9B,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,YAAY,CAAC,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC,CAAC;QAC5F,MAAM,UAAU,GAAG,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;QAC3G,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,oBAAoB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QACpF,MAAM,SAAS,GAAG,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAChF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAoB,EAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/E,qGAAqG;QACrG,uGAAuG;QACvG,uBAAuB;QACvB,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAoB,EAAW,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1F,MAAM,cAAc,GAAG,IAAI,CAAC,iBAAiB,CAAC,sBAAsB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC1F,OAAO,IAAI,aAAa,CACpB,OAAO,EACP,UAAU,EACV,QAAQ,EACR,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,UAAU,EAClF,IAAI,CAAC,YAAY,KAAK,EAAE;YACpB,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,CAAC;YAC/D,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,EAChG,UAAU,EACV,IAAI,EACJ,MAAM,EACN,IAAI,CAAC,iBAAiB,CAAC,qBAAqB,CAAC,UAAU,EAAE,OAAO,CAAC,EACjE,KAAK,EACL,YAAY,EACZ,OAAO,EACP,cAAc,CACjB,CAAC;IACN,CAAC;IAED;;;;;;;;OAQG;IACK,QAAQ,CAAC,SAAuC,EAAE,cAA4C;QAClG,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAoB,EAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpF,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,CAAoB,EAAW,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACnF,CAAC;IAED;;;;;;;;;OASG;IACK,YAAY,CAAC,QAAgB,EAAE,IAAY;QAC/C,IAAI,IAAI,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,IAAI,kCAAmB,EAAE,CAAC;QACvC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,oFAAoF;QACpF,OAAO,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3E,CAAC;CACJ,CAAA;AAzFY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGF,mCAAY;QACP,sCAAiB;QACzB,wBAAS;QACD,8BAAiB;QACnB,mCAAe;QACb,gCAAiB;GAPhD,gBAAgB,CAyF5B","sourcesContent":["import {\n ChangedFilesOptions, ChecklistDefinition, ChecklistResult, ChecklistReviewContext, DiffScope,\n RequiredChecklist, ReviewJsonService, reviewJsonPath,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { ChecklistDetector, ChecklistRoster } from './checklist-detector';\nimport { DiffBasis, DiffBasisResolver } from './diff-basis';\nimport { PrContextWriter } from './pr-context-writer';\n\n/** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */\nexport class ChecklistScanOptions {\n /**\n * false — `outstanding` is every applicable checklist (what `wp-review-upsert-pr` LISTS).\n * true — `outstanding` drops the ones already carrying a passing/overridden verdict, leaving only what\n * still owes review (what `wp-finish-upsert-pr` BLOCKS on).\n */\n filterAlreadyReviewed: boolean;\n /**\n * Which per-stage snapshot this scan should persist ('stage3-finish', …), or '' for \"do not write\n * pr-context.json here\".\n *\n * '' exists for stage ②, which cannot write yet: the context records `diffDir`, and the diff has not\n * been materialized at scan time. Letting the scan write anyway meant stage ② wrote the file twice,\n * ~60 lines apart, the first time with an empty diffDir. Now it writes once, after materializing.\n *\n * The DEFAULT is deliberately non-empty, so writing is what you get by NOT thinking about it. A\n * reviewer block that lost its diff command because nobody had written pr-context.json is a bug this\n * codebase has already shipped once (see PrContextWriter's docstring); opting out must be an explicit,\n * visible act by a caller that takes responsibility for writing it later — not an omission.\n */\n contextStage: string;\n\n constructor(filterAlreadyReviewed = false, contextStage = 'stage-scan') {\n this.filterAlreadyReviewed = filterAlreadyReviewed;\n this.contextStage = contextStage;\n }\n}\n\n/**\n * The answer to \"what review does this branch owe?\", in the X → N → Z terms the commands report:\n * X = `defined` — every checklist in pr-gate.checklists\n * N = `applicable` — those whose patterns matched (or that have no patterns, so always run)\n * Z = `outstanding` — of N, those still owing a verdict (only when filterAlreadyReviewed)\n * Data-only.\n */\nexport class ChecklistScan {\n defined: ChecklistDefinition[]; // X\n applicable: RequiredChecklist[]; // N\n reviewed: RequiredChecklist[]; // N − Z: already have a passing/warned/overridden review-<id>.json\n outstanding: RequiredChecklist[]; // Z (== applicable when not filtering)\n context: ChecklistReviewContext; // fork-point sha + pr-context.json path\n reviewPath: string; // the branch's review.json; verdict files sit beside it\n forkPoint: string; // '' when no fork point resolved\n // ALL X, matched or not, with why — what the PR comment publishes as its roster. Skipped checklists are\n // absent from `applicable` by construction, and recovering them downstream would mean a second\n // changed-file computation with different semantics (see the class comment).\n roster: ChecklistRoster;\n // Verdict files that exist but cannot be read as a verdict (e.g. still using the removed `success`).\n // Carried on the SCAN because wp-finish-upsert-pr refuses on missing reviewers before it ever parses\n // review.json — a complaint raised only in there would never reach the AI.\n formatErrors: string[];\n /**\n * The basis the matching ACTUALLY ran against. Carried out so a caller that materializes the diff\n * (stage ②) reuses the identical range instead of resolving its own — two independent resolutions is\n * exactly how the changed-file set and the printed `git diff` command came to disagree.\n */\n basis: DiffBasis;\n changedFiles: string[]; // the full changed-file set the matching ran against\n /**\n * The verdict files the scan ALREADY read, carried out rather than dropped.\n *\n * `outstanding` answers \"who still owes a verdict?\" but not \"why\" — and the two reasons demand opposite\n * actions from the reader: a reviewer that never ran must be SPAWNED, a reviewer that ran and REFUSED\n * must not be (it will refuse again; the finding has to be fixed first). Telling them apart means\n * resolving each checklist's verdict, and without the results here every caller either re-reads the same\n * files off disk — a second read that can disagree with this one — or merges the two cases into one\n * message, which is exactly the loop this field exists to break.\n */\n results: ChecklistResult[];\n /**\n * The OPTIONAL (`required: false`) applicable checklists carrying no verdict file — i.e. nobody ran them.\n *\n * Deliberately NOT in `outstanding` when filtering: that is the exemption that makes `required: false`\n * mean something. Carried out as its own set rather than merely subtracted, because both readers need it\n * BY NAME and neither can recover it from what is left: stage ② offers exactly these to the human, and\n * the PR dashboard must publish them as \"not run\" rather than let a shorter roster imply everything\n * passed. An optional checklist that ran and went RED is absent from here and stays in `outstanding`.\n */\n optionalNotRun: RequiredChecklist[];\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n defined: ChecklistDefinition[],\n applicable: RequiredChecklist[],\n reviewed: RequiredChecklist[],\n outstanding: RequiredChecklist[],\n context: ChecklistReviewContext,\n reviewPath: string,\n forkPoint: string,\n roster: ChecklistRoster,\n formatErrors: string[],\n basis: DiffBasis = new DiffBasis(),\n changedFiles: string[] = [],\n // Defaulted so a caller that only cares about the X/N/Z counts (and every existing test construction)\n // stays a one-liner; the scanner itself always passes the real set.\n results: ChecklistResult[] = [],\n optionalNotRun: RequiredChecklist[] = [],\n ) {\n this.defined = defined;\n this.applicable = applicable;\n this.reviewed = reviewed;\n this.outstanding = outstanding;\n this.context = context;\n this.reviewPath = reviewPath;\n this.forkPoint = forkPoint;\n this.roster = roster;\n this.formatErrors = formatErrors;\n this.basis = basis;\n this.changedFiles = changedFiles;\n this.results = results;\n this.optionalNotRun = optionalNotRun;\n }\n}\n\n/**\n * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and\n * `wp-finish-upsert-pr` (which blocks). They previously each assembled this from the same parts in slightly\n * different ways, and any divergence means the command that reports and the command that gates disagree.\n *\n * Two properties are deliberate and load-bearing:\n *\n * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which\n * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.\n * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.\n * 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that\n * diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and\n * silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files\n * would never fire and its reviewer would never be listed.\n * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set\n * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and\n * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing\n * caller cannot re-resolve and drift.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistScanner {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly checklistDetector: ChecklistDetector,\n private readonly diffScope: DiffScope,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly prContextWriter: PrContextWriter,\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n /**\n * `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load\n * config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array\n * `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and\n * reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of\n * here leaves this a function of its inputs rather than of the filesystem.\n */\n scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan {\n const featureName = this.aiBranchName.getFeatureName();\n const reviewPath = reviewJsonPath(repoRoot, featureName);\n // ONE basis for the file set, the reproduce command and any downstream materialization. The fork\n // point still comes from ForkPoint (never DiffScope.resolveBase) — DiffBasisResolver injects it.\n const basis = this.diffBasisResolver.resolve(repoRoot);\n const base = basis.base;\n // ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is\n // pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.\n const changedFiles = this.changedFiles(repoRoot, base);\n const roster = new ChecklistRoster(\n this.checklistDetector.roster(defined, changedFiles), changedFiles.length, base !== '');\n const applicable = this.checklistDetector.toRequired(this.checklistDetector.detect(defined, changedFiles));\n const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);\n const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);\n const owedIds = new Set(stillOwed.map((r: RequiredChecklist): string => r.id));\n // NOT `!owedIds.has(...)`-with-the-optional-exemption-folded-in: an optional checklist nobody ran is\n // neither reviewed nor blocking, and calling it \"reviewed\" would put a ✓ on the dashboard for a review\n // that never happened.\n const reviewed = applicable.filter((r: RequiredChecklist): boolean => !owedIds.has(r.id));\n const optionalNotRun = this.reviewJsonService.optionalWithoutVerdict(applicable, results);\n return new ChecklistScan(\n defined,\n applicable,\n reviewed,\n opts.filterAlreadyReviewed ? this.blocking(stillOwed, optionalNotRun) : applicable,\n opts.contextStage === ''\n ? this.prContextWriter.contextFor(repoRoot, featureName, basis)\n : this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles),\n reviewPath,\n base,\n roster,\n this.reviewJsonService.checklistFormatErrors(applicable, results),\n basis,\n changedFiles,\n results,\n optionalNotRun,\n );\n }\n\n /**\n * What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional\n * checklists nobody ran.\n *\n * The subtraction happens HERE — in the one place `outstanding` is computed — and not in the gate, so\n * there is a single answer to \"does this branch owe review?\". When the gate did its own filtering, the\n * command that lists and the command that blocks were two implementations of the same question, which is\n * exactly the divergence this class's docstring exists to prevent.\n */\n private blocking(stillOwed: readonly RequiredChecklist[], optionalNotRun: readonly RequiredChecklist[]): RequiredChecklist[] {\n const skipped = new Set(optionalNotRun.map((r: RequiredChecklist): string => r.id));\n return stillOwed.filter((r: RequiredChecklist): boolean => !skipped.has(r.id));\n }\n\n /**\n * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default\n * options, both load-bearing:\n *\n * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants\n * to key on, which would silently shrink the set a reviewer is pointed at.\n * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that\n * deletes a migration, an auth check or a terraform rule changed exactly what a\n * checklist exists to catch, and under the default no checklist fires at all.\n */\n private changedFiles(repoRoot: string, base: string): string[] {\n if (base === '') return [];\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n // No head argument — see the class comment. This is what includes the working tree.\n return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);\n }\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { DevDeployConfig } from '@webpieces/rules-config';
|
|
2
|
+
import { GitExec } from './git-exec';
|
|
3
|
+
/**
|
|
4
|
+
* One live copy in the dev namespace: the remote ref and the sha it points at. Data-only per CLAUDE.md —
|
|
5
|
+
* `--list` prints it and the clobber guard compares against `sha`, and neither should be re-parsing
|
|
6
|
+
* `ls-remote` output.
|
|
7
|
+
*/
|
|
8
|
+
export declare class DevCopy {
|
|
9
|
+
/** The full ref name WITHOUT `refs/heads/`, e.g. `dev-include/dean/ONE-2275`. */
|
|
10
|
+
ref: string;
|
|
11
|
+
sha: string;
|
|
12
|
+
/** The feature branch this is a copy OF — `ref` with the namespace prefix stripped. */
|
|
13
|
+
sourceBranch: string;
|
|
14
|
+
constructor(ref: string, sha: string, sourceBranch: string);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ref derivation and preconditions for the dev-deploy flow — the half of `wp-push-dev` that decides WHICH
|
|
18
|
+
* refs are in play, kept out of the commands so both `wp-push-dev` and `wp-finish-push-dev` answer those
|
|
19
|
+
* questions identically.
|
|
20
|
+
*
|
|
21
|
+
* THE INVARIANT THIS FILE EXISTS TO PROTECT: the feature branch is the PR head, so it must never acquire
|
|
22
|
+
* another developer's commits — landing that PR would ship their unreviewed work to production. Every
|
|
23
|
+
* write this flow performs therefore targets either the `<namespace>/<feature>` copy or a local throwaway
|
|
24
|
+
* branch, and NOTHING here ever moves `<feature>` itself. That is why a conflict resolution needs the copy
|
|
25
|
+
* to live in: it is the only place the resolution can be recorded without contaminating the PR.
|
|
26
|
+
*/
|
|
27
|
+
export declare class DevDeployRefs {
|
|
28
|
+
private readonly gitExec;
|
|
29
|
+
constructor(gitExec: GitExec);
|
|
30
|
+
/** The `pr-gate.devDeploy` block — `dev-include` / `dev` unless the consumer overrode it. */
|
|
31
|
+
config(repoRoot: string): DevDeployConfig;
|
|
32
|
+
/** The checked-out branch name, or `'HEAD'` on a detached HEAD (which {@link assertPushable} refuses). */
|
|
33
|
+
currentBranch(repoRoot: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Refuse every branch that must never be the SOURCE of a dev copy, before anything is fetched or
|
|
36
|
+
* pushed.
|
|
37
|
+
*
|
|
38
|
+
* `main` and the composed dev branch are refused for the same reason from opposite ends: `main` is the
|
|
39
|
+
* trunk (whatever is on it already deploys, so copying it is meaningless), and the dev branch is a
|
|
40
|
+
* BUILD ARTIFACT that CI recomputes from `origin/main` on every run — copying it would publish a
|
|
41
|
+
* composition of everyone's work under one developer's name, and it would be wiped on the next CI run
|
|
42
|
+
* anyway. A branch already inside the namespace is refused because publishing a copy of a copy nests
|
|
43
|
+
* the prefix (`dev-include/dev-include/…`) and nothing would ever clean it up.
|
|
44
|
+
*/
|
|
45
|
+
assertPushable(cfg: DevDeployConfig, branch: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Every live copy in the namespace, sorted by ref name.
|
|
48
|
+
*
|
|
49
|
+
* The sort is not cosmetic: `--resolve` with no argument queues these in exactly this order, and the
|
|
50
|
+
* consumer's CI composes `dev` in the same order, so what a developer resolves locally is the
|
|
51
|
+
* composition CI will actually build. A different order can produce a different (still valid) merge
|
|
52
|
+
* result, which would make the local resolution a guess about CI rather than a rehearsal of it.
|
|
53
|
+
*/
|
|
54
|
+
liveCopies(repoRoot: string, cfg: DevDeployConfig): DevCopy[];
|
|
55
|
+
/** The remote sha of one copy, or '' when that copy does not exist yet. */
|
|
56
|
+
remoteSha(repoRoot: string, ref: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Fetch the named copy refs into `refs/remotes/origin/<ref>` so the local merges below run against
|
|
59
|
+
* fresh state. A no-op for an empty list, and a hard failure otherwise: a resolve queued against
|
|
60
|
+
* stale refs rehearses a composition CI will not build.
|
|
61
|
+
*/
|
|
62
|
+
fetchCopies(repoRoot: string, refs: string[]): void;
|
|
63
|
+
/** How many commits `origin/<ref>` has that `HEAD` does not — the clobber signal. 0 = safe to publish. */
|
|
64
|
+
commitsOnlyOnRemote(repoRoot: string, ref: string): number;
|
|
65
|
+
private refusal;
|
|
66
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DevDeployRefs = exports.DevCopy = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
6
|
+
const inversify_1 = require("inversify");
|
|
7
|
+
const git_exec_1 = require("./git-exec");
|
|
8
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
9
|
+
const MAIN = 'main';
|
|
10
|
+
const DETACHED = 'HEAD';
|
|
11
|
+
/**
|
|
12
|
+
* One live copy in the dev namespace: the remote ref and the sha it points at. Data-only per CLAUDE.md —
|
|
13
|
+
* `--list` prints it and the clobber guard compares against `sha`, and neither should be re-parsing
|
|
14
|
+
* `ls-remote` output.
|
|
15
|
+
*/
|
|
16
|
+
class DevCopy {
|
|
17
|
+
/** The full ref name WITHOUT `refs/heads/`, e.g. `dev-include/dean/ONE-2275`. */
|
|
18
|
+
ref;
|
|
19
|
+
sha;
|
|
20
|
+
/** The feature branch this is a copy OF — `ref` with the namespace prefix stripped. */
|
|
21
|
+
sourceBranch;
|
|
22
|
+
constructor(ref, sha, sourceBranch) {
|
|
23
|
+
this.ref = ref;
|
|
24
|
+
this.sha = sha;
|
|
25
|
+
this.sourceBranch = sourceBranch;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.DevCopy = DevCopy;
|
|
29
|
+
/**
|
|
30
|
+
* Ref derivation and preconditions for the dev-deploy flow — the half of `wp-push-dev` that decides WHICH
|
|
31
|
+
* refs are in play, kept out of the commands so both `wp-push-dev` and `wp-finish-push-dev` answer those
|
|
32
|
+
* questions identically.
|
|
33
|
+
*
|
|
34
|
+
* THE INVARIANT THIS FILE EXISTS TO PROTECT: the feature branch is the PR head, so it must never acquire
|
|
35
|
+
* another developer's commits — landing that PR would ship their unreviewed work to production. Every
|
|
36
|
+
* write this flow performs therefore targets either the `<namespace>/<feature>` copy or a local throwaway
|
|
37
|
+
* branch, and NOTHING here ever moves `<feature>` itself. That is why a conflict resolution needs the copy
|
|
38
|
+
* to live in: it is the only place the resolution can be recorded without contaminating the PR.
|
|
39
|
+
*/
|
|
40
|
+
let DevDeployRefs = class DevDeployRefs {
|
|
41
|
+
gitExec;
|
|
42
|
+
constructor(gitExec) {
|
|
43
|
+
this.gitExec = gitExec;
|
|
44
|
+
}
|
|
45
|
+
/** The `pr-gate.devDeploy` block — `dev-include` / `dev` unless the consumer overrode it. */
|
|
46
|
+
config(repoRoot) {
|
|
47
|
+
return (0, rules_config_1.loadAndValidate)(repoRoot).prGate.devDeploy;
|
|
48
|
+
}
|
|
49
|
+
/** The checked-out branch name, or `'HEAD'` on a detached HEAD (which {@link assertPushable} refuses). */
|
|
50
|
+
currentBranch(repoRoot) {
|
|
51
|
+
return this.gitExec.gitQuery(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot, 'Failed to read the current branch (git rev-parse --abbrev-ref HEAD).');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Refuse every branch that must never be the SOURCE of a dev copy, before anything is fetched or
|
|
55
|
+
* pushed.
|
|
56
|
+
*
|
|
57
|
+
* `main` and the composed dev branch are refused for the same reason from opposite ends: `main` is the
|
|
58
|
+
* trunk (whatever is on it already deploys, so copying it is meaningless), and the dev branch is a
|
|
59
|
+
* BUILD ARTIFACT that CI recomputes from `origin/main` on every run — copying it would publish a
|
|
60
|
+
* composition of everyone's work under one developer's name, and it would be wiped on the next CI run
|
|
61
|
+
* anyway. A branch already inside the namespace is refused because publishing a copy of a copy nests
|
|
62
|
+
* the prefix (`dev-include/dev-include/…`) and nothing would ever clean it up.
|
|
63
|
+
*/
|
|
64
|
+
assertPushable(cfg, branch) {
|
|
65
|
+
if (branch === DETACHED || branch === '') {
|
|
66
|
+
throw new rules_config_1.CliExitError(2, this.refusal('Detached HEAD', 'There is no branch name to derive the dev copy from. Check out your feature branch first:\n'
|
|
67
|
+
+ ' git checkout <your-feature-branch>\n'));
|
|
68
|
+
}
|
|
69
|
+
if (branch === MAIN) {
|
|
70
|
+
throw new rules_config_1.CliExitError(2, this.refusal(`Refusing to publish a dev copy of \`${MAIN}\``, `\`${MAIN}\` is the trunk — whatever is on it already deploys, so a dev copy of it means nothing.\n`
|
|
71
|
+
+ 'Check out the feature branch you actually want on the dev server and re-run.\n'));
|
|
72
|
+
}
|
|
73
|
+
if (branch === cfg.devBranch) {
|
|
74
|
+
throw new rules_config_1.CliExitError(2, this.refusal(`Refusing to publish a dev copy of \`${cfg.devBranch}\``, `\`${cfg.devBranch}\` is a BUILD ARTIFACT, not a source branch: your CI resets it to origin/${MAIN},\n`
|
|
75
|
+
+ `merges every \`${cfg.copyRefGlob()}\` ref into it, and force-pushes — every run. Copying it would\n`
|
|
76
|
+
+ "publish a composition of everybody's work under your branch's name, and the next CI run would\n"
|
|
77
|
+
+ 'wipe it. Check out your own feature branch and re-run.\n'));
|
|
78
|
+
}
|
|
79
|
+
if (branch === cfg.branchNamespace || branch.startsWith(`${cfg.branchNamespace}/`)) {
|
|
80
|
+
throw new rules_config_1.CliExitError(2, this.refusal(`Refusing to publish a dev copy of a dev copy (\`${branch}\`)`, `\`${cfg.branchNamespace}/\` holds the disposable copies this command WRITES; you are standing on one.\n`
|
|
81
|
+
+ `Publishing from here would nest the prefix (\`${cfg.branchNamespace}/${branch}\`) and nothing ever\n`
|
|
82
|
+
+ 'cleans that up. Check out the real feature branch and re-run.\n'));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Every live copy in the namespace, sorted by ref name.
|
|
87
|
+
*
|
|
88
|
+
* The sort is not cosmetic: `--resolve` with no argument queues these in exactly this order, and the
|
|
89
|
+
* consumer's CI composes `dev` in the same order, so what a developer resolves locally is the
|
|
90
|
+
* composition CI will actually build. A different order can produce a different (still valid) merge
|
|
91
|
+
* result, which would make the local resolution a guess about CI rather than a rehearsal of it.
|
|
92
|
+
*/
|
|
93
|
+
liveCopies(repoRoot, cfg) {
|
|
94
|
+
const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', cfg.copyRefGlob()], repoRoot, `Failed to list the dev copies (git ls-remote --heads origin '${cfg.copyRefGlob()}').`);
|
|
95
|
+
const copies = [];
|
|
96
|
+
for (const line of out.split('\n')) {
|
|
97
|
+
if (line.trim() === '')
|
|
98
|
+
continue;
|
|
99
|
+
const fields = line.split(/\s+/);
|
|
100
|
+
const sha = fields[0];
|
|
101
|
+
const ref = fields[1];
|
|
102
|
+
if (sha === undefined || ref === undefined || !ref.startsWith('refs/heads/'))
|
|
103
|
+
continue;
|
|
104
|
+
const shortRef = ref.slice('refs/heads/'.length);
|
|
105
|
+
copies.push(new DevCopy(shortRef, sha, shortRef.slice(cfg.branchNamespace.length + 1)));
|
|
106
|
+
}
|
|
107
|
+
return copies.sort((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));
|
|
108
|
+
}
|
|
109
|
+
/** The remote sha of one copy, or '' when that copy does not exist yet. */
|
|
110
|
+
remoteSha(repoRoot, ref) {
|
|
111
|
+
const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', ref], repoRoot, `Failed to look up origin/${ref} (git ls-remote).`);
|
|
112
|
+
const line = out.split('\n').find((l) => l.trim() !== '');
|
|
113
|
+
if (line === undefined)
|
|
114
|
+
return '';
|
|
115
|
+
return line.split(/\s+/)[0] ?? '';
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Fetch the named copy refs into `refs/remotes/origin/<ref>` so the local merges below run against
|
|
119
|
+
* fresh state. A no-op for an empty list, and a hard failure otherwise: a resolve queued against
|
|
120
|
+
* stale refs rehearses a composition CI will not build.
|
|
121
|
+
*/
|
|
122
|
+
fetchCopies(repoRoot, refs) {
|
|
123
|
+
if (refs.length === 0)
|
|
124
|
+
return;
|
|
125
|
+
const specs = refs.map((r) => `+refs/heads/${r}:refs/remotes/origin/${r}`);
|
|
126
|
+
this.gitExec.runGitChecked(['-C', repoRoot, 'fetch', 'origin', ...specs], 'Failed to fetch the dev copy refs from origin');
|
|
127
|
+
}
|
|
128
|
+
/** How many commits `origin/<ref>` has that `HEAD` does not — the clobber signal. 0 = safe to publish. */
|
|
129
|
+
commitsOnlyOnRemote(repoRoot, ref) {
|
|
130
|
+
const out = this.gitExec.gitQuery(['rev-list', '--count', `HEAD..refs/remotes/origin/${ref}`], repoRoot, `Failed to compare HEAD against origin/${ref} (git rev-list --count).`);
|
|
131
|
+
const count = Number(out);
|
|
132
|
+
return Number.isInteger(count) ? count : 0;
|
|
133
|
+
}
|
|
134
|
+
refusal(headline, body) {
|
|
135
|
+
return '\n' + SEP + `⛔ ${headline}\n` + SEP + '\n' + body + `\nRun \`${rules_config_1.WP_PUSH_DEV} --help\` for the full command.\n`;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
exports.DevDeployRefs = DevDeployRefs;
|
|
139
|
+
exports.DevDeployRefs = DevDeployRefs = tslib_1.__decorate([
|
|
140
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
141
|
+
tslib_1.__metadata("design:paramtypes", [git_exec_1.GitExec])
|
|
142
|
+
], DevDeployRefs);
|
|
143
|
+
//# sourceMappingURL=dev-deploy-refs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-deploy-refs.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/dev-deploy-refs.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, DevDeployConfig, loadAndValidate, WP_PUSH_DEV } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from './git-exec';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\nconst MAIN = 'main';\nconst DETACHED = 'HEAD';\n\n/**\n * One live copy in the dev namespace: the remote ref and the sha it points at. Data-only per CLAUDE.md —\n * `--list` prints it and the clobber guard compares against `sha`, and neither should be re-parsing\n * `ls-remote` output.\n */\nexport class DevCopy {\n /** The full ref name WITHOUT `refs/heads/`, e.g. `dev-include/dean/ONE-2275`. */\n ref: string;\n sha: string;\n /** The feature branch this is a copy OF — `ref` with the namespace prefix stripped. */\n sourceBranch: string;\n\n constructor(ref: string, sha: string, sourceBranch: string) {\n this.ref = ref;\n this.sha = sha;\n this.sourceBranch = sourceBranch;\n }\n}\n\n/**\n * Ref derivation and preconditions for the dev-deploy flow — the half of `wp-push-dev` that decides WHICH\n * refs are in play, kept out of the commands so both `wp-push-dev` and `wp-finish-push-dev` answer those\n * questions identically.\n *\n * THE INVARIANT THIS FILE EXISTS TO PROTECT: the feature branch is the PR head, so it must never acquire\n * another developer's commits — landing that PR would ship their unreviewed work to production. Every\n * write this flow performs therefore targets either the `<namespace>/<feature>` copy or a local throwaway\n * branch, and NOTHING here ever moves `<feature>` itself. That is why a conflict resolution needs the copy\n * to live in: it is the only place the resolution can be recorded without contaminating the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DevDeployRefs {\n constructor(private readonly gitExec: GitExec) {}\n\n /** The `pr-gate.devDeploy` block — `dev-include` / `dev` unless the consumer overrode it. */\n config(repoRoot: string): DevDeployConfig {\n return loadAndValidate(repoRoot).prGate.devDeploy;\n }\n\n /** The checked-out branch name, or `'HEAD'` on a detached HEAD (which {@link assertPushable} refuses). */\n currentBranch(repoRoot: string): string {\n return this.gitExec.gitQuery(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot,\n 'Failed to read the current branch (git rev-parse --abbrev-ref HEAD).');\n }\n\n /**\n * Refuse every branch that must never be the SOURCE of a dev copy, before anything is fetched or\n * pushed.\n *\n * `main` and the composed dev branch are refused for the same reason from opposite ends: `main` is the\n * trunk (whatever is on it already deploys, so copying it is meaningless), and the dev branch is a\n * BUILD ARTIFACT that CI recomputes from `origin/main` on every run — copying it would publish a\n * composition of everyone's work under one developer's name, and it would be wiped on the next CI run\n * anyway. A branch already inside the namespace is refused because publishing a copy of a copy nests\n * the prefix (`dev-include/dev-include/…`) and nothing would ever clean it up.\n */\n assertPushable(cfg: DevDeployConfig, branch: string): void {\n if (branch === DETACHED || branch === '') {\n throw new CliExitError(2, this.refusal(\n 'Detached HEAD',\n 'There is no branch name to derive the dev copy from. Check out your feature branch first:\\n'\n + ' git checkout <your-feature-branch>\\n'));\n }\n if (branch === MAIN) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of \\`${MAIN}\\``,\n `\\`${MAIN}\\` is the trunk — whatever is on it already deploys, so a dev copy of it means nothing.\\n`\n + 'Check out the feature branch you actually want on the dev server and re-run.\\n'));\n }\n if (branch === cfg.devBranch) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of \\`${cfg.devBranch}\\``,\n `\\`${cfg.devBranch}\\` is a BUILD ARTIFACT, not a source branch: your CI resets it to origin/${MAIN},\\n`\n + `merges every \\`${cfg.copyRefGlob()}\\` ref into it, and force-pushes — every run. Copying it would\\n`\n + \"publish a composition of everybody's work under your branch's name, and the next CI run would\\n\"\n + 'wipe it. Check out your own feature branch and re-run.\\n'));\n }\n if (branch === cfg.branchNamespace || branch.startsWith(`${cfg.branchNamespace}/`)) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of a dev copy (\\`${branch}\\`)`,\n `\\`${cfg.branchNamespace}/\\` holds the disposable copies this command WRITES; you are standing on one.\\n`\n + `Publishing from here would nest the prefix (\\`${cfg.branchNamespace}/${branch}\\`) and nothing ever\\n`\n + 'cleans that up. Check out the real feature branch and re-run.\\n'));\n }\n }\n\n /**\n * Every live copy in the namespace, sorted by ref name.\n *\n * The sort is not cosmetic: `--resolve` with no argument queues these in exactly this order, and the\n * consumer's CI composes `dev` in the same order, so what a developer resolves locally is the\n * composition CI will actually build. A different order can produce a different (still valid) merge\n * result, which would make the local resolution a guess about CI rather than a rehearsal of it.\n */\n liveCopies(repoRoot: string, cfg: DevDeployConfig): DevCopy[] {\n const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', cfg.copyRefGlob()], repoRoot,\n `Failed to list the dev copies (git ls-remote --heads origin '${cfg.copyRefGlob()}').`);\n const copies: DevCopy[] = [];\n for (const line of out.split('\\n')) {\n if (line.trim() === '') continue;\n const fields = line.split(/\\s+/);\n const sha = fields[0];\n const ref = fields[1];\n if (sha === undefined || ref === undefined || !ref.startsWith('refs/heads/')) continue;\n const shortRef = ref.slice('refs/heads/'.length);\n copies.push(new DevCopy(shortRef, sha, shortRef.slice(cfg.branchNamespace.length + 1)));\n }\n return copies.sort((a: DevCopy, b: DevCopy): number => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));\n }\n\n /** The remote sha of one copy, or '' when that copy does not exist yet. */\n remoteSha(repoRoot: string, ref: string): string {\n const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', ref], repoRoot,\n `Failed to look up origin/${ref} (git ls-remote).`);\n const line = out.split('\\n').find((l: string): boolean => l.trim() !== '');\n if (line === undefined) return '';\n return line.split(/\\s+/)[0] ?? '';\n }\n\n /**\n * Fetch the named copy refs into `refs/remotes/origin/<ref>` so the local merges below run against\n * fresh state. A no-op for an empty list, and a hard failure otherwise: a resolve queued against\n * stale refs rehearses a composition CI will not build.\n */\n fetchCopies(repoRoot: string, refs: string[]): void {\n if (refs.length === 0) return;\n const specs = refs.map((r: string): string => `+refs/heads/${r}:refs/remotes/origin/${r}`);\n this.gitExec.runGitChecked(['-C', repoRoot, 'fetch', 'origin', ...specs],\n 'Failed to fetch the dev copy refs from origin');\n }\n\n /** How many commits `origin/<ref>` has that `HEAD` does not — the clobber signal. 0 = safe to publish. */\n commitsOnlyOnRemote(repoRoot: string, ref: string): number {\n const out = this.gitExec.gitQuery(['rev-list', '--count', `HEAD..refs/remotes/origin/${ref}`], repoRoot,\n `Failed to compare HEAD against origin/${ref} (git rev-list --count).`);\n const count = Number(out);\n return Number.isInteger(count) ? count : 0;\n }\n\n private refusal(headline: string, body: string): string {\n return '\\n' + SEP + `⛔ ${headline}\\n` + SEP + '\\n' + body + `\\nRun \\`${WP_PUSH_DEV} --help\\` for the full command.\\n`;\n }\n}\n"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { DevDeployConfig } from '@webpieces/rules-config';
|
|
2
|
+
import { GitExec } from './git-exec';
|
|
3
|
+
import { DevDeployRefs } from './dev-deploy-refs';
|
|
4
|
+
import { PushDevState, PushDevStateStore } from './push-dev-state';
|
|
5
|
+
/**
|
|
6
|
+
* The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.
|
|
7
|
+
*
|
|
8
|
+
* ONE class owns every git WRITE in the resolve flow, and that is a hard requirement rather than tidiness:
|
|
9
|
+
* `redirect-how-to-merge-main` blocks `git merge` in every form INCLUDING `--continue`, and
|
|
10
|
+
* `pr-creation-or-push-guard` blocks every `git push`. So an AI cannot be told "now run `git commit`" or
|
|
11
|
+
* "`git merge --continue`" — those instructions are unrunnable by the only party reading them. The command
|
|
12
|
+
* owns the merges, the commit and the push (as child processes the hooks never see); the AI's entire job is
|
|
13
|
+
* editing the conflicted files and `git add`-ing them, neither of which is blocked.
|
|
14
|
+
*/
|
|
15
|
+
export declare class DevResolveRunner {
|
|
16
|
+
private readonly gitExec;
|
|
17
|
+
private readonly refs;
|
|
18
|
+
private readonly store;
|
|
19
|
+
constructor(gitExec: GitExec, refs: DevDeployRefs, store: PushDevStateStore);
|
|
20
|
+
/**
|
|
21
|
+
* Start a resolve: park on a throwaway branch at the current copy, then drain the queue.
|
|
22
|
+
*
|
|
23
|
+
* The checkout is `-B <tmp> <base>` and NEVER the feature branch, which is the cardinal constraint made
|
|
24
|
+
* mechanical — every merge below lands on a branch that is deleted at the end, so no other developer's
|
|
25
|
+
* commits can reach the PR head even if the resolve is abandoned halfway.
|
|
26
|
+
*
|
|
27
|
+
* `base` differs between the two callers and the difference IS the semantics. `--resolve` starts at the
|
|
28
|
+
* published copy and merges the other copies in (compose what CI will build). `--rebase-resolution`
|
|
29
|
+
* starts at the LOCAL HEAD and merges the published copy in (replay an existing resolution on top of
|
|
30
|
+
* the new commits, instead of force-pushing over it).
|
|
31
|
+
*
|
|
32
|
+
* `fetchRefs` is passed separately from the queue because `--rebase-resolution` merges a ref that is
|
|
33
|
+
* also its push destination, so "everything to fetch" and "everything to merge" are not the same list.
|
|
34
|
+
*/
|
|
35
|
+
start(repoRoot: string, state: PushDevState, base: string, fetchRefs: string[]): void;
|
|
36
|
+
/**
|
|
37
|
+
* Resume after the human/AI has resolved the halted merge: commit it, then keep draining.
|
|
38
|
+
*
|
|
39
|
+
* REFETCHES the remaining queue first. A resolution session can run long, and queuing against refs
|
|
40
|
+
* fetched an hour ago rehearses a composition CI will not build — the one freshness gap the sibling
|
|
41
|
+
* update flow is still known to have (only its stage ① ever fetches).
|
|
42
|
+
*/
|
|
43
|
+
resume(repoRoot: string, state: PushDevState): void;
|
|
44
|
+
/**
|
|
45
|
+
* Throw the whole resolve away: undo any halted merge, go back to the original branch, delete the
|
|
46
|
+
* throwaway branch and the state file. Nothing published, nothing on the feature branch.
|
|
47
|
+
*/
|
|
48
|
+
abort(repoRoot: string, state: PushDevState): void;
|
|
49
|
+
/**
|
|
50
|
+
* Merge queued refs until one conflicts (HALT, hand the tree over) or the queue empties (PUBLISH).
|
|
51
|
+
*
|
|
52
|
+
* A LATER ref can conflict too, which is why this is a loop resumed from a state file rather than a
|
|
53
|
+
* single pass: `wp-finish-push-dev` can legitimately stop again on the next ref in the queue.
|
|
54
|
+
*/
|
|
55
|
+
private drain;
|
|
56
|
+
/** Stop with the conflicted files listed and the ONE command that continues. Exit 2 = "your turn". */
|
|
57
|
+
private halt;
|
|
58
|
+
/** Queue empty: force-push the composed result over the copy, then restore the original checkout. */
|
|
59
|
+
private publish;
|
|
60
|
+
/** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */
|
|
61
|
+
private unmergedPaths;
|
|
62
|
+
private assertNoUnmergedPaths;
|
|
63
|
+
/**
|
|
64
|
+
* The name of the throwaway branch for a resolve of `branch`.
|
|
65
|
+
*
|
|
66
|
+
* Suffixed rather than prefixed, matching the existing `<feature>Squash` / `<feature>PreMerge<n>`
|
|
67
|
+
* convention — and deliberately NOT inside the dev namespace, which holds REMOTE copies only.
|
|
68
|
+
*/
|
|
69
|
+
tmpBranchFor(branch: string): string;
|
|
70
|
+
/** The banner `--resolve` prints before the first merge, so the queue is visible before it halts. */
|
|
71
|
+
announce(cfg: DevDeployConfig, state: PushDevState): string;
|
|
72
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DevResolveRunner = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
6
|
+
const inversify_1 = require("inversify");
|
|
7
|
+
const git_exec_1 = require("./git-exec");
|
|
8
|
+
const dev_deploy_refs_1 = require("./dev-deploy-refs");
|
|
9
|
+
const push_dev_state_1 = require("./push-dev-state");
|
|
10
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
11
|
+
/**
|
|
12
|
+
* The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.
|
|
13
|
+
*
|
|
14
|
+
* ONE class owns every git WRITE in the resolve flow, and that is a hard requirement rather than tidiness:
|
|
15
|
+
* `redirect-how-to-merge-main` blocks `git merge` in every form INCLUDING `--continue`, and
|
|
16
|
+
* `pr-creation-or-push-guard` blocks every `git push`. So an AI cannot be told "now run `git commit`" or
|
|
17
|
+
* "`git merge --continue`" — those instructions are unrunnable by the only party reading them. The command
|
|
18
|
+
* owns the merges, the commit and the push (as child processes the hooks never see); the AI's entire job is
|
|
19
|
+
* editing the conflicted files and `git add`-ing them, neither of which is blocked.
|
|
20
|
+
*/
|
|
21
|
+
let DevResolveRunner = class DevResolveRunner {
|
|
22
|
+
gitExec;
|
|
23
|
+
refs;
|
|
24
|
+
store;
|
|
25
|
+
constructor(gitExec, refs, store) {
|
|
26
|
+
this.gitExec = gitExec;
|
|
27
|
+
this.refs = refs;
|
|
28
|
+
this.store = store;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Start a resolve: park on a throwaway branch at the current copy, then drain the queue.
|
|
32
|
+
*
|
|
33
|
+
* The checkout is `-B <tmp> <base>` and NEVER the feature branch, which is the cardinal constraint made
|
|
34
|
+
* mechanical — every merge below lands on a branch that is deleted at the end, so no other developer's
|
|
35
|
+
* commits can reach the PR head even if the resolve is abandoned halfway.
|
|
36
|
+
*
|
|
37
|
+
* `base` differs between the two callers and the difference IS the semantics. `--resolve` starts at the
|
|
38
|
+
* published copy and merges the other copies in (compose what CI will build). `--rebase-resolution`
|
|
39
|
+
* starts at the LOCAL HEAD and merges the published copy in (replay an existing resolution on top of
|
|
40
|
+
* the new commits, instead of force-pushing over it).
|
|
41
|
+
*
|
|
42
|
+
* `fetchRefs` is passed separately from the queue because `--rebase-resolution` merges a ref that is
|
|
43
|
+
* also its push destination, so "everything to fetch" and "everything to merge" are not the same list.
|
|
44
|
+
*/
|
|
45
|
+
start(repoRoot, state, base, fetchRefs) {
|
|
46
|
+
this.refs.fetchCopies(repoRoot, fetchRefs);
|
|
47
|
+
this.store.write(repoRoot, state);
|
|
48
|
+
this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', '-B', state.tmpBranch, base], `Failed to create the throwaway resolve branch ${state.tmpBranch}`);
|
|
49
|
+
this.drain(repoRoot, state);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resume after the human/AI has resolved the halted merge: commit it, then keep draining.
|
|
53
|
+
*
|
|
54
|
+
* REFETCHES the remaining queue first. A resolution session can run long, and queuing against refs
|
|
55
|
+
* fetched an hour ago rehearses a composition CI will not build — the one freshness gap the sibling
|
|
56
|
+
* update flow is still known to have (only its stage ① ever fetches).
|
|
57
|
+
*/
|
|
58
|
+
resume(repoRoot, state) {
|
|
59
|
+
this.assertNoUnmergedPaths(repoRoot, state);
|
|
60
|
+
this.gitExec.assertNoUntracked(repoRoot);
|
|
61
|
+
this.gitExec.runGitChecked(['-C', repoRoot, 'add', '-u'], 'Failed to stage the resolved files (git add -u)');
|
|
62
|
+
this.gitExec.runGitChecked(['-C', repoRoot, 'commit', '--no-edit'], `Failed to commit the resolution of ${state.current}`);
|
|
63
|
+
process.stdout.write(`✅ Committed the resolution of ${state.current}.\n`);
|
|
64
|
+
state.current = '';
|
|
65
|
+
this.refs.fetchCopies(repoRoot, state.queue);
|
|
66
|
+
this.drain(repoRoot, state);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Throw the whole resolve away: undo any halted merge, go back to the original branch, delete the
|
|
70
|
+
* throwaway branch and the state file. Nothing published, nothing on the feature branch.
|
|
71
|
+
*/
|
|
72
|
+
abort(repoRoot, state) {
|
|
73
|
+
// Allowed to fail: there is no merge to abort when the resolve halted between merges, and that is
|
|
74
|
+
// not an error condition — the rest of the teardown must still run.
|
|
75
|
+
this.gitExec.tryGit(['-C', repoRoot, 'merge', '--abort'], repoRoot);
|
|
76
|
+
this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch], `Failed to return to ${state.originalBranch}`);
|
|
77
|
+
this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);
|
|
78
|
+
this.store.clear(repoRoot);
|
|
79
|
+
process.stdout.write('\n' + SEP + '🛑 Dev-deploy resolve aborted\n' + SEP + '\n'
|
|
80
|
+
+ `You are back on \`${state.originalBranch}\`; \`${state.tmpBranch}\` and the state file are gone.\n`
|
|
81
|
+
+ `The remote copy \`${state.targetRef}\` is untouched, and so is your feature branch.\n`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Merge queued refs until one conflicts (HALT, hand the tree over) or the queue empties (PUBLISH).
|
|
85
|
+
*
|
|
86
|
+
* A LATER ref can conflict too, which is why this is a loop resumed from a state file rather than a
|
|
87
|
+
* single pass: `wp-finish-push-dev` can legitimately stop again on the next ref in the queue.
|
|
88
|
+
*/
|
|
89
|
+
drain(repoRoot, state) {
|
|
90
|
+
while (state.queue.length > 0) {
|
|
91
|
+
const ref = state.queue[0];
|
|
92
|
+
state.queue = state.queue.slice(1);
|
|
93
|
+
const outcome = this.gitExec.tryGit(['-C', repoRoot, 'merge', '--no-ff', '--no-edit', '-m',
|
|
94
|
+
`Dev-deploy: merge ${ref} into ${state.targetRef}`, `refs/remotes/origin/${ref}`], repoRoot);
|
|
95
|
+
if (outcome.ok) {
|
|
96
|
+
process.stdout.write(` ✓ merged ${ref}\n`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
state.current = ref;
|
|
100
|
+
this.store.write(repoRoot, state);
|
|
101
|
+
this.halt(repoRoot, state);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.publish(repoRoot, state);
|
|
105
|
+
}
|
|
106
|
+
/** Stop with the conflicted files listed and the ONE command that continues. Exit 2 = "your turn". */
|
|
107
|
+
halt(repoRoot, state) {
|
|
108
|
+
const conflicted = this.unmergedPaths(repoRoot);
|
|
109
|
+
const remaining = state.queue.length > 0
|
|
110
|
+
? `\nStill queued after this one: ${state.queue.join(', ')}\n`
|
|
111
|
+
+ `\`${rules_config_1.WP_FINISH_PUSH_DEV}\` resumes the queue, so it may stop again on a later ref.\n`
|
|
112
|
+
: '';
|
|
113
|
+
throw new rules_config_1.CliExitError(2, '\n' + SEP + `⚠️ CONFLICT merging ${state.current}\n` + SEP + '\n'
|
|
114
|
+
+ `You are on the throwaway branch \`${state.tmpBranch}\` — NOT \`${state.originalBranch}\`. Nothing here\n`
|
|
115
|
+
+ 'can reach your feature branch or your PR, which is exactly why the resolution happens over here.\n\n'
|
|
116
|
+
+ 'Resolve these files (edit until no conflict markers remain, then `git add` each one):\n'
|
|
117
|
+
+ conflicted.map((f) => ` ${f}`).join('\n') + '\n'
|
|
118
|
+
+ remaining
|
|
119
|
+
+ '\nThen run ONE of:\n'
|
|
120
|
+
+ ` ${rules_config_1.WP_FINISH_PUSH_DEV} ← commit, resume the queue, publish the copy\n`
|
|
121
|
+
+ ` ${rules_config_1.WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \`${state.originalBranch}\`\n\n`
|
|
122
|
+
+ 'Do NOT run `git merge --continue`, `git commit` or `git push` yourself — they are blocked, and\n'
|
|
123
|
+
+ 'the finish command above does all three for you.\n');
|
|
124
|
+
}
|
|
125
|
+
/** Queue empty: force-push the composed result over the copy, then restore the original checkout. */
|
|
126
|
+
publish(repoRoot, state) {
|
|
127
|
+
this.gitExec.runGitChecked(['-C', repoRoot, 'push', '--force', 'origin', `${state.tmpBranch}:refs/heads/${state.targetRef}`], `Failed to publish the dev copy ${state.targetRef}`);
|
|
128
|
+
this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch], `Failed to return to ${state.originalBranch}`);
|
|
129
|
+
this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);
|
|
130
|
+
this.store.clear(repoRoot);
|
|
131
|
+
process.stdout.write('\n' + SEP + `✅ Published ${state.targetRef} with the resolutions\n` + SEP + '\n'
|
|
132
|
+
+ `You are back on \`${state.originalBranch}\`, which was never modified — verify with:\n`
|
|
133
|
+
+ ` git log --oneline ${state.originalBranch}\n`
|
|
134
|
+
+ 'Your CI will pick the copy up on its next composition run.\n');
|
|
135
|
+
}
|
|
136
|
+
/** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */
|
|
137
|
+
unmergedPaths(repoRoot) {
|
|
138
|
+
const out = this.gitExec.gitQuery(['-C', repoRoot, 'diff', '--name-only', '--diff-filter=U'], repoRoot, 'Failed to list the conflicted files (git diff --name-only --diff-filter=U).');
|
|
139
|
+
return out.split('\n').filter((l) => l.trim() !== '');
|
|
140
|
+
}
|
|
141
|
+
// The gate on `wp-finish-push-dev`: refuse (and re-print the list) while anything is still unmerged.
|
|
142
|
+
assertNoUnmergedPaths(repoRoot, state) {
|
|
143
|
+
const conflicted = this.unmergedPaths(repoRoot);
|
|
144
|
+
if (conflicted.length === 0)
|
|
145
|
+
return;
|
|
146
|
+
throw new rules_config_1.CliExitError(2, '\n' + SEP + '⛔ Conflicts are NOT resolved yet\n' + SEP + '\n'
|
|
147
|
+
+ `${rules_config_1.WP_FINISH_PUSH_DEV} commits the resolution — it cannot write one for you. These files are still\n`
|
|
148
|
+
+ 'unmerged (edit until no conflict markers remain, then `git add` each one):\n'
|
|
149
|
+
+ conflicted.map((f) => ` ${f}`).join('\n') + '\n\n'
|
|
150
|
+
+ `Merging: ${state.current === '' ? '(none)' : state.current}\n`
|
|
151
|
+
+ `Give up instead with: ${rules_config_1.WP_FINISH_PUSH_DEV} --abort\n`);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The name of the throwaway branch for a resolve of `branch`.
|
|
155
|
+
*
|
|
156
|
+
* Suffixed rather than prefixed, matching the existing `<feature>Squash` / `<feature>PreMerge<n>`
|
|
157
|
+
* convention — and deliberately NOT inside the dev namespace, which holds REMOTE copies only.
|
|
158
|
+
*/
|
|
159
|
+
tmpBranchFor(branch) {
|
|
160
|
+
return `${branch}DevResolve`;
|
|
161
|
+
}
|
|
162
|
+
/** The banner `--resolve` prints before the first merge, so the queue is visible before it halts. */
|
|
163
|
+
announce(cfg, state) {
|
|
164
|
+
return '\n' + SEP + `🔀 Resolving the dev composition for ${state.targetRef}\n` + SEP + '\n'
|
|
165
|
+
+ `Merging ${String(state.queue.length)} other cop${state.queue.length === 1 ? 'y' : 'ies'} from `
|
|
166
|
+
+ `\`${cfg.copyRefGlob()}\`, in the order your CI composes\n\`${cfg.devBranch}\`, onto the throwaway branch `
|
|
167
|
+
+ `\`${state.tmpBranch}\`:\n`
|
|
168
|
+
+ state.queue.map((r) => ` ${r}`).join('\n') + '\n\n'
|
|
169
|
+
+ `Your feature branch \`${state.originalBranch}\` is not touched by any of this.\n`
|
|
170
|
+
+ `A clean run publishes the copy itself — ${rules_config_1.WP_FINISH_PUSH_DEV} is only needed if a merge stops.\n\n`;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
exports.DevResolveRunner = DevResolveRunner;
|
|
174
|
+
exports.DevResolveRunner = DevResolveRunner = tslib_1.__decorate([
|
|
175
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
176
|
+
tslib_1.__metadata("design:paramtypes", [git_exec_1.GitExec,
|
|
177
|
+
dev_deploy_refs_1.DevDeployRefs,
|
|
178
|
+
push_dev_state_1.PushDevStateStore])
|
|
179
|
+
], DevResolveRunner);
|
|
180
|
+
//# sourceMappingURL=dev-resolve-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-resolve-runner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/dev-resolve-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, DevDeployConfig, WP_FINISH_PUSH_DEV } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from './git-exec';\nimport { DevDeployRefs } from './dev-deploy-refs';\nimport { PushDevState, PushDevStateStore } from './push-dev-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.\n *\n * ONE class owns every git WRITE in the resolve flow, and that is a hard requirement rather than tidiness:\n * `redirect-how-to-merge-main` blocks `git merge` in every form INCLUDING `--continue`, and\n * `pr-creation-or-push-guard` blocks every `git push`. So an AI cannot be told \"now run `git commit`\" or\n * \"`git merge --continue`\" — those instructions are unrunnable by the only party reading them. The command\n * owns the merges, the commit and the push (as child processes the hooks never see); the AI's entire job is\n * editing the conflicted files and `git add`-ing them, neither of which is blocked.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DevResolveRunner {\n constructor(\n private readonly gitExec: GitExec,\n private readonly refs: DevDeployRefs,\n private readonly store: PushDevStateStore,\n ) {}\n\n /**\n * Start a resolve: park on a throwaway branch at the current copy, then drain the queue.\n *\n * The checkout is `-B <tmp> <base>` and NEVER the feature branch, which is the cardinal constraint made\n * mechanical — every merge below lands on a branch that is deleted at the end, so no other developer's\n * commits can reach the PR head even if the resolve is abandoned halfway.\n *\n * `base` differs between the two callers and the difference IS the semantics. `--resolve` starts at the\n * published copy and merges the other copies in (compose what CI will build). `--rebase-resolution`\n * starts at the LOCAL HEAD and merges the published copy in (replay an existing resolution on top of\n * the new commits, instead of force-pushing over it).\n *\n * `fetchRefs` is passed separately from the queue because `--rebase-resolution` merges a ref that is\n * also its push destination, so \"everything to fetch\" and \"everything to merge\" are not the same list.\n */\n start(repoRoot: string, state: PushDevState, base: string, fetchRefs: string[]): void {\n this.refs.fetchCopies(repoRoot, fetchRefs);\n this.store.write(repoRoot, state);\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'checkout', '-B', state.tmpBranch, base],\n `Failed to create the throwaway resolve branch ${state.tmpBranch}`);\n this.drain(repoRoot, state);\n }\n\n /**\n * Resume after the human/AI has resolved the halted merge: commit it, then keep draining.\n *\n * REFETCHES the remaining queue first. A resolution session can run long, and queuing against refs\n * fetched an hour ago rehearses a composition CI will not build — the one freshness gap the sibling\n * update flow is still known to have (only its stage ① ever fetches).\n */\n resume(repoRoot: string, state: PushDevState): void {\n this.assertNoUnmergedPaths(repoRoot, state);\n this.gitExec.assertNoUntracked(repoRoot);\n this.gitExec.runGitChecked(['-C', repoRoot, 'add', '-u'], 'Failed to stage the resolved files (git add -u)');\n this.gitExec.runGitChecked(['-C', repoRoot, 'commit', '--no-edit'],\n `Failed to commit the resolution of ${state.current}`);\n process.stdout.write(`✅ Committed the resolution of ${state.current}.\\n`);\n state.current = '';\n this.refs.fetchCopies(repoRoot, state.queue);\n this.drain(repoRoot, state);\n }\n\n /**\n * Throw the whole resolve away: undo any halted merge, go back to the original branch, delete the\n * throwaway branch and the state file. Nothing published, nothing on the feature branch.\n */\n abort(repoRoot: string, state: PushDevState): void {\n // Allowed to fail: there is no merge to abort when the resolve halted between merges, and that is\n // not an error condition — the rest of the teardown must still run.\n this.gitExec.tryGit(['-C', repoRoot, 'merge', '--abort'], repoRoot);\n this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch],\n `Failed to return to ${state.originalBranch}`);\n this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);\n this.store.clear(repoRoot);\n process.stdout.write(\n '\\n' + SEP + '🛑 Dev-deploy resolve aborted\\n' + SEP + '\\n'\n + `You are back on \\`${state.originalBranch}\\`; \\`${state.tmpBranch}\\` and the state file are gone.\\n`\n + `The remote copy \\`${state.targetRef}\\` is untouched, and so is your feature branch.\\n`);\n }\n\n /**\n * Merge queued refs until one conflicts (HALT, hand the tree over) or the queue empties (PUBLISH).\n *\n * A LATER ref can conflict too, which is why this is a loop resumed from a state file rather than a\n * single pass: `wp-finish-push-dev` can legitimately stop again on the next ref in the queue.\n */\n private drain(repoRoot: string, state: PushDevState): void {\n while (state.queue.length > 0) {\n const ref = state.queue[0];\n state.queue = state.queue.slice(1);\n const outcome = this.gitExec.tryGit(\n ['-C', repoRoot, 'merge', '--no-ff', '--no-edit', '-m',\n `Dev-deploy: merge ${ref} into ${state.targetRef}`, `refs/remotes/origin/${ref}`],\n repoRoot);\n if (outcome.ok) {\n process.stdout.write(` ✓ merged ${ref}\\n`);\n continue;\n }\n state.current = ref;\n this.store.write(repoRoot, state);\n this.halt(repoRoot, state);\n return;\n }\n this.publish(repoRoot, state);\n }\n\n /** Stop with the conflicted files listed and the ONE command that continues. Exit 2 = \"your turn\". */\n private halt(repoRoot: string, state: PushDevState): never {\n const conflicted = this.unmergedPaths(repoRoot);\n const remaining = state.queue.length > 0\n ? `\\nStill queued after this one: ${state.queue.join(', ')}\\n`\n + `\\`${WP_FINISH_PUSH_DEV}\\` resumes the queue, so it may stop again on a later ref.\\n`\n : '';\n throw new CliExitError(2,\n '\\n' + SEP + `⚠️ CONFLICT merging ${state.current}\\n` + SEP + '\\n'\n + `You are on the throwaway branch \\`${state.tmpBranch}\\` — NOT \\`${state.originalBranch}\\`. Nothing here\\n`\n + 'can reach your feature branch or your PR, which is exactly why the resolution happens over here.\\n\\n'\n + 'Resolve these files (edit until no conflict markers remain, then `git add` each one):\\n'\n + conflicted.map((f: string): string => ` ${f}`).join('\\n') + '\\n'\n + remaining\n + '\\nThen run ONE of:\\n'\n + ` ${WP_FINISH_PUSH_DEV} ← commit, resume the queue, publish the copy\\n`\n + ` ${WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \\`${state.originalBranch}\\`\\n\\n`\n + 'Do NOT run `git merge --continue`, `git commit` or `git push` yourself — they are blocked, and\\n'\n + 'the finish command above does all three for you.\\n');\n }\n\n /** Queue empty: force-push the composed result over the copy, then restore the original checkout. */\n private publish(repoRoot: string, state: PushDevState): void {\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'push', '--force', 'origin', `${state.tmpBranch}:refs/heads/${state.targetRef}`],\n `Failed to publish the dev copy ${state.targetRef}`);\n this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch],\n `Failed to return to ${state.originalBranch}`);\n this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);\n this.store.clear(repoRoot);\n process.stdout.write(\n '\\n' + SEP + `✅ Published ${state.targetRef} with the resolutions\\n` + SEP + '\\n'\n + `You are back on \\`${state.originalBranch}\\`, which was never modified — verify with:\\n`\n + ` git log --oneline ${state.originalBranch}\\n`\n + 'Your CI will pick the copy up on its next composition run.\\n');\n }\n\n /** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */\n private unmergedPaths(repoRoot: string): string[] {\n const out = this.gitExec.gitQuery(['-C', repoRoot, 'diff', '--name-only', '--diff-filter=U'], repoRoot,\n 'Failed to list the conflicted files (git diff --name-only --diff-filter=U).');\n return out.split('\\n').filter((l: string): boolean => l.trim() !== '');\n }\n\n // The gate on `wp-finish-push-dev`: refuse (and re-print the list) while anything is still unmerged.\n private assertNoUnmergedPaths(repoRoot: string, state: PushDevState): void {\n const conflicted = this.unmergedPaths(repoRoot);\n if (conflicted.length === 0) return;\n throw new CliExitError(2,\n '\\n' + SEP + '⛔ Conflicts are NOT resolved yet\\n' + SEP + '\\n'\n + `${WP_FINISH_PUSH_DEV} commits the resolution — it cannot write one for you. These files are still\\n`\n + 'unmerged (edit until no conflict markers remain, then `git add` each one):\\n'\n + conflicted.map((f: string): string => ` ${f}`).join('\\n') + '\\n\\n'\n + `Merging: ${state.current === '' ? '(none)' : state.current}\\n`\n + `Give up instead with: ${WP_FINISH_PUSH_DEV} --abort\\n`);\n }\n\n /**\n * The name of the throwaway branch for a resolve of `branch`.\n *\n * Suffixed rather than prefixed, matching the existing `<feature>Squash` / `<feature>PreMerge<n>`\n * convention — and deliberately NOT inside the dev namespace, which holds REMOTE copies only.\n */\n tmpBranchFor(branch: string): string {\n return `${branch}DevResolve`;\n }\n\n /** The banner `--resolve` prints before the first merge, so the queue is visible before it halts. */\n announce(cfg: DevDeployConfig, state: PushDevState): string {\n return '\\n' + SEP + `🔀 Resolving the dev composition for ${state.targetRef}\\n` + SEP + '\\n'\n + `Merging ${String(state.queue.length)} other cop${state.queue.length === 1 ? 'y' : 'ies'} from `\n + `\\`${cfg.copyRefGlob()}\\`, in the order your CI composes\\n\\`${cfg.devBranch}\\`, onto the throwaway branch `\n + `\\`${state.tmpBranch}\\`:\\n`\n + state.queue.map((r: string): string => ` ${r}`).join('\\n') + '\\n\\n'\n + `Your feature branch \\`${state.originalBranch}\\` is not touched by any of this.\\n`\n + `A clean run publishes the copy itself — ${WP_FINISH_PUSH_DEV} is only needed if a merge stops.\\n\\n`;\n }\n}\n"]}
|