Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,13 @@ async function sweepRepoRegate(env: Env, repoFullName: string | undefined): Prom
const duplicateWinnerEnabled = env.GITTENSORY_DUPLICATE_WINNER === "true";
for (const pr of candidates) {
const others = openPullRequests.filter((other) => other.number !== pr.number);
const advisory = buildPullRequestAdvisory(repo, pr, { otherOpenPullRequests: others, requireLinkedIssue, duplicateWinnerEnabled });
const linkedIssueAuthorLogins = await resolveLinkedIssueAuthorLogins(env, repoFullName, pr.linkedIssues);
const advisory = buildPullRequestAdvisory(repo, pr, {
otherOpenPullRequests: others,
requireLinkedIssue,
duplicateWinnerEnabled,
linkedIssueAuthorLogins,
});
const gate = evaluateGateCheck(advisory, gateCheckPolicy(settings, null, undefined, pr.slopRisk ?? null));
verdicts[String(pr.number)] = gate.conclusion;
if (gate.conclusion === "failure" || gate.conclusion === "action_required") flaggedPulls.push(pr.number);
Expand Down Expand Up @@ -3056,17 +3062,22 @@ async function authorizePrActionActor(args: {

// #824 the common "load the PR's repo context + build its advisory" step every authorized action command runs
// before its mutation. Identical across gate-override and the PR-panel retrigger.
async function buildAuthorizedPrActionAdvisory(
export async function buildAuthorizedPrActionAdvisory(
env: Env,
repoFullName: string,
pr: PullRequestRecord,
settings: RepositorySettings,
): Promise<{ repo: Awaited<ReturnType<typeof getRepository>>; advisory: ReturnType<typeof buildPullRequestAdvisory> }> {
const [repo, otherOpenPullRequests] = await Promise.all([getRepository(env, repoFullName), listOtherOpenPullRequests(env, repoFullName, pr.number)]);
const [repo, otherOpenPullRequests, linkedIssueAuthorLogins] = await Promise.all([
getRepository(env, repoFullName),
listOtherOpenPullRequests(env, repoFullName, pr.number),
resolveLinkedIssueAuthorLogins(env, repoFullName, pr.linkedIssues),
]);
const advisory = buildPullRequestAdvisory(repo, pr, {
otherOpenPullRequests,
requireLinkedIssue: shouldCollectLinkedIssueEvidence(settings),
duplicateWinnerEnabled: env.GITTENSORY_DUPLICATE_WINNER === "true",
linkedIssueAuthorLogins,
});
return { repo, advisory };
}
Expand Down
14 changes: 13 additions & 1 deletion src/rules/predicted-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,19 @@ export function buildPredictedGateVerdict(args: {
// LOSER, never the winner. So the predictor must keep showing the duplicate finding (the honest pre-submit
// answer). Threading the flag here would let isDuplicateClusterWinner(0, …) treat #0 as the winner and
// falsely suppress the block — a false-optimism regression. Do NOT add it without modeling #0 as the loser.
const advisory = buildPullRequestAdvisory(repo, syntheticPr, { otherOpenPullRequests: pullRequests, requireLinkedIssue });
const issueAuthorsByNumber = new Map(
issues
.filter((issue) => issue.repoFullName === input.repoFullName)
.map((issue) => [issue.number, issue.authorLogin]),
);
const linkedIssueAuthorLogins = syntheticPr.linkedIssues.map(
(issueNumber) => issueAuthorsByNumber.get(issueNumber) ?? null,
);
const advisory = buildPullRequestAdvisory(repo, syntheticPr, {
otherOpenPullRequests: pullRequests,
requireLinkedIssue,
linkedIssueAuthorLogins,
});

// Pack-aware (#693): under `oss-anti-slop` the gate blocks ANY author, so drop the confirmed-contributor
// gate entirely (mirrors gateCheckPolicy). `gittensor` keeps it. Pack comes from the PUBLIC .gittensory.yml.
Expand Down
18 changes: 16 additions & 2 deletions test/unit/gate-check-policy.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import { gateCheckPolicy, resolveLinkedIssueAuthorLogins, shouldCollectLinkedIssueEvidence, shouldCollectSlopEvidence, shouldRunSlopAiAdvisory } from "../../src/queue/processors";
import { buildAuthorizedPrActionAdvisory, gateCheckPolicy, resolveLinkedIssueAuthorLogins, shouldCollectLinkedIssueEvidence, shouldCollectSlopEvidence, shouldRunSlopAiAdvisory } from "../../src/queue/processors";
import { createTestEnv } from "../helpers/d1";
import { upsertIssueFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories";
import { evaluateGateCheck } from "../../src/rules/advisory";
import { parseFocusManifest, resolveEffectiveSettings } from "../../src/signals/focus-manifest";
import type { Advisory, RepositorySettings } from "../../src/types";
import type { Advisory, PullRequestRecord, RepositorySettings } from "../../src/types";

function settings(over: Partial<RepositorySettings> = {}): RepositorySettings {
return {
Expand Down Expand Up @@ -400,4 +400,18 @@ describe("resolveLinkedIssueAuthorLogins", () => {
const result = await resolveLinkedIssueAuthorLogins(brokenEnv, "owner/repo", [1]);
expect(result).toEqual([null]);
});

it("threads linked issue authors into authorized PR action advisories", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" } }, 1);
await upsertIssueFromGitHub(env, "owner/repo", { number: 12, title: "Self-authored bug", body: "", state: "open", user: { login: "miner1" }, labels: [], html_url: "https://github.com/owner/repo/issues/12", created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z" });
const pr: PullRequestRecord = { repoFullName: "owner/repo", number: 99, title: "Fix self-authored bug", state: "open", authorLogin: "Miner1", body: "Closes #12", labels: [], linkedIssues: [12] };

const { advisory } = await buildAuthorizedPrActionAdvisory(env, "owner/repo", pr, settings({ selfAuthoredLinkedIssueGateMode: "block" }));
const gate = evaluateGateCheck(advisory, gateCheckPolicy(settings({ selfAuthoredLinkedIssueGateMode: "block" }), null));

expect(advisory.findings.some((finding) => finding.code === "self_authored_linked_issue")).toBe(true);
expect(gate.conclusion).toBe("failure");
expect(gate.blockers.some((finding) => finding.code === "self_authored_linked_issue")).toBe(true);
});
});
16 changes: 14 additions & 2 deletions test/unit/predicted-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ function openPr(number: number, title: string, linkedIssues: number[] = [], auth
return { repoFullName: "acme/widgets", number, title, state: "open", authorLogin, linkedIssues, labels: [] };
}

function openIssue(number: number, title: string): IssueRecord {
return { repoFullName: "acme/widgets", number, title, state: "open", labels: [], linkedPrs: [], authorAssociation: null } as IssueRecord;
function openIssue(number: number, title: string, authorLogin?: string | null): IssueRecord {
return { repoFullName: "acme/widgets", number, title, state: "open", labels: [], linkedPrs: [], authorAssociation: null, authorLogin } as IssueRecord;
}

const BASE_INPUT: PredictedGateInput = {
Expand Down Expand Up @@ -72,6 +72,18 @@ describe("buildPredictedGateVerdict", () => {
expect(result.blockers.some((b) => b.code === "missing_linked_issue")).toBe(false);
});

it("predicts a BLOCK when the public self-authored linked-issue gate has issue-author evidence", () => {
const result = verdict({ gate: { selfAuthoredLinkedIssue: "block" }, issues: [openIssue(7, "Uploads should retry on 5xx", "Miner1")] });
expect(result.conclusion).toBe("failure");
expect(result.blockers.some((b) => b.code === "self_authored_linked_issue")).toBe(true);
});

it("does not predict the self-authored linked-issue finding when issue-author evidence is absent", () => {
const result = verdict({ gate: { selfAuthoredLinkedIssue: "block" }, issues: [openIssue(7, "Uploads should retry on 5xx")] });
expect(result.conclusion).toBe("success");
expect(result.blockers.some((b) => b.code === "self_authored_linked_issue")).toBe(false);
});

it("honors public gate.mergeReadiness when predicting blockers", () => {
const result = verdict({
gate: { duplicates: "off", mergeReadiness: "block" },
Expand Down
Loading