pullfrog 0.1.33 → 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 +459 -193
- package/dist/index.js +60 -46
- package/dist/internal/index.d.ts +0 -1
- package/dist/internal.js +0 -22
- package/dist/utils/autoMerge.d.ts +16 -25
- package/dist/utils/proxy.d.ts +1 -0
- package/package.json +1 -1
- package/dist/utils/checksGate.d.ts +0 -45
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",
|
|
@@ -163558,6 +163558,11 @@ async function mintProxyKey(ctx) {
|
|
|
163558
163558
|
body?.error ?? "billing service temporarily unavailable \u2014 retry shortly"
|
|
163559
163559
|
);
|
|
163560
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
|
+
}
|
|
163561
163566
|
if (!response.ok) {
|
|
163562
163567
|
log.warning(`proxy key mint failed (${response.status})`);
|
|
163563
163568
|
return null;
|
|
@@ -164122,28 +164127,7 @@ ${genericBody}`,
|
|
|
164122
164127
|
// utils/runLifecycle.ts
|
|
164123
164128
|
var core10 = __toESM(require_core(), 1);
|
|
164124
164129
|
|
|
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
164130
|
// utils/autoMerge.ts
|
|
164146
|
-
var NON_MERGEABLE_STATES = /* @__PURE__ */ new Set(["dirty", "behind", "blocked"]);
|
|
164147
164131
|
function hasBlockingHumanReview(reviews) {
|
|
164148
164132
|
const latestByReviewer = /* @__PURE__ */ new Map();
|
|
164149
164133
|
for (const review of reviews) {
|
|
@@ -164160,6 +164144,20 @@ function hasBlockingHumanReview(reviews) {
|
|
|
164160
164144
|
}
|
|
164161
164145
|
return false;
|
|
164162
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);
|
|
164163
164161
|
async function autoMergeAfterApprove(ctx) {
|
|
164164
164162
|
if (!ctx.autoMergeEnabled) return;
|
|
164165
164163
|
if (!ctx.prApproveEnabled) return;
|
|
@@ -164170,6 +164168,12 @@ async function autoMergeAfterApprove(ctx) {
|
|
|
164170
164168
|
const skip = (reason) => log.info(
|
|
164171
164169
|
`autoMerge: pr=#${pullNumber} approvalSha=${approval.sha?.slice(0, 7)} \u2192 skipped: ${reason}`
|
|
164172
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}`));
|
|
164173
164177
|
const pr = await ctx.octokit.rest.pulls.get({
|
|
164174
164178
|
owner: ctx.repo.owner,
|
|
164175
164179
|
repo: ctx.repo.name,
|
|
@@ -164177,6 +164181,7 @@ async function autoMergeAfterApprove(ctx) {
|
|
|
164177
164181
|
});
|
|
164178
164182
|
if (pr.data.state !== "open") return skip("pr not open");
|
|
164179
164183
|
if (pr.data.draft) return skip("pr is draft");
|
|
164184
|
+
if (pr.data.auto_merge) return skip("auto-merge already enabled");
|
|
164180
164185
|
const headSha = pr.data.head.sha;
|
|
164181
164186
|
if (approval.sha !== headSha) return skip(`approval sha != head ${headSha.slice(0, 7)}`);
|
|
164182
164187
|
const outstanding = await countOutstandingPullfrogThreads(ctx, pullNumber);
|
|
@@ -164188,40 +164193,49 @@ async function autoMergeAfterApprove(ctx) {
|
|
|
164188
164193
|
per_page: 100
|
|
164189
164194
|
});
|
|
164190
164195
|
if (hasBlockingHumanReview(reviews)) return skip("human changes requested");
|
|
164191
|
-
|
|
164192
|
-
|
|
164193
|
-
|
|
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;
|
|
164194
164222
|
}
|
|
164195
|
-
const
|
|
164196
|
-
|
|
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");
|
|
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");
|
|
164204
164225
|
const merge4 = await ctx.octokit.rest.pulls.merge({
|
|
164205
164226
|
owner: ctx.repo.owner,
|
|
164206
164227
|
repo: ctx.repo.name,
|
|
164207
164228
|
pull_number: pullNumber,
|
|
164208
164229
|
sha: headSha,
|
|
164209
164230
|
merge_method: "squash",
|
|
164210
|
-
commit_title: `${pr.data.title} (#${pullNumber})
|
|
164211
|
-
commit_message: "merged autonomously by Pullfrog after a clean approval."
|
|
164231
|
+
commit_title: `${pr.data.title} (#${pullNumber})`
|
|
164212
164232
|
});
|
|
164213
164233
|
log.info(
|
|
164214
|
-
`autoMerge: pr=#${pullNumber}
|
|
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."
|
|
164215
164238
|
);
|
|
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
164239
|
}
|
|
164226
164240
|
|
|
164227
164241
|
// utils/reviewCleanup.ts
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ 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";
|
|
12
11
|
export type { CodexAuthBody } from "../utils/codexOAuth.ts";
|
|
13
12
|
export { decodeJwtExpMs, OAuthInvalidGrantError, parseCodexAuthBody, refreshCodexAuthBody, stringifyCodexAuthBody, } from "../utils/codexOAuth.ts";
|
|
14
13
|
export type { ResourceUsage, UsageSummary } from "../utils/github.ts";
|
package/dist/internal.js
CHANGED
|
@@ -1245,26 +1245,6 @@ 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
|
-
|
|
1268
1248
|
// utils/codexOAuth.ts
|
|
1269
1249
|
var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
1270
1250
|
var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
@@ -1492,7 +1472,6 @@ export {
|
|
|
1492
1472
|
PULLFROG_DIVIDER,
|
|
1493
1473
|
TIMEOUT_DISABLED,
|
|
1494
1474
|
buildPullfrogFooter,
|
|
1495
|
-
checkRunsGate,
|
|
1496
1475
|
createLeapingProgressComment,
|
|
1497
1476
|
decodeJwtExpMs,
|
|
1498
1477
|
defaultAutoTier,
|
|
@@ -1503,7 +1482,6 @@ export {
|
|
|
1503
1482
|
getModelProvider,
|
|
1504
1483
|
getProgressComment,
|
|
1505
1484
|
getProviderDisplayName,
|
|
1506
|
-
hasVerifiedCheck,
|
|
1507
1485
|
isAutoTier,
|
|
1508
1486
|
isCardGatedModel,
|
|
1509
1487
|
isLeapingIntoActionCommentBody,
|
|
@@ -1,37 +1,28 @@
|
|
|
1
1
|
import type { ToolContext } from "../mcp/server.ts";
|
|
2
2
|
/**
|
|
3
|
-
* Run-end lifecycle action:
|
|
4
|
-
*
|
|
5
|
-
* contributor PRs included
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
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.
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* maintainer's controls.
|
|
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.
|
|
20
15
|
*
|
|
21
16
|
* The invariant (all must hold):
|
|
22
17
|
* 1. `ctx.autoMergeEnabled` — the per-repo toggle ANDed with the global
|
|
23
18
|
* `isAutonomousMaintenanceEnabled()` kill switch, server-side (run-context).
|
|
24
19
|
* 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.
|
|
20
|
+
* 3. this run recorded an APPROVE verdict (`toolState.approval.wouldApprove`).
|
|
27
21
|
* 4. the PR is open and not a draft.
|
|
28
|
-
* 5. `toolState.approval.sha === <current head sha>` — approved THIS head
|
|
29
|
-
* commit pushed after the review can't ride the approval in (409-safe below).
|
|
22
|
+
* 5. `toolState.approval.sha === <current head sha>` — approved THIS head.
|
|
30
23
|
* 6. `countOutstandingPullfrogThreads === 0` — re-queried at merge time.
|
|
31
|
-
* 7. no un-dismissed human CHANGES_REQUESTED
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* non-failing (`checkRunsGate`) — red or in-flight CI never auto-merges,
|
|
35
|
-
* even on a repo without branch protection.
|
|
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.
|
|
36
27
|
*/
|
|
37
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";
|
package/package.json
CHANGED
|
@@ -1,45 +0,0 @@
|
|
|
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;
|