pi-pr-review 1.10.3 → 1.10.5
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/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/lib/pr-review-publish.ts +16 -7
- package/package.json +24 -2
- package/prompts/pr-review.md +1 -1
- package/.github/workflows/pull-request.yml +0 -55
- package/.github/workflows/release-please.yml +0 -105
- package/.release-please-manifest.json +0 -3
- package/RELEASING.md +0 -29
- package/release-please-config.json +0 -24
- package/scripts/verify-release-version.mjs +0 -66
- package/tests/pr-review-concurrency.test.ts +0 -45
- package/tests/pr-review-context.test.ts +0 -66
- package/tests/pr-review-extension-lifecycle.test.ts +0 -658
- package/tests/pr-review-focus-view.test.ts +0 -235
- package/tests/pr-review-focus.test.ts +0 -250
- package/tests/pr-review-loop.test.ts +0 -167
- package/tests/pr-review-policy.test.ts +0 -95
- package/tests/pr-review-prompt.test.ts +0 -207
- package/tests/pr-review-publish.test.ts +0 -730
- package/tests/pr-review-registration.test.ts +0 -157
- package/tests/pr-review-subprocess.test.ts +0 -440
- package/tests/pr-review-telemetry.test.ts +0 -179
- package/tests/pr-review-thinking.test.ts +0 -83
- package/tests/pr-review-tool-gate.test.ts +0 -125
- package/tests/pr-review-verify.test.ts +0 -826
- package/tests/pr-self-review.test.ts +0 -342
- package/tests/release-version.test.ts +0 -63
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.10.5](https://github.com/10ego/pi-pr-review/compare/v1.10.4...v1.10.5) (2026-07-17)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **release:** harden Node 24 automation ([#32](https://github.com/10ego/pi-pr-review/issues/32)) ([6b670b3](https://github.com/10ego/pi-pr-review/commit/6b670b3a1e33f3f2392027ae79c159afd9738690))
|
|
9
|
+
|
|
10
|
+
## [1.10.4](https://github.com/10ego/pi-pr-review/compare/v1.10.3...v1.10.4) (2026-07-15)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **publish:** summarize findings without diff patches ([#29](https://github.com/10ego/pi-pr-review/issues/29)) ([bdee1ff](https://github.com/10ego/pi-pr-review/commit/bdee1ff024863019a2f357b04cffe944006a5726))
|
|
16
|
+
|
|
3
17
|
## [1.10.3](https://github.com/10ego/pi-pr-review/compare/v1.10.2...v1.10.3) (2026-07-15)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ The extension—not the model—owns publishing. It creates one formal review wi
|
|
|
186
186
|
|
|
187
187
|
If the agent's final review is not valid exact-contract JSON, the extension logs the validation reason and automatically asks the same agent to correct its completed output once, with all tools disabled and without changing the captured posting authority. A valid correction is cached and publication is attempted under the original flags/config. If that single correction is also invalid or attempts to call a tool, publication stops and reports the error instead of looping. Overlapping input cancels the correction, remains unqueued, and must be retried after the agent settles.
|
|
188
188
|
|
|
189
|
-
Closed or merged PRs use a body-only review. Open PRs attach eligible P0–P3 findings as inline comments and keep nits or off-diff findings in the review body. When multiple findings target the same diff anchor, the first is attached inline and later findings remain in the review body so publication neither fails nor drops review content.
|
|
189
|
+
Closed or merged PRs use a body-only review. Open PRs attach eligible P0–P3 findings as inline comments and keep nits or off-diff findings in the review body. When GitHub omits patch metadata needed to validate an inline anchor, the affected finding remains in the review summary instead of blocking publication. When multiple findings target the same diff anchor, the first is attached inline and later findings remain in the review body so publication neither fails nor drops review content.
|
|
190
190
|
|
|
191
191
|
After a review completes, you can directly ask the agent to “post the inline review” or “post it as an inline review.” The extension handles that request directly before an agent turn, selecting the latest cached review for an unnumbered request or the named PR in “publish the review for PR #123.” Only fresh interactive/RPC input can trigger this path; extension-generated, queued, or steering input cannot. The extension publishes only validated cached content, never replacement model-authored text, and never starts or reruns review agents. A direct request permits stale publication.
|
|
192
192
|
|
package/lib/pr-review-publish.ts
CHANGED
|
@@ -837,6 +837,7 @@ function isInlineSeverity(finding: ReviewFindingLike): boolean {
|
|
|
837
837
|
export interface CommentValidationResult {
|
|
838
838
|
comments: PublishComment[];
|
|
839
839
|
errors: string[];
|
|
840
|
+
warnings?: string[];
|
|
840
841
|
}
|
|
841
842
|
|
|
842
843
|
export function validateInlineComments(
|
|
@@ -844,6 +845,7 @@ export function validateInlineComments(
|
|
|
844
845
|
changedFiles: ChangedFileLike[],
|
|
845
846
|
): CommentValidationResult {
|
|
846
847
|
const errors: string[] = [];
|
|
848
|
+
const warnings: string[] = [];
|
|
847
849
|
const comments: PublishComment[] = [];
|
|
848
850
|
const files = new Map<string, ChangedFileLike>();
|
|
849
851
|
for (const file of changedFiles) {
|
|
@@ -876,7 +878,9 @@ export function validateInlineComments(
|
|
|
876
878
|
continue;
|
|
877
879
|
}
|
|
878
880
|
if (!file.patch) {
|
|
879
|
-
|
|
881
|
+
// GitHub legitimately omits patch metadata for some large, binary, or transiently unavailable diffs.
|
|
882
|
+
// Keep the complete finding in the review summary rather than sending an unvalidated inline anchor.
|
|
883
|
+
warnings.push(`${label}: diff patch is unavailable; kept in the review summary`);
|
|
880
884
|
continue;
|
|
881
885
|
}
|
|
882
886
|
const sideKey = side === "LEFT" ? "left" : "right";
|
|
@@ -913,7 +917,7 @@ export function validateInlineComments(
|
|
|
913
917
|
if (comments.length > MAX_INLINE_COMMENTS) {
|
|
914
918
|
errors.push(`too many inline comments (${comments.length}; max ${MAX_INLINE_COMMENTS})`);
|
|
915
919
|
}
|
|
916
|
-
return { comments, errors };
|
|
920
|
+
return { comments, errors, warnings };
|
|
917
921
|
}
|
|
918
922
|
|
|
919
923
|
export function collectFoldedComments(review: ReviewLike): CommentValidationResult {
|
|
@@ -954,7 +958,7 @@ export function collectFoldedComments(review: ReviewLike): CommentValidationResu
|
|
|
954
958
|
...(Number(start) < Number(end) ? { start_line: Number(start), start_side: side } : {}),
|
|
955
959
|
});
|
|
956
960
|
}
|
|
957
|
-
return { comments, errors };
|
|
961
|
+
return { comments, errors, warnings: [] };
|
|
958
962
|
}
|
|
959
963
|
|
|
960
964
|
export function foldInlineComments(summary: string, comments: PublishComment[]): string {
|
|
@@ -1244,6 +1248,7 @@ export async function publishPullReview(input: {
|
|
|
1244
1248
|
if (!lifecycle.lifecycle) return { status: "failed", message: lifecycle.error ?? "invalid PR lifecycle" };
|
|
1245
1249
|
const isOpen = lifecycle.lifecycle === "open";
|
|
1246
1250
|
let comments: PublishComment[] = [];
|
|
1251
|
+
let inlineWarningCount = 0;
|
|
1247
1252
|
if (!isOpen) {
|
|
1248
1253
|
const candidates = collectFoldedComments(review);
|
|
1249
1254
|
if (candidates.errors.length > 0) {
|
|
@@ -1270,6 +1275,7 @@ export async function publishPullReview(input: {
|
|
|
1270
1275
|
return { status: "failed", message: `inline validation failed: ${validated.errors.join("; ")}` };
|
|
1271
1276
|
}
|
|
1272
1277
|
comments = validated.comments;
|
|
1278
|
+
inlineWarningCount = validated.warnings?.length ?? 0;
|
|
1273
1279
|
}
|
|
1274
1280
|
}
|
|
1275
1281
|
|
|
@@ -1316,6 +1322,10 @@ export async function publishPullReview(input: {
|
|
|
1316
1322
|
return { status: "failed", message: `final head check failed: ${String(error)}` };
|
|
1317
1323
|
}
|
|
1318
1324
|
|
|
1325
|
+
const inlineWarning = inlineWarningCount > 0
|
|
1326
|
+
? `; ${inlineWarningCount} inline finding${inlineWarningCount === 1 ? "" : "s"} kept in the summary because GitHub omitted diff patch metadata`
|
|
1327
|
+
: "";
|
|
1328
|
+
const degraded = !isOpen || headPlan.stale || inlineWarningCount > 0;
|
|
1319
1329
|
const post = await runGh(
|
|
1320
1330
|
githubApiArgs(hostname, "--method", "POST", `repos/${repository}/pulls/${prNumber}/reviews`, "--input", "-"),
|
|
1321
1331
|
cwd,
|
|
@@ -1328,13 +1338,12 @@ export async function publishPullReview(input: {
|
|
|
1328
1338
|
} catch {
|
|
1329
1339
|
/* accepted response without parseable metadata */
|
|
1330
1340
|
}
|
|
1331
|
-
const degraded = !isOpen || headPlan.stale;
|
|
1332
1341
|
return {
|
|
1333
1342
|
status: degraded ? "posted_degraded" : "posted",
|
|
1334
1343
|
message: headPlan.stale
|
|
1335
1344
|
? `body-only stale COMMENT review posted (${headPlan.reviewedHeadSha} -> ${headPlan.currentHeadSha})`
|
|
1336
1345
|
: isOpen
|
|
1337
|
-
?
|
|
1346
|
+
? `GitHub COMMENT review posted${inlineWarning}`
|
|
1338
1347
|
: "body-only COMMENT review posted for non-open PR",
|
|
1339
1348
|
reviewId: response.id,
|
|
1340
1349
|
url: response.html_url,
|
|
@@ -1344,8 +1353,8 @@ export async function publishPullReview(input: {
|
|
|
1344
1353
|
try {
|
|
1345
1354
|
if (await hasExistingMarker(cwd, hostname, repository, prNumber, identity, normalizedHeadSha)) {
|
|
1346
1355
|
return {
|
|
1347
|
-
status:
|
|
1348
|
-
message:
|
|
1356
|
+
status: degraded ? "posted_degraded" : "posted",
|
|
1357
|
+
message: `GitHub review found during failure reconciliation${inlineWarning}`,
|
|
1349
1358
|
reconciled: true,
|
|
1350
1359
|
};
|
|
1351
1360
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pr-review",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.5",
|
|
4
4
|
"description": "Parallel AI code review for GitHub pull requests in the Pi coding agent, with model-agnostic tiered subagents, structured findings, optional verification, and safe COMMENT-only publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -30,9 +30,31 @@
|
|
|
30
30
|
"type": "git",
|
|
31
31
|
"url": "git+https://github.com/10ego/pi-pr-review.git"
|
|
32
32
|
},
|
|
33
|
+
"homepage": "https://github.com/10ego/pi-pr-review#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/10ego/pi-pr-review/issues"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"README.md",
|
|
42
|
+
"CHANGELOG.md",
|
|
43
|
+
"extensions/",
|
|
44
|
+
"lib/",
|
|
45
|
+
"prompts/"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"registry": "https://registry.npmjs.org/",
|
|
50
|
+
"provenance": true
|
|
51
|
+
},
|
|
33
52
|
"scripts": {
|
|
34
53
|
"test": "bun test",
|
|
35
|
-
"
|
|
54
|
+
"test:tooling": "node --test tests/tooling/compare-semver.node.mjs tests/tooling/package-contents.node.mjs tests/tooling/workflows.node.mjs",
|
|
55
|
+
"verify:release-version": "node scripts/verify-release-version.mjs",
|
|
56
|
+
"verify:package": "node scripts/verify-package-contents.mjs",
|
|
57
|
+
"verify:workflows": "node scripts/verify-workflows.mjs"
|
|
36
58
|
},
|
|
37
59
|
"peerDependencies": {
|
|
38
60
|
"@earendil-works/pi-ai": "*",
|
package/prompts/pr-review.md
CHANGED
|
@@ -194,7 +194,7 @@ The orchestrator must never call `gh` to post comments or reviews. Always finish
|
|
|
194
194
|
- `--comment` → force publication for this run, but never bypass validation, stale-head checks, or duplicate checks
|
|
195
195
|
- `--no-comment` → suppress publication for this run
|
|
196
196
|
|
|
197
|
-
When enabled, the extension creates exactly one formal GitHub pull-request review. Its top-level body contains the overview, verification, strengths, suggested verdict, counts, nits, and findings that cannot be attached inline. Eligible P0–P3 diff-anchored findings are attached as inline comments within that same review and are omitted from the top-level issue list to avoid duplication. If multiple findings share one diff anchor, the first is attached inline and later findings remain in the top-level body instead of failing publication or dropping content. The API event is hardcoded to `COMMENT`: publication never sends `APPROVE` or `REQUEST_CHANGES`, even when the suggested verdict is `request_changes`. It appends the same-head marker, verifies the current head, validates every inline anchor against GitHub diff metadata, and refuses partial open-PR publication. For a known closed/merged PR it requires either trusted `--include-closed`/`--review-closed` invocation authority or the one-shot affirmative confirmation flow, then posts one body-only `COMMENT` review with each inline finding folded into the body exactly once. Unknown lifecycle states and unconfirmed non-open writes fail without posting or falling back to an issue comment.
|
|
197
|
+
When enabled, the extension creates exactly one formal GitHub pull-request review. Its top-level body contains the overview, verification, strengths, suggested verdict, counts, nits, and findings that cannot be attached inline. Eligible P0–P3 diff-anchored findings are attached as inline comments within that same review and are omitted from the top-level issue list to avoid duplication. If GitHub omits patch metadata needed to validate an anchor, that finding remains in the top-level body instead of blocking publication. If multiple findings share one diff anchor, the first is attached inline and later findings remain in the top-level body instead of failing publication or dropping content. The API event is hardcoded to `COMMENT`: publication never sends `APPROVE` or `REQUEST_CHANGES`, even when the suggested verdict is `request_changes`. It appends the same-head marker, verifies the current head, validates every attempted inline anchor against GitHub diff metadata, and refuses partial open-PR publication. For a known closed/merged PR it requires either trusted `--include-closed`/`--review-closed` invocation authority or the one-shot affirmative confirmation flow, then posts one body-only `COMMENT` review with each inline finding folded into the body exactly once. Unknown lifecycle states and unconfirmed non-open writes fail without posting or falling back to an issue comment.
|
|
198
198
|
|
|
199
199
|
The extension caches the latest valid completed review per repository and PR before publication preflight in the current Pi session. The session-backed cache survives extension reloads and session resumes but is bound to the originating session instance and repository. On a later turn, the extension intercepts that direct input before an agent turn, publishes only extension-cached content, permits stale publication, and cannot rerun review agents. Stale publication is also enabled by default through the invocation-captured `allowStalePublish` setting. Every stale review is body-only, displays a warning with the reviewed and preflight-observed commit hashes, and omits potentially invalid inline anchors. If the captured setting disabled stale publication, the user can still run `/pr-review-publish <PR-NUM> --allow-stale`. Never rerun the review merely to change posting intent, and never attempt a direct GitHub write yourself.
|
|
200
200
|
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
name: Pull Request
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
types:
|
|
6
|
-
- opened
|
|
7
|
-
- edited
|
|
8
|
-
- synchronize
|
|
9
|
-
- reopened
|
|
10
|
-
- ready_for_review
|
|
11
|
-
|
|
12
|
-
permissions:
|
|
13
|
-
contents: read
|
|
14
|
-
|
|
15
|
-
jobs:
|
|
16
|
-
title:
|
|
17
|
-
name: Validate PR title
|
|
18
|
-
runs-on: ubuntu-latest
|
|
19
|
-
timeout-minutes: 5
|
|
20
|
-
steps:
|
|
21
|
-
- name: Check Conventional Commit format
|
|
22
|
-
env:
|
|
23
|
-
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
24
|
-
run: |
|
|
25
|
-
node <<'NODE'
|
|
26
|
-
const title = process.env.PR_TITLE ?? "";
|
|
27
|
-
const conventionalTitle = /^(feat|fix|perf|revert|docs|style|refactor|test|chore|release)(\([a-z0-9][a-z0-9._/-]*\))?!?: \S.*$/;
|
|
28
|
-
|
|
29
|
-
if (!conventionalTitle.test(title)) {
|
|
30
|
-
console.error(`Invalid PR title: ${title}`);
|
|
31
|
-
console.error("Use: <type>(optional-scope): description");
|
|
32
|
-
console.error("Allowed types: feat, fix, perf, revert, docs, style, refactor, test, chore, release");
|
|
33
|
-
console.error("Add ! before : for a breaking change, for example feat!: description");
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
NODE
|
|
37
|
-
|
|
38
|
-
test:
|
|
39
|
-
name: Test
|
|
40
|
-
runs-on: ubuntu-latest
|
|
41
|
-
timeout-minutes: 20
|
|
42
|
-
steps:
|
|
43
|
-
- name: Check out repository
|
|
44
|
-
uses: actions/checkout@v4
|
|
45
|
-
|
|
46
|
-
- name: Verify release version metadata
|
|
47
|
-
run: npm run verify:release-version
|
|
48
|
-
|
|
49
|
-
- name: Set up Bun
|
|
50
|
-
uses: oven-sh/setup-bun@v2
|
|
51
|
-
with:
|
|
52
|
-
bun-version: 1.2.15
|
|
53
|
-
|
|
54
|
-
- name: Run tests
|
|
55
|
-
run: bun test
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
name: Release Please
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
|
|
9
|
-
concurrency:
|
|
10
|
-
group: release-please-${{ github.ref }}
|
|
11
|
-
cancel-in-progress: false
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
release:
|
|
15
|
-
name: Create release PR or GitHub release
|
|
16
|
-
runs-on: ubuntu-latest
|
|
17
|
-
timeout-minutes: 10
|
|
18
|
-
permissions:
|
|
19
|
-
contents: read
|
|
20
|
-
outputs:
|
|
21
|
-
release_created: ${{ steps.release.outputs.release_created }}
|
|
22
|
-
release_version: ${{ steps.release.outputs.version }}
|
|
23
|
-
steps:
|
|
24
|
-
- name: Create nerv-ops installation token
|
|
25
|
-
id: app-token
|
|
26
|
-
uses: actions/create-github-app-token@v2
|
|
27
|
-
with:
|
|
28
|
-
app-id: ${{ vars.NERV_OPS_APP_ID }}
|
|
29
|
-
private-key: ${{ secrets.NERV_OPS_PRIVATE_KEY }}
|
|
30
|
-
|
|
31
|
-
- name: Run Release Please
|
|
32
|
-
id: release
|
|
33
|
-
uses: googleapis/release-please-action@v4
|
|
34
|
-
with:
|
|
35
|
-
config-file: release-please-config.json
|
|
36
|
-
manifest-file: .release-please-manifest.json
|
|
37
|
-
token: ${{ steps.app-token.outputs.token }}
|
|
38
|
-
|
|
39
|
-
- name: Enable squash auto-merge for release PRs
|
|
40
|
-
if: ${{ steps.release.outputs.prs_created == 'true' }}
|
|
41
|
-
env:
|
|
42
|
-
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
43
|
-
RELEASE_PRS: ${{ steps.release.outputs.prs }}
|
|
44
|
-
run: |
|
|
45
|
-
jq -r '.[].number' <<< "$RELEASE_PRS" | while read -r number; do
|
|
46
|
-
merge_method="$(gh pr view "$number" --repo "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest.mergeMethod // "NONE"')"
|
|
47
|
-
echo "Release PR #$number auto-merge method: $merge_method"
|
|
48
|
-
|
|
49
|
-
case "$merge_method" in
|
|
50
|
-
SQUASH)
|
|
51
|
-
echo "Squash auto-merge is already enabled for release PR #$number"
|
|
52
|
-
continue
|
|
53
|
-
;;
|
|
54
|
-
MERGE|REBASE)
|
|
55
|
-
echo "Replacing $merge_method auto-merge for release PR #$number"
|
|
56
|
-
gh pr merge "$number" --repo "$GITHUB_REPOSITORY" --disable-auto
|
|
57
|
-
;;
|
|
58
|
-
NONE)
|
|
59
|
-
;;
|
|
60
|
-
*)
|
|
61
|
-
echo "Unexpected auto-merge method for release PR #$number: $merge_method" >&2
|
|
62
|
-
exit 1
|
|
63
|
-
;;
|
|
64
|
-
esac
|
|
65
|
-
|
|
66
|
-
gh pr merge "$number" --repo "$GITHUB_REPOSITORY" --auto --squash
|
|
67
|
-
done
|
|
68
|
-
|
|
69
|
-
publish:
|
|
70
|
-
name: Test and publish to npm
|
|
71
|
-
needs: release
|
|
72
|
-
if: ${{ needs.release.outputs.release_created == 'true' }}
|
|
73
|
-
runs-on: ubuntu-latest
|
|
74
|
-
timeout-minutes: 20
|
|
75
|
-
permissions:
|
|
76
|
-
contents: read
|
|
77
|
-
id-token: write
|
|
78
|
-
steps:
|
|
79
|
-
- name: Check out release commit
|
|
80
|
-
uses: actions/checkout@v4
|
|
81
|
-
|
|
82
|
-
- name: Set up Node.js
|
|
83
|
-
uses: actions/setup-node@v4
|
|
84
|
-
with:
|
|
85
|
-
node-version: 22
|
|
86
|
-
registry-url: https://registry.npmjs.org
|
|
87
|
-
|
|
88
|
-
- name: Set up Bun
|
|
89
|
-
uses: oven-sh/setup-bun@v2
|
|
90
|
-
with:
|
|
91
|
-
bun-version: 1.2.15
|
|
92
|
-
|
|
93
|
-
- name: Install npm with trusted publishing support
|
|
94
|
-
run: npm install --global npm@11.5.1
|
|
95
|
-
|
|
96
|
-
- name: Test
|
|
97
|
-
run: bun test
|
|
98
|
-
|
|
99
|
-
- name: Verify release version metadata
|
|
100
|
-
env:
|
|
101
|
-
EXPECTED_VERSION: ${{ needs.release.outputs.release_version }}
|
|
102
|
-
run: npm run verify:release-version -- --expected "$EXPECTED_VERSION"
|
|
103
|
-
|
|
104
|
-
- name: Publish to npm
|
|
105
|
-
run: npm publish --access public --provenance
|
package/RELEASING.md
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# Releasing
|
|
2
|
-
|
|
3
|
-
[Release Please](https://github.com/googleapis/release-please) watches conventional commits merged into `main`. It opens or updates a release PR containing the calculated version change, `CHANGELOG.md`, and release manifest, then enables squash auto-merge for that PR. Required checks must pass before GitHub merges it. Its root Node strategy is the sole version writer: the generated PR keeps `package.json` and `.release-please-manifest.json` in sync. The generated PR title—and therefore its squash commit—uses `release(main): release <version>`. The merge creates a GitHub release and, after the test suite passes, publishes `pi-pr-review` to npm with signed provenance.
|
|
4
|
-
|
|
5
|
-
Pull-request CI verifies both root version fields remain equal. The publish job repeats that read-only check against the version emitted by Release Please, so an inconsistent release commit cannot publish; no feature PR should manually set a release version.
|
|
6
|
-
|
|
7
|
-
## Semver
|
|
8
|
-
|
|
9
|
-
The squash-merged PR title becomes the commit Release Please evaluates:
|
|
10
|
-
|
|
11
|
-
- `feat: ...` creates a minor release.
|
|
12
|
-
- `feat!: ...`, `fix!: ...`, or a `BREAKING CHANGE:` footer creates a major release.
|
|
13
|
-
- Every other allowed type—`fix:`, `perf:`, `revert:`, `chore:`, `docs:`, `refactor:`, `style:`, and `test:`—creates a patch release.
|
|
14
|
-
|
|
15
|
-
All changes to `main` must go through a pull request and use squash merging. The required `Validate PR title` check enforces the conventional title, and the required `Test` check runs the Bun test suite. Direct pushes, force pushes, branch deletion, and administrator bypass are disabled.
|
|
16
|
-
|
|
17
|
-
## One-time setup
|
|
18
|
-
|
|
19
|
-
1. Install the private [`nerv-ops`](https://github.com/settings/apps/nerv-ops) GitHub App on this repository with **Contents: read and write** and **Pull requests: read and write** permissions.
|
|
20
|
-
2. Under the repository's **Settings → General → Pull Requests**, enable **Allow auto-merge** and keep squash merging enabled.
|
|
21
|
-
3. Add the App ID as the repository Actions variable `NERV_OPS_APP_ID`, and add a generated PEM private key as the repository Actions secret `NERV_OPS_PRIVATE_KEY`.
|
|
22
|
-
4. In the npm settings for [`pi-pr-review`](https://www.npmjs.com/package/pi-pr-review), add a GitHub Actions trusted publisher with:
|
|
23
|
-
- organization/user: `10ego`
|
|
24
|
-
- repository: `pi-pr-review`
|
|
25
|
-
- workflow filename: `release-please.yml`
|
|
26
|
-
- environment: leave blank
|
|
27
|
-
5. Use conventional titles for squash-merged PRs.
|
|
28
|
-
|
|
29
|
-
No npm token is stored in GitHub. The workflow exchanges the App credentials for a short-lived repository installation token and uses npm trusted publishing through GitHub OIDC.
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
-
"bootstrap-sha": "b9b62811aab72269b5f2bbb58d08d37b3c20ecff",
|
|
4
|
-
"packages": {
|
|
5
|
-
".": {
|
|
6
|
-
"release-type": "node",
|
|
7
|
-
"package-name": "pi-pr-review",
|
|
8
|
-
"changelog-path": "CHANGELOG.md",
|
|
9
|
-
"pull-request-title-pattern": "release${scope}: release${component} ${version}",
|
|
10
|
-
"include-component-in-tag": false,
|
|
11
|
-
"changelog-sections": [
|
|
12
|
-
{ "type": "feat", "section": "Features", "hidden": false },
|
|
13
|
-
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
|
|
14
|
-
{ "type": "perf", "section": "Performance Improvements", "hidden": false },
|
|
15
|
-
{ "type": "revert", "section": "Reverts", "hidden": false },
|
|
16
|
-
{ "type": "docs", "section": "Documentation", "hidden": false },
|
|
17
|
-
{ "type": "style", "section": "Styles", "hidden": false },
|
|
18
|
-
{ "type": "refactor", "section": "Code Refactoring", "hidden": false },
|
|
19
|
-
{ "type": "test", "section": "Tests", "hidden": false },
|
|
20
|
-
{ "type": "chore", "section": "Miscellaneous Chores", "hidden": false }
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
|
|
5
|
-
// SemVer numeric prerelease identifiers cannot contain leading zeroes.
|
|
6
|
-
const NUMERIC_IDENTIFIER = "(?:0|[1-9]\\d*)";
|
|
7
|
-
const PRERELEASE_IDENTIFIER = `(?:${NUMERIC_IDENTIFIER}|\\d*[A-Za-z-][0-9A-Za-z-]*)`;
|
|
8
|
-
const SEMVER = new RegExp(`^${NUMERIC_IDENTIFIER}\\.${NUMERIC_IDENTIFIER}\\.${NUMERIC_IDENTIFIER}(?:-${PRERELEASE_IDENTIFIER}(?:\\.${PRERELEASE_IDENTIFIER})*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`);
|
|
9
|
-
|
|
10
|
-
function readJson(filePath) {
|
|
11
|
-
try {
|
|
12
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
13
|
-
} catch (error) {
|
|
14
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
-
throw new Error(`Could not read ${filePath}: ${message}`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function assertVersion(location, value) {
|
|
20
|
-
if (typeof value !== "string" || !SEMVER.test(value)) {
|
|
21
|
-
throw new Error(`${location} must contain a valid semantic version; received ${JSON.stringify(value)}`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Verify the root package metadata that Release Please updates in one release PR.
|
|
27
|
-
* This deliberately reads only: Release Please remains the sole version writer.
|
|
28
|
-
*/
|
|
29
|
-
export function verifyRootReleaseVersion(rootDir = process.cwd(), expectedVersion) {
|
|
30
|
-
const manifest = readJson(path.join(rootDir, ".release-please-manifest.json"));
|
|
31
|
-
const packageJson = readJson(path.join(rootDir, "package.json"));
|
|
32
|
-
const versions = {
|
|
33
|
-
'.release-please-manifest.json["."]': manifest["."],
|
|
34
|
-
"package.json.version": packageJson.version,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
for (const [location, version] of Object.entries(versions)) assertVersion(location, version);
|
|
38
|
-
if (expectedVersion !== undefined) assertVersion("expected release version", expectedVersion);
|
|
39
|
-
|
|
40
|
-
const [firstLocation, firstVersion] = Object.entries(versions)[0];
|
|
41
|
-
for (const [location, version] of Object.entries(versions).slice(1)) {
|
|
42
|
-
if (version !== firstVersion) {
|
|
43
|
-
throw new Error(`Root release version mismatch: ${firstLocation} is ${firstVersion}, but ${location} is ${version}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (expectedVersion !== undefined && firstVersion !== expectedVersion) {
|
|
47
|
-
throw new Error(`Root release version mismatch: ${firstLocation} is ${firstVersion}, but expected release version is ${expectedVersion}`);
|
|
48
|
-
}
|
|
49
|
-
return firstVersion;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function main(args) {
|
|
53
|
-
if (args.length === 0) return verifyRootReleaseVersion();
|
|
54
|
-
if (args.length === 2 && args[0] === "--expected") return verifyRootReleaseVersion(process.cwd(), args[1]);
|
|
55
|
-
throw new Error("Usage: node scripts/verify-release-version.mjs [--expected <version>]");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
59
|
-
try {
|
|
60
|
-
const version = main(process.argv.slice(2));
|
|
61
|
-
console.log(`Verified root release version ${version}.`);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
64
|
-
process.exitCode = 1;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { runWithConcurrency } from "../lib/pr-review-concurrency.ts";
|
|
3
|
-
|
|
4
|
-
async function waitFor(condition: () => boolean): Promise<void> {
|
|
5
|
-
for (let attempts = 0; attempts < 50; attempts++) {
|
|
6
|
-
if (condition()) return;
|
|
7
|
-
await Promise.resolve();
|
|
8
|
-
}
|
|
9
|
-
throw new Error("condition was not reached");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe("bounded review-pass scheduling", () => {
|
|
13
|
-
test("starts independent workers in parallel without exceeding the concurrency cap", async () => {
|
|
14
|
-
const started: number[] = [];
|
|
15
|
-
const releases = new Map<number, () => void>();
|
|
16
|
-
let active = 0;
|
|
17
|
-
let peakActive = 0;
|
|
18
|
-
|
|
19
|
-
const batch = runWithConcurrency([0, 1, 2, 3], 2, async (item) => {
|
|
20
|
-
started.push(item);
|
|
21
|
-
active++;
|
|
22
|
-
peakActive = Math.max(peakActive, active);
|
|
23
|
-
await new Promise<void>((resolve) => releases.set(item, resolve));
|
|
24
|
-
active--;
|
|
25
|
-
return item * 10;
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
expect(started).toEqual([0, 1]);
|
|
29
|
-
expect(peakActive).toBe(2);
|
|
30
|
-
|
|
31
|
-
releases.get(0)!();
|
|
32
|
-
await waitFor(() => started.length === 3);
|
|
33
|
-
expect(started).toEqual([0, 1, 2]);
|
|
34
|
-
expect(active).toBe(2);
|
|
35
|
-
|
|
36
|
-
releases.get(1)!();
|
|
37
|
-
await waitFor(() => started.length === 4);
|
|
38
|
-
expect(started).toEqual([0, 1, 2, 3]);
|
|
39
|
-
expect(peakActive).toBe(2);
|
|
40
|
-
|
|
41
|
-
releases.get(2)!();
|
|
42
|
-
releases.get(3)!();
|
|
43
|
-
expect(await batch).toEqual([0, 10, 20, 30]);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
5
|
-
import { loadReviewContext, shardUnifiedDiff } from "../lib/pr-review-context.ts";
|
|
6
|
-
|
|
7
|
-
const roots: string[] = [];
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
function fixture(): string {
|
|
13
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pr-review-context-test-"));
|
|
14
|
-
roots.push(root);
|
|
15
|
-
return root;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("unified diff sharding", () => {
|
|
19
|
-
test("keeps file blocks whole and covers each block exactly once", () => {
|
|
20
|
-
const diff = [
|
|
21
|
-
"diff --git a/a.ts b/a.ts\n--- a/a.ts\n+++ b/a.ts\n@@ -1 +1 @@\n-old\n+new",
|
|
22
|
-
"diff --git a/b.ts b/b.ts\n--- a/b.ts\n+++ b/b.ts\n@@ -1 +1,3 @@\n-old\n+one\n+two\n+three",
|
|
23
|
-
"diff --git a/c.ts b/c.ts\n--- a/c.ts\n+++ b/c.ts\n@@ -1 +1 @@\n-old\n+new",
|
|
24
|
-
].join("\n");
|
|
25
|
-
const shards = shardUnifiedDiff(diff, 3);
|
|
26
|
-
expect(shards).toHaveLength(3);
|
|
27
|
-
for (const path of ["a.ts", "b.ts", "c.ts"]) {
|
|
28
|
-
expect(shards.filter((shard) => shard.includes(`a/${path} b/${path}`))).toHaveLength(1);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("does not invent empty shards for a single changed file", () => {
|
|
33
|
-
const diff = "diff --git a/a.ts b/a.ts\n--- a/a.ts\n+++ b/a.ts\n@@ -1 +1 @@\n-old\n+new";
|
|
34
|
-
expect(shardUnifiedDiff(diff, 2)).toEqual([diff]);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("review context files", () => {
|
|
39
|
-
test("appends a relative complete diff to compact inline metadata", async () => {
|
|
40
|
-
const root = fixture();
|
|
41
|
-
fs.writeFileSync(path.join(root, "pr.diff"), "diff --git a/a.ts b/a.ts\n+added\n");
|
|
42
|
-
const loaded = await loadReviewContext(root, "PR #7 metadata", "pr.diff");
|
|
43
|
-
expect(loaded.context).toContain("PR #7 metadata");
|
|
44
|
-
expect(loaded.context).toContain("--- Complete PR diff from context_file ---");
|
|
45
|
-
expect(loaded.context).toContain("diff --git a/a.ts b/a.ts");
|
|
46
|
-
expect(loaded.contextFile).toBe(path.join(root, "pr.diff"));
|
|
47
|
-
expect(loaded.contextFileText).toContain("diff --git a/a.ts b/a.ts");
|
|
48
|
-
expect(loaded.contextFileBytes).toBeGreaterThan(0);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("preserves inline-only compatibility", async () => {
|
|
52
|
-
expect(await loadReviewContext("/tmp", " inline diff ", undefined)).toEqual({
|
|
53
|
-
context: "inline diff",
|
|
54
|
-
contextFileBytes: 0,
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("rejects empty, non-file, and oversized inputs before dispatch", async () => {
|
|
59
|
-
const root = fixture();
|
|
60
|
-
fs.writeFileSync(path.join(root, "empty.diff"), "");
|
|
61
|
-
fs.writeFileSync(path.join(root, "large.diff"), "12345");
|
|
62
|
-
await expect(loadReviewContext(root, undefined, "empty.diff")).rejects.toThrow("is empty");
|
|
63
|
-
await expect(loadReviewContext(root, undefined, ".")).rejects.toThrow("not a regular file");
|
|
64
|
-
await expect(loadReviewContext(root, undefined, "large.diff", 4)).rejects.toThrow("exceeds 4 bytes");
|
|
65
|
-
});
|
|
66
|
-
});
|