pullfrog 0.1.32 → 0.1.34
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 +548 -159
- package/dist/index.js +149 -12
- package/dist/mcp/server.d.ts +1 -0
- package/dist/utils/autoMerge.d.ts +28 -0
- package/dist/utils/proxy.d.ts +1 -0
- package/dist/utils/runContext.d.ts +1 -0
- package/package.json +1 -1
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.34",
|
|
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(
|
|
@@ -163544,6 +163558,11 @@ async function mintProxyKey(ctx) {
|
|
|
163544
163558
|
body?.error ?? "billing service temporarily unavailable \u2014 retry shortly"
|
|
163545
163559
|
);
|
|
163546
163560
|
}
|
|
163561
|
+
if (response.status === 404) {
|
|
163562
|
+
throw new TransientError(
|
|
163563
|
+
"Pullfrog couldn't match this repository to its account \u2014 it may have just been renamed or transferred. The link refreshes automatically on the next run; if it keeps failing, reinstall the GitHub App on the repo's current owner."
|
|
163564
|
+
);
|
|
163565
|
+
}
|
|
163547
163566
|
if (!response.ok) {
|
|
163548
163567
|
log.warning(`proxy key mint failed (${response.status})`);
|
|
163549
163568
|
return null;
|
|
@@ -163787,6 +163806,7 @@ var defaultSettings = {
|
|
|
163787
163806
|
push: "restricted",
|
|
163788
163807
|
shell: "restricted",
|
|
163789
163808
|
prApproveEnabled: false,
|
|
163809
|
+
autoMergeEnabled: false,
|
|
163790
163810
|
signedCommits: false,
|
|
163791
163811
|
modeInstructions: {},
|
|
163792
163812
|
learnings: null,
|
|
@@ -164107,6 +164127,117 @@ ${genericBody}`,
|
|
|
164107
164127
|
// utils/runLifecycle.ts
|
|
164108
164128
|
var core10 = __toESM(require_core(), 1);
|
|
164109
164129
|
|
|
164130
|
+
// utils/autoMerge.ts
|
|
164131
|
+
function hasBlockingHumanReview(reviews) {
|
|
164132
|
+
const latestByReviewer = /* @__PURE__ */ new Map();
|
|
164133
|
+
for (const review of reviews) {
|
|
164134
|
+
const login = review.user?.login;
|
|
164135
|
+
if (!login || isPullfrog(login)) continue;
|
|
164136
|
+
if (review.state === "APPROVED" || review.state === "CHANGES_REQUESTED") {
|
|
164137
|
+
latestByReviewer.set(login, review.state);
|
|
164138
|
+
} else if (review.state === "DISMISSED") {
|
|
164139
|
+
latestByReviewer.delete(login);
|
|
164140
|
+
}
|
|
164141
|
+
}
|
|
164142
|
+
for (const state of latestByReviewer.values()) {
|
|
164143
|
+
if (state === "CHANGES_REQUESTED") return true;
|
|
164144
|
+
}
|
|
164145
|
+
return false;
|
|
164146
|
+
}
|
|
164147
|
+
var ENABLE_AUTO_MERGE = (
|
|
164148
|
+
/* GraphQL */
|
|
164149
|
+
`
|
|
164150
|
+
mutation ($pullRequestId: ID!, $headSha: GitObjectID!) {
|
|
164151
|
+
enablePullRequestAutoMerge(input: {
|
|
164152
|
+
pullRequestId: $pullRequestId
|
|
164153
|
+
mergeMethod: SQUASH
|
|
164154
|
+
expectedHeadOid: $headSha
|
|
164155
|
+
}) { clientMutationId }
|
|
164156
|
+
}`
|
|
164157
|
+
);
|
|
164158
|
+
var isAlreadyMergeable = (error49) => error49 instanceof Error && /clean status/i.test(error49.message);
|
|
164159
|
+
var isHeadChanged = (error49) => error49 instanceof Error && /head has changed|expected head/i.test(error49.message);
|
|
164160
|
+
var isAutoMergeDisallowed = (error49) => error49 instanceof Error && /not allowed/i.test(error49.message);
|
|
164161
|
+
async function autoMergeAfterApprove(ctx) {
|
|
164162
|
+
if (!ctx.autoMergeEnabled) return;
|
|
164163
|
+
if (!ctx.prApproveEnabled) return;
|
|
164164
|
+
const approval = ctx.toolState.approval;
|
|
164165
|
+
if (!approval?.wouldApprove) return;
|
|
164166
|
+
const pullNumber = ctx.payload.event.issue_number;
|
|
164167
|
+
if (typeof pullNumber !== "number") return;
|
|
164168
|
+
const skip = (reason) => log.info(
|
|
164169
|
+
`autoMerge: pr=#${pullNumber} approvalSha=${approval.sha?.slice(0, 7)} \u2192 skipped: ${reason}`
|
|
164170
|
+
);
|
|
164171
|
+
const comment = (body) => ctx.octokit.rest.issues.createComment({
|
|
164172
|
+
owner: ctx.repo.owner,
|
|
164173
|
+
repo: ctx.repo.name,
|
|
164174
|
+
issue_number: pullNumber,
|
|
164175
|
+
body: addFooter(ctx, body)
|
|
164176
|
+
}).catch((err) => log.debug(`autoMerge comment failed: ${err}`));
|
|
164177
|
+
const pr = await ctx.octokit.rest.pulls.get({
|
|
164178
|
+
owner: ctx.repo.owner,
|
|
164179
|
+
repo: ctx.repo.name,
|
|
164180
|
+
pull_number: pullNumber
|
|
164181
|
+
});
|
|
164182
|
+
if (pr.data.state !== "open") return skip("pr not open");
|
|
164183
|
+
if (pr.data.draft) return skip("pr is draft");
|
|
164184
|
+
if (pr.data.auto_merge) return skip("auto-merge already enabled");
|
|
164185
|
+
const headSha = pr.data.head.sha;
|
|
164186
|
+
if (approval.sha !== headSha) return skip(`approval sha != head ${headSha.slice(0, 7)}`);
|
|
164187
|
+
const outstanding = await countOutstandingPullfrogThreads(ctx, pullNumber);
|
|
164188
|
+
if (outstanding > 0) return skip(`${outstanding} unresolved Pullfrog thread(s)`);
|
|
164189
|
+
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
|
164190
|
+
owner: ctx.repo.owner,
|
|
164191
|
+
repo: ctx.repo.name,
|
|
164192
|
+
pull_number: pullNumber,
|
|
164193
|
+
per_page: 100
|
|
164194
|
+
});
|
|
164195
|
+
if (hasBlockingHumanReview(reviews)) return skip("human changes requested");
|
|
164196
|
+
try {
|
|
164197
|
+
await op(
|
|
164198
|
+
() => ctx.octokit.graphql(ENABLE_AUTO_MERGE, {
|
|
164199
|
+
pullRequestId: pr.data.node_id,
|
|
164200
|
+
headSha
|
|
164201
|
+
}),
|
|
164202
|
+
{
|
|
164203
|
+
name: "enablePullRequestAutoMerge",
|
|
164204
|
+
retries: [500, 2e3],
|
|
164205
|
+
bail: (error49) => !isTransientOctokitError(error49)
|
|
164206
|
+
}
|
|
164207
|
+
)();
|
|
164208
|
+
log.info(`autoMerge: pr=#${pullNumber} @ ${headSha.slice(0, 7)} \u2192 native auto-merge enabled`);
|
|
164209
|
+
await comment(
|
|
164210
|
+
"> \u2705 Approved \u2014 auto-merge enabled; GitHub will merge once required checks pass."
|
|
164211
|
+
);
|
|
164212
|
+
return;
|
|
164213
|
+
} catch (error49) {
|
|
164214
|
+
if (isHeadChanged(error49)) return skip("head moved during enable");
|
|
164215
|
+
if (isAutoMergeDisallowed(error49)) {
|
|
164216
|
+
await comment(
|
|
164217
|
+
"> \u26A0\uFE0F Approved, but auto-merge is off for this repo. Enable `Allow auto-merge` in Settings > General > Pull Requests."
|
|
164218
|
+
);
|
|
164219
|
+
return;
|
|
164220
|
+
}
|
|
164221
|
+
if (!isAlreadyMergeable(error49)) throw error49;
|
|
164222
|
+
}
|
|
164223
|
+
const base = await ctx.octokit.rest.repos.getBranch({ owner: ctx.repo.owner, repo: ctx.repo.name, branch: pr.data.base.ref }).catch(() => null);
|
|
164224
|
+
if (!base?.data.protected) return skip("base branch not protected \u2014 refusing CI-ungated merge");
|
|
164225
|
+
const merge4 = await ctx.octokit.rest.pulls.merge({
|
|
164226
|
+
owner: ctx.repo.owner,
|
|
164227
|
+
repo: ctx.repo.name,
|
|
164228
|
+
pull_number: pullNumber,
|
|
164229
|
+
sha: headSha,
|
|
164230
|
+
merge_method: "squash",
|
|
164231
|
+
commit_title: `${pr.data.title} (#${pullNumber})`
|
|
164232
|
+
});
|
|
164233
|
+
log.info(
|
|
164234
|
+
`autoMerge: pr=#${pullNumber} @ ${headSha.slice(0, 7)} \u2192 merged directly ${merge4.data.sha?.slice(0, 7)}`
|
|
164235
|
+
);
|
|
164236
|
+
await comment(
|
|
164237
|
+
"> \u2705 Merged autonomously after Pullfrog approved this PR and resolved all of its own review threads."
|
|
164238
|
+
);
|
|
164239
|
+
}
|
|
164240
|
+
|
|
164110
164241
|
// utils/reviewCleanup.ts
|
|
164111
164242
|
var RE_REVIEW_PREAMBLE = "Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
|
|
164112
164243
|
async function postReviewCleanup(ctx) {
|
|
@@ -164287,6 +164418,11 @@ async function finalizeSuccessRun(input) {
|
|
|
164287
164418
|
await approveAfterFix(input.toolContext).catch((error49) => {
|
|
164288
164419
|
log.debug(`fix auto-approval failed: ${error49}`);
|
|
164289
164420
|
});
|
|
164421
|
+
await autoMergeAfterApprove(input.toolContext).catch((error49) => {
|
|
164422
|
+
const raced = typeof error49 === "object" && error49 !== null && "status" in error49 && error49.status === 409;
|
|
164423
|
+
if (raced) log.debug(`auto-merge skipped (head moved): ${error49}`);
|
|
164424
|
+
else log.warning(`auto-merge failed: ${error49}`);
|
|
164425
|
+
});
|
|
164290
164426
|
}
|
|
164291
164427
|
await reportStatusChecks(input.toolContext, { runSucceeded: input.result.success });
|
|
164292
164428
|
}
|
|
@@ -164728,6 +164864,7 @@ async function main() {
|
|
|
164728
164864
|
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
|
|
164729
164865
|
prepushScript: runContext.repoSettings.prepushScript,
|
|
164730
164866
|
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
|
|
164867
|
+
autoMergeEnabled: runContext.repoSettings.autoMergeEnabled,
|
|
164731
164868
|
signedCommits: runContext.repoSettings.signedCommits,
|
|
164732
164869
|
modeInstructions: runContext.repoSettings.modeInstructions,
|
|
164733
164870
|
toolState,
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ToolContext } from "../mcp/server.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Run-end lifecycle action: hand any PR this run mechanically approved on its
|
|
4
|
+
* current head to GitHub's NATIVE auto-merge — Pullfrog acting as a full repo
|
|
5
|
+
* maintainer, contributor PRs included. There is deliberately NO agent-callable
|
|
6
|
+
* merge tool — the merge is a deterministic consequence of the recorded approval
|
|
7
|
+
* verdict, so a prompt-injected agent has no merge surface to reach for.
|
|
8
|
+
*
|
|
9
|
+
* Native auto-merge waits for all REQUIRED checks + reviews (un-fakeable by a
|
|
10
|
+
* fork), respects the human veto via branch protection, and requires branch
|
|
11
|
+
* protection to exist. We keep only the Pullfrog-specific pre-gates GitHub can't
|
|
12
|
+
* express: armed toggles, approved-THIS-head, zero outstanding Pullfrog threads,
|
|
13
|
+
* no human CHANGES_REQUESTED. Best-effort — the caller wraps this in `.catch`,
|
|
14
|
+
* and a merge/comment failure can never flip the run's outcome.
|
|
15
|
+
*
|
|
16
|
+
* The invariant (all must hold):
|
|
17
|
+
* 1. `ctx.autoMergeEnabled` — the per-repo toggle ANDed with the global
|
|
18
|
+
* `isAutonomousMaintenanceEnabled()` kill switch, server-side (run-context).
|
|
19
|
+
* 2. `ctx.prApproveEnabled` — cannot auto-merge what it may not approve.
|
|
20
|
+
* 3. this run recorded an APPROVE verdict (`toolState.approval.wouldApprove`).
|
|
21
|
+
* 4. the PR is open and not a draft.
|
|
22
|
+
* 5. `toolState.approval.sha === <current head sha>` — approved THIS head.
|
|
23
|
+
* 6. `countOutstandingPullfrogThreads === 0` — re-queried at merge time.
|
|
24
|
+
* 7. no un-dismissed human CHANGES_REQUESTED at handoff.
|
|
25
|
+
* The remaining wait-for-required-checks + wait-for-required-reviews semantics
|
|
26
|
+
* are GitHub's, via native auto-merge and branch protection.
|
|
27
|
+
*/
|
|
28
|
+
export declare function autoMergeAfterApprove(ctx: ToolContext): Promise<void>;
|
package/dist/utils/proxy.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* - 402 → `BillingError` (card declined, balance empty, 3DS, etc.)
|
|
18
18
|
* - 503 → `TransientError` (transient sync issue — retry next dispatch)
|
|
19
|
+
* - 404 → `TransientError` (stale repo↔account link — re-homes on next webhook)
|
|
19
20
|
*/
|
|
20
21
|
import type { ToolState } from "../toolState.ts";
|
|
21
22
|
import { type OidcCredentials } from "./github.ts";
|