pullfrog 0.1.32 → 0.1.33
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/dist/cli.mjs +136 -13
- package/dist/index.js +135 -12
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal.js +22 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/utils/autoMerge.d.ts +37 -0
- package/dist/utils/checksGate.d.ts +45 -0
- package/dist/utils/runContext.d.ts +1 -0
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -102841,7 +102841,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
102841
102841
|
// package.json
|
|
102842
102842
|
var package_default = {
|
|
102843
102843
|
name: "pullfrog",
|
|
102844
|
-
version: "0.1.
|
|
102844
|
+
version: "0.1.33",
|
|
102845
102845
|
type: "module",
|
|
102846
102846
|
bin: {
|
|
102847
102847
|
pullfrog: "dist/cli.mjs",
|
|
@@ -159380,13 +159380,14 @@ async function approveAfterFix(ctx) {
|
|
|
159380
159380
|
});
|
|
159381
159381
|
if (pr.data.state !== "open") return;
|
|
159382
159382
|
const headSha = pr.data.head.sha;
|
|
159383
|
+
const selfAuthored = isPullfrog(pr.data.user?.login);
|
|
159383
159384
|
const summary2 = ctx.toolState.lastProgressBody?.trim();
|
|
159384
159385
|
const body = "> \u2705 Pullfrog addressed all of its review feedback on this PR.\n\n" + (summary2 || "all Pullfrog-raised review threads are resolved \u2014 no outstanding findings remain.");
|
|
159385
159386
|
const params = {
|
|
159386
159387
|
owner: ctx.repo.owner,
|
|
159387
159388
|
repo: ctx.repo.name,
|
|
159388
159389
|
pull_number: pullNumber,
|
|
159389
|
-
event: "APPROVE",
|
|
159390
|
+
event: selfAuthored ? "COMMENT" : "APPROVE",
|
|
159390
159391
|
commit_id: headSha
|
|
159391
159392
|
};
|
|
159392
159393
|
const result = await createAndSubmitWithFooter(ctx, params, {
|
|
@@ -159558,22 +159559,39 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
159558
159559
|
);
|
|
159559
159560
|
}
|
|
159560
159561
|
}
|
|
159562
|
+
const prSnapshot = (await ctx.octokit.rest.pulls.get({
|
|
159563
|
+
owner: ctx.repo.owner,
|
|
159564
|
+
repo: ctx.repo.name,
|
|
159565
|
+
pull_number
|
|
159566
|
+
})).data;
|
|
159567
|
+
const selfAuthored = isPullfrog(prSnapshot.user?.login);
|
|
159561
159568
|
ctx.toolState.approval = { wouldApprove: approved === true, sha: primary.checkoutSha };
|
|
159562
159569
|
const skip = reviewSkipDecision({
|
|
159563
159570
|
approved: approved ?? false,
|
|
159564
159571
|
requestChanges: request_changes ?? false,
|
|
159565
159572
|
body,
|
|
159566
159573
|
hasComments: comments.length > 0,
|
|
159567
|
-
|
|
159574
|
+
// a self-authored approve downgrades to COMMENT (GitHub blocks a
|
|
159575
|
+
// self-APPROVE), so treat it as non-binding here — an empty self-
|
|
159576
|
+
// approve is then skipped (the verdict is still recorded above)
|
|
159577
|
+
// instead of POSTing an empty COMMENT that GitHub 422s.
|
|
159578
|
+
prApproveEnabled: ctx.prApproveEnabled && !selfAuthored
|
|
159568
159579
|
});
|
|
159569
159580
|
if (skip) {
|
|
159570
159581
|
log.info(`skipping review submission: ${skip.reason}`);
|
|
159571
159582
|
return { success: true, skipped: true, reason: skip.reason };
|
|
159572
159583
|
}
|
|
159573
159584
|
let event = approved ? "APPROVE" : request_changes ? "REQUEST_CHANGES" : "COMMENT";
|
|
159574
|
-
if (
|
|
159575
|
-
|
|
159576
|
-
|
|
159585
|
+
if (event === "APPROVE" || event === "REQUEST_CHANGES") {
|
|
159586
|
+
if (!ctx.prApproveEnabled) {
|
|
159587
|
+
log.info(`prApproveEnabled is disabled \u2014 downgrading ${event} to COMMENT`);
|
|
159588
|
+
event = "COMMENT";
|
|
159589
|
+
} else if (selfAuthored) {
|
|
159590
|
+
log.info(
|
|
159591
|
+
`self-authored PR \u2014 downgrading binding ${event} to COMMENT (verdict recorded internally)`
|
|
159592
|
+
);
|
|
159593
|
+
event = "COMMENT";
|
|
159594
|
+
}
|
|
159577
159595
|
}
|
|
159578
159596
|
const params = {
|
|
159579
159597
|
owner: ctx.repo.owner,
|
|
@@ -159585,12 +159603,7 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
159585
159603
|
if (commit_id) {
|
|
159586
159604
|
params.commit_id = commit_id;
|
|
159587
159605
|
} else {
|
|
159588
|
-
|
|
159589
|
-
owner: ctx.repo.owner,
|
|
159590
|
-
repo: ctx.repo.name,
|
|
159591
|
-
pull_number
|
|
159592
|
-
});
|
|
159593
|
-
latestHeadSha = pr.data.head.sha;
|
|
159606
|
+
latestHeadSha = prSnapshot.head.sha;
|
|
159594
159607
|
params.commit_id = primary.checkoutSha ?? latestHeadSha;
|
|
159595
159608
|
if (primary.checkoutSha && latestHeadSha !== primary.checkoutSha) {
|
|
159596
159609
|
log.info(
|
|
@@ -159658,6 +159671,7 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
159658
159671
|
}
|
|
159659
159672
|
)();
|
|
159660
159673
|
} catch (err) {
|
|
159674
|
+
if (ctx.toolState.approval) ctx.toolState.approval.wouldApprove = false;
|
|
159661
159675
|
if (isTransientReviewError(err)) {
|
|
159662
159676
|
const rawMsg2 = err instanceof Error ? err.message : String(err);
|
|
159663
159677
|
throw new Error(
|
|
@@ -165545,6 +165559,7 @@ var defaultSettings = {
|
|
|
165545
165559
|
push: "restricted",
|
|
165546
165560
|
shell: "restricted",
|
|
165547
165561
|
prApproveEnabled: false,
|
|
165562
|
+
autoMergeEnabled: false,
|
|
165548
165563
|
signedCommits: false,
|
|
165549
165564
|
modeInstructions: {},
|
|
165550
165565
|
learnings: null,
|
|
@@ -165865,6 +165880,108 @@ ${genericBody}`,
|
|
|
165865
165880
|
// utils/runLifecycle.ts
|
|
165866
165881
|
var core10 = __toESM(require_core(), 1);
|
|
165867
165882
|
|
|
165883
|
+
// utils/checksGate.ts
|
|
165884
|
+
var OK_CONCLUSIONS = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
|
|
165885
|
+
var PULLFROG_CHECKS = /* @__PURE__ */ new Set(["pullfrog", "pullfrog-approval"]);
|
|
165886
|
+
function checkRunsGate(params) {
|
|
165887
|
+
for (const run4 of params.checkRuns) {
|
|
165888
|
+
if (PULLFROG_CHECKS.has(run4.name.toLowerCase())) continue;
|
|
165889
|
+
if (run4.status !== "completed")
|
|
165890
|
+
return { ok: false, reason: `check "${run4.name}" ${run4.status}` };
|
|
165891
|
+
if (!OK_CONCLUSIONS.has(run4.conclusion ?? "")) {
|
|
165892
|
+
return { ok: false, reason: `check "${run4.name}" concluded ${run4.conclusion}` };
|
|
165893
|
+
}
|
|
165894
|
+
}
|
|
165895
|
+
return { ok: true, reason: "all checks passed" };
|
|
165896
|
+
}
|
|
165897
|
+
function hasVerifiedCheck(params) {
|
|
165898
|
+
return params.checkRuns.some(
|
|
165899
|
+
(r) => !PULLFROG_CHECKS.has(r.name.toLowerCase()) && r.status === "completed"
|
|
165900
|
+
);
|
|
165901
|
+
}
|
|
165902
|
+
|
|
165903
|
+
// utils/autoMerge.ts
|
|
165904
|
+
var NON_MERGEABLE_STATES = /* @__PURE__ */ new Set(["dirty", "behind", "blocked"]);
|
|
165905
|
+
function hasBlockingHumanReview(reviews) {
|
|
165906
|
+
const latestByReviewer = /* @__PURE__ */ new Map();
|
|
165907
|
+
for (const review of reviews) {
|
|
165908
|
+
const login = review.user?.login;
|
|
165909
|
+
if (!login || isPullfrog(login)) continue;
|
|
165910
|
+
if (review.state === "APPROVED" || review.state === "CHANGES_REQUESTED") {
|
|
165911
|
+
latestByReviewer.set(login, review.state);
|
|
165912
|
+
} else if (review.state === "DISMISSED") {
|
|
165913
|
+
latestByReviewer.delete(login);
|
|
165914
|
+
}
|
|
165915
|
+
}
|
|
165916
|
+
for (const state of latestByReviewer.values()) {
|
|
165917
|
+
if (state === "CHANGES_REQUESTED") return true;
|
|
165918
|
+
}
|
|
165919
|
+
return false;
|
|
165920
|
+
}
|
|
165921
|
+
async function autoMergeAfterApprove(ctx) {
|
|
165922
|
+
if (!ctx.autoMergeEnabled) return;
|
|
165923
|
+
if (!ctx.prApproveEnabled) return;
|
|
165924
|
+
const approval = ctx.toolState.approval;
|
|
165925
|
+
if (!approval?.wouldApprove) return;
|
|
165926
|
+
const pullNumber = ctx.payload.event.issue_number;
|
|
165927
|
+
if (typeof pullNumber !== "number") return;
|
|
165928
|
+
const skip = (reason) => log.info(
|
|
165929
|
+
`autoMerge: pr=#${pullNumber} approvalSha=${approval.sha?.slice(0, 7)} \u2192 skipped: ${reason}`
|
|
165930
|
+
);
|
|
165931
|
+
const pr = await ctx.octokit.rest.pulls.get({
|
|
165932
|
+
owner: ctx.repo.owner,
|
|
165933
|
+
repo: ctx.repo.name,
|
|
165934
|
+
pull_number: pullNumber
|
|
165935
|
+
});
|
|
165936
|
+
if (pr.data.state !== "open") return skip("pr not open");
|
|
165937
|
+
if (pr.data.draft) return skip("pr is draft");
|
|
165938
|
+
const headSha = pr.data.head.sha;
|
|
165939
|
+
if (approval.sha !== headSha) return skip(`approval sha != head ${headSha.slice(0, 7)}`);
|
|
165940
|
+
const outstanding = await countOutstandingPullfrogThreads(ctx, pullNumber);
|
|
165941
|
+
if (outstanding > 0) return skip(`${outstanding} unresolved Pullfrog thread(s)`);
|
|
165942
|
+
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
|
165943
|
+
owner: ctx.repo.owner,
|
|
165944
|
+
repo: ctx.repo.name,
|
|
165945
|
+
pull_number: pullNumber,
|
|
165946
|
+
per_page: 100
|
|
165947
|
+
});
|
|
165948
|
+
if (hasBlockingHumanReview(reviews)) return skip("human changes requested");
|
|
165949
|
+
if (pr.data.mergeable !== true) return skip(`not mergeable (mergeable=${pr.data.mergeable})`);
|
|
165950
|
+
if (pr.data.mergeable_state && NON_MERGEABLE_STATES.has(pr.data.mergeable_state)) {
|
|
165951
|
+
return skip(`mergeable_state=${pr.data.mergeable_state}`);
|
|
165952
|
+
}
|
|
165953
|
+
const checkRuns = await ctx.octokit.paginate(ctx.octokit.rest.checks.listForRef, {
|
|
165954
|
+
owner: ctx.repo.owner,
|
|
165955
|
+
repo: ctx.repo.name,
|
|
165956
|
+
ref: headSha,
|
|
165957
|
+
per_page: 100
|
|
165958
|
+
});
|
|
165959
|
+
const gate = checkRunsGate({ checkRuns });
|
|
165960
|
+
if (!gate.ok) return skip(`ci: ${gate.reason}`);
|
|
165961
|
+
if (!hasVerifiedCheck({ checkRuns })) return skip("no completed external check-run to verify");
|
|
165962
|
+
const merge4 = await ctx.octokit.rest.pulls.merge({
|
|
165963
|
+
owner: ctx.repo.owner,
|
|
165964
|
+
repo: ctx.repo.name,
|
|
165965
|
+
pull_number: pullNumber,
|
|
165966
|
+
sha: headSha,
|
|
165967
|
+
merge_method: "squash",
|
|
165968
|
+
commit_title: `${pr.data.title} (#${pullNumber})`,
|
|
165969
|
+
commit_message: "merged autonomously by Pullfrog after a clean approval."
|
|
165970
|
+
});
|
|
165971
|
+
log.info(
|
|
165972
|
+
`autoMerge: pr=#${pullNumber} approvalSha=${headSha.slice(0, 7)} outstanding=0 \u2192 merged ${merge4.data.sha?.slice(0, 7)}`
|
|
165973
|
+
);
|
|
165974
|
+
await ctx.octokit.rest.issues.createComment({
|
|
165975
|
+
owner: ctx.repo.owner,
|
|
165976
|
+
repo: ctx.repo.name,
|
|
165977
|
+
issue_number: pullNumber,
|
|
165978
|
+
body: addFooter(
|
|
165979
|
+
ctx,
|
|
165980
|
+
"> \u2705 Merged autonomously after Pullfrog approved this PR and resolved all of its own review threads."
|
|
165981
|
+
)
|
|
165982
|
+
}).catch((err) => log.debug(`autoMerge comment failed: ${err}`));
|
|
165983
|
+
}
|
|
165984
|
+
|
|
165868
165985
|
// utils/reviewCleanup.ts
|
|
165869
165986
|
var RE_REVIEW_PREAMBLE = "Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
|
|
165870
165987
|
async function postReviewCleanup(ctx) {
|
|
@@ -166045,6 +166162,11 @@ async function finalizeSuccessRun(input) {
|
|
|
166045
166162
|
await approveAfterFix(input.toolContext).catch((error49) => {
|
|
166046
166163
|
log.debug(`fix auto-approval failed: ${error49}`);
|
|
166047
166164
|
});
|
|
166165
|
+
await autoMergeAfterApprove(input.toolContext).catch((error49) => {
|
|
166166
|
+
const raced = typeof error49 === "object" && error49 !== null && "status" in error49 && error49.status === 409;
|
|
166167
|
+
if (raced) log.debug(`auto-merge skipped (head moved): ${error49}`);
|
|
166168
|
+
else log.warning(`auto-merge failed: ${error49}`);
|
|
166169
|
+
});
|
|
166048
166170
|
}
|
|
166049
166171
|
await reportStatusChecks(input.toolContext, { runSucceeded: input.result.success });
|
|
166050
166172
|
}
|
|
@@ -166486,6 +166608,7 @@ async function main() {
|
|
|
166486
166608
|
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
|
|
166487
166609
|
prepushScript: runContext.repoSettings.prepushScript,
|
|
166488
166610
|
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
|
|
166611
|
+
autoMergeEnabled: runContext.repoSettings.autoMergeEnabled,
|
|
166489
166612
|
signedCommits: runContext.repoSettings.signedCommits,
|
|
166490
166613
|
modeInstructions: runContext.repoSettings.modeInstructions,
|
|
166491
166614
|
toolState,
|
|
@@ -167642,7 +167765,7 @@ async function run2() {
|
|
|
167642
167765
|
}
|
|
167643
167766
|
|
|
167644
167767
|
// cli.ts
|
|
167645
|
-
var VERSION10 = "0.1.
|
|
167768
|
+
var VERSION10 = "0.1.33";
|
|
167646
167769
|
var bin = basename2(process.argv[1] || "");
|
|
167647
167770
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
167648
167771
|
var rawArgs = process.argv.slice(2);
|
package/dist/index.js
CHANGED
|
@@ -101041,7 +101041,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
101041
101041
|
// package.json
|
|
101042
101042
|
var package_default = {
|
|
101043
101043
|
name: "pullfrog",
|
|
101044
|
-
version: "0.1.
|
|
101044
|
+
version: "0.1.33",
|
|
101045
101045
|
type: "module",
|
|
101046
101046
|
bin: {
|
|
101047
101047
|
pullfrog: "dist/cli.mjs",
|
|
@@ -157622,13 +157622,14 @@ async function approveAfterFix(ctx) {
|
|
|
157622
157622
|
});
|
|
157623
157623
|
if (pr.data.state !== "open") return;
|
|
157624
157624
|
const headSha = pr.data.head.sha;
|
|
157625
|
+
const selfAuthored = isPullfrog(pr.data.user?.login);
|
|
157625
157626
|
const summary2 = ctx.toolState.lastProgressBody?.trim();
|
|
157626
157627
|
const body = "> \u2705 Pullfrog addressed all of its review feedback on this PR.\n\n" + (summary2 || "all Pullfrog-raised review threads are resolved \u2014 no outstanding findings remain.");
|
|
157627
157628
|
const params = {
|
|
157628
157629
|
owner: ctx.repo.owner,
|
|
157629
157630
|
repo: ctx.repo.name,
|
|
157630
157631
|
pull_number: pullNumber,
|
|
157631
|
-
event: "APPROVE",
|
|
157632
|
+
event: selfAuthored ? "COMMENT" : "APPROVE",
|
|
157632
157633
|
commit_id: headSha
|
|
157633
157634
|
};
|
|
157634
157635
|
const result = await createAndSubmitWithFooter(ctx, params, {
|
|
@@ -157800,22 +157801,39 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
157800
157801
|
);
|
|
157801
157802
|
}
|
|
157802
157803
|
}
|
|
157804
|
+
const prSnapshot = (await ctx.octokit.rest.pulls.get({
|
|
157805
|
+
owner: ctx.repo.owner,
|
|
157806
|
+
repo: ctx.repo.name,
|
|
157807
|
+
pull_number
|
|
157808
|
+
})).data;
|
|
157809
|
+
const selfAuthored = isPullfrog(prSnapshot.user?.login);
|
|
157803
157810
|
ctx.toolState.approval = { wouldApprove: approved === true, sha: primary.checkoutSha };
|
|
157804
157811
|
const skip = reviewSkipDecision({
|
|
157805
157812
|
approved: approved ?? false,
|
|
157806
157813
|
requestChanges: request_changes ?? false,
|
|
157807
157814
|
body,
|
|
157808
157815
|
hasComments: comments.length > 0,
|
|
157809
|
-
|
|
157816
|
+
// a self-authored approve downgrades to COMMENT (GitHub blocks a
|
|
157817
|
+
// self-APPROVE), so treat it as non-binding here — an empty self-
|
|
157818
|
+
// approve is then skipped (the verdict is still recorded above)
|
|
157819
|
+
// instead of POSTing an empty COMMENT that GitHub 422s.
|
|
157820
|
+
prApproveEnabled: ctx.prApproveEnabled && !selfAuthored
|
|
157810
157821
|
});
|
|
157811
157822
|
if (skip) {
|
|
157812
157823
|
log.info(`skipping review submission: ${skip.reason}`);
|
|
157813
157824
|
return { success: true, skipped: true, reason: skip.reason };
|
|
157814
157825
|
}
|
|
157815
157826
|
let event = approved ? "APPROVE" : request_changes ? "REQUEST_CHANGES" : "COMMENT";
|
|
157816
|
-
if (
|
|
157817
|
-
|
|
157818
|
-
|
|
157827
|
+
if (event === "APPROVE" || event === "REQUEST_CHANGES") {
|
|
157828
|
+
if (!ctx.prApproveEnabled) {
|
|
157829
|
+
log.info(`prApproveEnabled is disabled \u2014 downgrading ${event} to COMMENT`);
|
|
157830
|
+
event = "COMMENT";
|
|
157831
|
+
} else if (selfAuthored) {
|
|
157832
|
+
log.info(
|
|
157833
|
+
`self-authored PR \u2014 downgrading binding ${event} to COMMENT (verdict recorded internally)`
|
|
157834
|
+
);
|
|
157835
|
+
event = "COMMENT";
|
|
157836
|
+
}
|
|
157819
157837
|
}
|
|
157820
157838
|
const params = {
|
|
157821
157839
|
owner: ctx.repo.owner,
|
|
@@ -157827,12 +157845,7 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
157827
157845
|
if (commit_id) {
|
|
157828
157846
|
params.commit_id = commit_id;
|
|
157829
157847
|
} else {
|
|
157830
|
-
|
|
157831
|
-
owner: ctx.repo.owner,
|
|
157832
|
-
repo: ctx.repo.name,
|
|
157833
|
-
pull_number
|
|
157834
|
-
});
|
|
157835
|
-
latestHeadSha = pr.data.head.sha;
|
|
157848
|
+
latestHeadSha = prSnapshot.head.sha;
|
|
157836
157849
|
params.commit_id = primary.checkoutSha ?? latestHeadSha;
|
|
157837
157850
|
if (primary.checkoutSha && latestHeadSha !== primary.checkoutSha) {
|
|
157838
157851
|
log.info(
|
|
@@ -157900,6 +157913,7 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
157900
157913
|
}
|
|
157901
157914
|
)();
|
|
157902
157915
|
} catch (err) {
|
|
157916
|
+
if (ctx.toolState.approval) ctx.toolState.approval.wouldApprove = false;
|
|
157903
157917
|
if (isTransientReviewError(err)) {
|
|
157904
157918
|
const rawMsg2 = err instanceof Error ? err.message : String(err);
|
|
157905
157919
|
throw new Error(
|
|
@@ -163787,6 +163801,7 @@ var defaultSettings = {
|
|
|
163787
163801
|
push: "restricted",
|
|
163788
163802
|
shell: "restricted",
|
|
163789
163803
|
prApproveEnabled: false,
|
|
163804
|
+
autoMergeEnabled: false,
|
|
163790
163805
|
signedCommits: false,
|
|
163791
163806
|
modeInstructions: {},
|
|
163792
163807
|
learnings: null,
|
|
@@ -164107,6 +164122,108 @@ ${genericBody}`,
|
|
|
164107
164122
|
// utils/runLifecycle.ts
|
|
164108
164123
|
var core10 = __toESM(require_core(), 1);
|
|
164109
164124
|
|
|
164125
|
+
// utils/checksGate.ts
|
|
164126
|
+
var OK_CONCLUSIONS = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
|
|
164127
|
+
var PULLFROG_CHECKS = /* @__PURE__ */ new Set(["pullfrog", "pullfrog-approval"]);
|
|
164128
|
+
function checkRunsGate(params) {
|
|
164129
|
+
for (const run of params.checkRuns) {
|
|
164130
|
+
if (PULLFROG_CHECKS.has(run.name.toLowerCase())) continue;
|
|
164131
|
+
if (run.status !== "completed")
|
|
164132
|
+
return { ok: false, reason: `check "${run.name}" ${run.status}` };
|
|
164133
|
+
if (!OK_CONCLUSIONS.has(run.conclusion ?? "")) {
|
|
164134
|
+
return { ok: false, reason: `check "${run.name}" concluded ${run.conclusion}` };
|
|
164135
|
+
}
|
|
164136
|
+
}
|
|
164137
|
+
return { ok: true, reason: "all checks passed" };
|
|
164138
|
+
}
|
|
164139
|
+
function hasVerifiedCheck(params) {
|
|
164140
|
+
return params.checkRuns.some(
|
|
164141
|
+
(r) => !PULLFROG_CHECKS.has(r.name.toLowerCase()) && r.status === "completed"
|
|
164142
|
+
);
|
|
164143
|
+
}
|
|
164144
|
+
|
|
164145
|
+
// utils/autoMerge.ts
|
|
164146
|
+
var NON_MERGEABLE_STATES = /* @__PURE__ */ new Set(["dirty", "behind", "blocked"]);
|
|
164147
|
+
function hasBlockingHumanReview(reviews) {
|
|
164148
|
+
const latestByReviewer = /* @__PURE__ */ new Map();
|
|
164149
|
+
for (const review of reviews) {
|
|
164150
|
+
const login = review.user?.login;
|
|
164151
|
+
if (!login || isPullfrog(login)) continue;
|
|
164152
|
+
if (review.state === "APPROVED" || review.state === "CHANGES_REQUESTED") {
|
|
164153
|
+
latestByReviewer.set(login, review.state);
|
|
164154
|
+
} else if (review.state === "DISMISSED") {
|
|
164155
|
+
latestByReviewer.delete(login);
|
|
164156
|
+
}
|
|
164157
|
+
}
|
|
164158
|
+
for (const state of latestByReviewer.values()) {
|
|
164159
|
+
if (state === "CHANGES_REQUESTED") return true;
|
|
164160
|
+
}
|
|
164161
|
+
return false;
|
|
164162
|
+
}
|
|
164163
|
+
async function autoMergeAfterApprove(ctx) {
|
|
164164
|
+
if (!ctx.autoMergeEnabled) return;
|
|
164165
|
+
if (!ctx.prApproveEnabled) return;
|
|
164166
|
+
const approval = ctx.toolState.approval;
|
|
164167
|
+
if (!approval?.wouldApprove) return;
|
|
164168
|
+
const pullNumber = ctx.payload.event.issue_number;
|
|
164169
|
+
if (typeof pullNumber !== "number") return;
|
|
164170
|
+
const skip = (reason) => log.info(
|
|
164171
|
+
`autoMerge: pr=#${pullNumber} approvalSha=${approval.sha?.slice(0, 7)} \u2192 skipped: ${reason}`
|
|
164172
|
+
);
|
|
164173
|
+
const pr = await ctx.octokit.rest.pulls.get({
|
|
164174
|
+
owner: ctx.repo.owner,
|
|
164175
|
+
repo: ctx.repo.name,
|
|
164176
|
+
pull_number: pullNumber
|
|
164177
|
+
});
|
|
164178
|
+
if (pr.data.state !== "open") return skip("pr not open");
|
|
164179
|
+
if (pr.data.draft) return skip("pr is draft");
|
|
164180
|
+
const headSha = pr.data.head.sha;
|
|
164181
|
+
if (approval.sha !== headSha) return skip(`approval sha != head ${headSha.slice(0, 7)}`);
|
|
164182
|
+
const outstanding = await countOutstandingPullfrogThreads(ctx, pullNumber);
|
|
164183
|
+
if (outstanding > 0) return skip(`${outstanding} unresolved Pullfrog thread(s)`);
|
|
164184
|
+
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
|
164185
|
+
owner: ctx.repo.owner,
|
|
164186
|
+
repo: ctx.repo.name,
|
|
164187
|
+
pull_number: pullNumber,
|
|
164188
|
+
per_page: 100
|
|
164189
|
+
});
|
|
164190
|
+
if (hasBlockingHumanReview(reviews)) return skip("human changes requested");
|
|
164191
|
+
if (pr.data.mergeable !== true) return skip(`not mergeable (mergeable=${pr.data.mergeable})`);
|
|
164192
|
+
if (pr.data.mergeable_state && NON_MERGEABLE_STATES.has(pr.data.mergeable_state)) {
|
|
164193
|
+
return skip(`mergeable_state=${pr.data.mergeable_state}`);
|
|
164194
|
+
}
|
|
164195
|
+
const checkRuns = await ctx.octokit.paginate(ctx.octokit.rest.checks.listForRef, {
|
|
164196
|
+
owner: ctx.repo.owner,
|
|
164197
|
+
repo: ctx.repo.name,
|
|
164198
|
+
ref: headSha,
|
|
164199
|
+
per_page: 100
|
|
164200
|
+
});
|
|
164201
|
+
const gate = checkRunsGate({ checkRuns });
|
|
164202
|
+
if (!gate.ok) return skip(`ci: ${gate.reason}`);
|
|
164203
|
+
if (!hasVerifiedCheck({ checkRuns })) return skip("no completed external check-run to verify");
|
|
164204
|
+
const merge4 = await ctx.octokit.rest.pulls.merge({
|
|
164205
|
+
owner: ctx.repo.owner,
|
|
164206
|
+
repo: ctx.repo.name,
|
|
164207
|
+
pull_number: pullNumber,
|
|
164208
|
+
sha: headSha,
|
|
164209
|
+
merge_method: "squash",
|
|
164210
|
+
commit_title: `${pr.data.title} (#${pullNumber})`,
|
|
164211
|
+
commit_message: "merged autonomously by Pullfrog after a clean approval."
|
|
164212
|
+
});
|
|
164213
|
+
log.info(
|
|
164214
|
+
`autoMerge: pr=#${pullNumber} approvalSha=${headSha.slice(0, 7)} outstanding=0 \u2192 merged ${merge4.data.sha?.slice(0, 7)}`
|
|
164215
|
+
);
|
|
164216
|
+
await ctx.octokit.rest.issues.createComment({
|
|
164217
|
+
owner: ctx.repo.owner,
|
|
164218
|
+
repo: ctx.repo.name,
|
|
164219
|
+
issue_number: pullNumber,
|
|
164220
|
+
body: addFooter(
|
|
164221
|
+
ctx,
|
|
164222
|
+
"> \u2705 Merged autonomously after Pullfrog approved this PR and resolved all of its own review threads."
|
|
164223
|
+
)
|
|
164224
|
+
}).catch((err) => log.debug(`autoMerge comment failed: ${err}`));
|
|
164225
|
+
}
|
|
164226
|
+
|
|
164110
164227
|
// utils/reviewCleanup.ts
|
|
164111
164228
|
var RE_REVIEW_PREAMBLE = "Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
|
|
164112
164229
|
async function postReviewCleanup(ctx) {
|
|
@@ -164287,6 +164404,11 @@ async function finalizeSuccessRun(input) {
|
|
|
164287
164404
|
await approveAfterFix(input.toolContext).catch((error49) => {
|
|
164288
164405
|
log.debug(`fix auto-approval failed: ${error49}`);
|
|
164289
164406
|
});
|
|
164407
|
+
await autoMergeAfterApprove(input.toolContext).catch((error49) => {
|
|
164408
|
+
const raced = typeof error49 === "object" && error49 !== null && "status" in error49 && error49.status === 409;
|
|
164409
|
+
if (raced) log.debug(`auto-merge skipped (head moved): ${error49}`);
|
|
164410
|
+
else log.warning(`auto-merge failed: ${error49}`);
|
|
164411
|
+
});
|
|
164290
164412
|
}
|
|
164291
164413
|
await reportStatusChecks(input.toolContext, { runSucceeded: input.result.success });
|
|
164292
164414
|
}
|
|
@@ -164728,6 +164850,7 @@ async function main() {
|
|
|
164728
164850
|
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
|
|
164729
164851
|
prepushScript: runContext.repoSettings.prepushScript,
|
|
164730
164852
|
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
|
|
164853
|
+
autoMergeEnabled: runContext.repoSettings.autoMergeEnabled,
|
|
164731
164854
|
signedCommits: runContext.repoSettings.signedCommits,
|
|
164732
164855
|
modeInstructions: runContext.repoSettings.modeInstructions,
|
|
164733
164856
|
toolState,
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type { Mode } from "../modes.ts";
|
|
|
8
8
|
export { modes } from "../modes.ts";
|
|
9
9
|
export type { BuildPullfrogFooterParams, WorkflowRunFooterInfo, } from "../utils/buildPullfrogFooter.ts";
|
|
10
10
|
export { buildPullfrogFooter, PULLFROG_DIVIDER, stripExistingFooter, } from "../utils/buildPullfrogFooter.ts";
|
|
11
|
+
export { checkRunsGate, hasVerifiedCheck } from "../utils/checksGate.ts";
|
|
11
12
|
export type { CodexAuthBody } from "../utils/codexOAuth.ts";
|
|
12
13
|
export { decodeJwtExpMs, OAuthInvalidGrantError, parseCodexAuthBody, refreshCodexAuthBody, stringifyCodexAuthBody, } from "../utils/codexOAuth.ts";
|
|
13
14
|
export type { ResourceUsage, UsageSummary } from "../utils/github.ts";
|
package/dist/internal.js
CHANGED
|
@@ -1245,6 +1245,26 @@ function stripExistingFooter(body) {
|
|
|
1245
1245
|
return body.substring(0, dividerIndex).trimEnd();
|
|
1246
1246
|
}
|
|
1247
1247
|
|
|
1248
|
+
// utils/checksGate.ts
|
|
1249
|
+
var OK_CONCLUSIONS = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
|
|
1250
|
+
var PULLFROG_CHECKS = /* @__PURE__ */ new Set(["pullfrog", "pullfrog-approval"]);
|
|
1251
|
+
function checkRunsGate(params) {
|
|
1252
|
+
for (const run of params.checkRuns) {
|
|
1253
|
+
if (PULLFROG_CHECKS.has(run.name.toLowerCase())) continue;
|
|
1254
|
+
if (run.status !== "completed")
|
|
1255
|
+
return { ok: false, reason: `check "${run.name}" ${run.status}` };
|
|
1256
|
+
if (!OK_CONCLUSIONS.has(run.conclusion ?? "")) {
|
|
1257
|
+
return { ok: false, reason: `check "${run.name}" concluded ${run.conclusion}` };
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return { ok: true, reason: "all checks passed" };
|
|
1261
|
+
}
|
|
1262
|
+
function hasVerifiedCheck(params) {
|
|
1263
|
+
return params.checkRuns.some(
|
|
1264
|
+
(r) => !PULLFROG_CHECKS.has(r.name.toLowerCase()) && r.status === "completed"
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1248
1268
|
// utils/codexOAuth.ts
|
|
1249
1269
|
var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
1250
1270
|
var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
@@ -1472,6 +1492,7 @@ export {
|
|
|
1472
1492
|
PULLFROG_DIVIDER,
|
|
1473
1493
|
TIMEOUT_DISABLED,
|
|
1474
1494
|
buildPullfrogFooter,
|
|
1495
|
+
checkRunsGate,
|
|
1475
1496
|
createLeapingProgressComment,
|
|
1476
1497
|
decodeJwtExpMs,
|
|
1477
1498
|
defaultAutoTier,
|
|
@@ -1482,6 +1503,7 @@ export {
|
|
|
1482
1503
|
getModelProvider,
|
|
1483
1504
|
getProgressComment,
|
|
1484
1505
|
getProviderDisplayName,
|
|
1506
|
+
hasVerifiedCheck,
|
|
1485
1507
|
isAutoTier,
|
|
1486
1508
|
isCardGatedModel,
|
|
1487
1509
|
isLeapingIntoActionCommentBody,
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ToolContext } from "../mcp/server.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Run-end lifecycle action: merge ANY open PR that this run mechanically
|
|
4
|
+
* approved on its current head — Pullfrog acting as a full repo maintainer,
|
|
5
|
+
* contributor PRs included, not just merging its own work. There is deliberately
|
|
6
|
+
* NO agent-callable merge tool — the merge is a pure, deterministic consequence
|
|
7
|
+
* of the runtime observing a clean approved state, so a prompt-injected agent has
|
|
8
|
+
* no merge surface to reach for. Every clause is fail-closed and re-verified
|
|
9
|
+
* against GitHub's own state at merge time; any failure logs why and returns
|
|
10
|
+
* without merging. Best-effort — the caller wraps this in a `.catch`, and a
|
|
11
|
+
* merge/comment failure can never flip the run's outcome.
|
|
12
|
+
*
|
|
13
|
+
* There is NO self-authorship restriction: the approval verdict IS the trust
|
|
14
|
+
* decision (Pullfrog reviewed it and would approve), exactly like a human
|
|
15
|
+
* maintainer merging a contributor PR. The population of mergeable PRs is bounded
|
|
16
|
+
* upstream by the review triggers (`prCreated` / `prCreatedAllowNonCollaborator`)
|
|
17
|
+
* — Pullfrog can only merge what it was configured to review + approve — and the
|
|
18
|
+
* whole capability is opt-in per repo (clause 1). The remaining clauses are the
|
|
19
|
+
* maintainer's controls.
|
|
20
|
+
*
|
|
21
|
+
* The invariant (all must hold):
|
|
22
|
+
* 1. `ctx.autoMergeEnabled` — the per-repo toggle ANDed with the global
|
|
23
|
+
* `isAutonomousMaintenanceEnabled()` kill switch, server-side (run-context).
|
|
24
|
+
* 2. `ctx.prApproveEnabled` — cannot auto-merge what it may not approve.
|
|
25
|
+
* 3. this run recorded an APPROVE verdict (`toolState.approval.wouldApprove`) —
|
|
26
|
+
* Pullfrog's review is the merge decision.
|
|
27
|
+
* 4. the PR is open and not a draft.
|
|
28
|
+
* 5. `toolState.approval.sha === <current head sha>` — approved THIS head, so a
|
|
29
|
+
* commit pushed after the review can't ride the approval in (409-safe below).
|
|
30
|
+
* 6. `countOutstandingPullfrogThreads === 0` — re-queried at merge time.
|
|
31
|
+
* 7. no un-dismissed human CHANGES_REQUESTED — the human veto always wins.
|
|
32
|
+
* 8. GitHub reports the PR mergeable and not blocked/dirty/behind.
|
|
33
|
+
* 9. every external check-run/commit-status on the head is complete and
|
|
34
|
+
* non-failing (`checkRunsGate`) — red or in-flight CI never auto-merges,
|
|
35
|
+
* even on a repo without branch protection.
|
|
36
|
+
*/
|
|
37
|
+
export declare function autoMergeAfterApprove(ctx: ToolContext): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, dependency-free CI green/red predicate — the single source of truth for
|
|
3
|
+
* "given these check-runs, is this PR head safe to merge?". Consumed by the
|
|
4
|
+
* auto-merge path: `autoMerge.ts` gates the PR head at merge time, and the
|
|
5
|
+
* `pr-merge-completion` webhook re-checks it before dispatching a slow-CI re-wake.
|
|
6
|
+
* A red or in-flight sha never merges, and a sha with no confirmable green
|
|
7
|
+
* check-run is treated as unverified (refused, not merged blind).
|
|
8
|
+
*
|
|
9
|
+
* Gates on the Checks API only. The Pullfrog App deliberately holds no `statuses`
|
|
10
|
+
* permission (requesting it would force re-approval on every install — see
|
|
11
|
+
* wiki/app-permissions.md), so the legacy Commit Statuses API is unreadable
|
|
12
|
+
* (`listCommitStatusesForRef` 403s). Modern CI (GitHub Actions, etc.) reports via
|
|
13
|
+
* check-runs; a repo relying on the legacy Statuses API is covered by branch
|
|
14
|
+
* protection instead — a failing required status makes `mergeable_state` `blocked`,
|
|
15
|
+
* which the merge invariant already refuses (`NON_MERGEABLE_STATES`).
|
|
16
|
+
*/
|
|
17
|
+
export type CheckRun = {
|
|
18
|
+
name: string;
|
|
19
|
+
status: string;
|
|
20
|
+
conclusion: string | null;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Refuse if any external check-run is incomplete (queued/in_progress) or concluded
|
|
24
|
+
* anything other than success/neutral/skipped. Fail-closed on unknown/absent
|
|
25
|
+
* conclusions. Pure so the pass/fail logic is inspectable directly.
|
|
26
|
+
*/
|
|
27
|
+
export declare function checkRunsGate(params: {
|
|
28
|
+
checkRuns: CheckRun[];
|
|
29
|
+
}): {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
reason: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* true when the head has ≥1 CONCLUDED non-failing EXTERNAL check-run — CI
|
|
35
|
+
* positively ran, not merely "no red was seen". paired with `checkRunsGate.ok`
|
|
36
|
+
* (any completed check is non-failing), this == "≥1 concluded non-failing check".
|
|
37
|
+
* the merge requires it: a head with ZERO check-runs passes
|
|
38
|
+
* `checkRunsGate` (nothing red) but has no verification, and since the app can't
|
|
39
|
+
* read legacy commit statuses, "zero check-runs" is indistinguishable from "green
|
|
40
|
+
* Actions but a red CircleCI status" — so we only act on a head we can positively
|
|
41
|
+
* confirm is green. Pullfrog's own verdict checks don't count as external CI.
|
|
42
|
+
*/
|
|
43
|
+
export declare function hasVerifiedCheck(params: {
|
|
44
|
+
checkRuns: CheckRun[];
|
|
45
|
+
}): boolean;
|