From a6505190bfe862925c4e8d34114b3c94e5396ce2 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:51:57 -0700 Subject: [PATCH] fix(review): guard mixed-trust label propagation --- .../linked-issue-label-propagation-fetch.ts | 17 +++++++++++--- ...nked-issue-label-propagation-fetch.test.ts | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/review/linked-issue-label-propagation-fetch.ts b/src/review/linked-issue-label-propagation-fetch.ts index 1c17dbc90d..5d1ef49682 100644 --- a/src/review/linked-issue-label-propagation-fetch.ts +++ b/src/review/linked-issue-label-propagation-fetch.ts @@ -129,10 +129,21 @@ export async function fetchLinkedIssueLabelsForPropagation(args: { args.installationId, ); const prAuthorLogin = args.prAuthorLogin?.toLowerCase(); + const mappingsByIssueLabel = new Map(); + for (const mapping of args.mappings ?? []) { + const issueLabel = mapping.issueLabel.toLowerCase(); + mappingsByIssueLabel.set(issueLabel, [...(mappingsByIssueLabel.get(issueLabel) ?? []), mapping]); + } const relaxableLabels = new Set( - (args.mappings ?? []) - .filter((mapping) => mapping.trustMaintainerAuthoredIssue === true || mapping.trustMaintainerAuthoredIssueForReward === true) - .map((mapping) => mapping.issueLabel.toLowerCase()), + [...mappingsByIssueLabel.entries()] + .filter(([, mappings]) => + mappings.every( + (mapping) => + mapping.trustMaintainerAuthoredIssue === true || + mapping.trustMaintainerAuthoredIssueForReward === true, + ), + ) + .map(([issueLabel]) => issueLabel), ); const results = await Promise.all( linkedIssues.map((issueNumber) => diff --git a/test/unit/linked-issue-label-propagation-fetch.test.ts b/test/unit/linked-issue-label-propagation-fetch.test.ts index 2f181d2b4f..42031c1019 100644 --- a/test/unit/linked-issue-label-propagation-fetch.test.ts +++ b/test/unit/linked-issue-label-propagation-fetch.test.ts @@ -477,6 +477,29 @@ describe("fetchLinkedIssueLabelsForPropagation (#priority-linked-issue-gate)", ( expect(result).toEqual(["gittensor:bug"]); }); + it("REGRESSION: mixed-trust duplicate issue labels do not relax strict mappings through a shared label name", async () => { + const mappings = [ + { issueLabel: "shared:gate", prLabel: "safe:type", removeOtherTypeLabels: true, trustMaintainerAuthoredIssue: true }, + { issueLabel: "shared:gate", prLabel: "sensitive:reward", removeOtherTypeLabels: false }, + ]; + stubFetch((url) => { + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/3951")) + return Response.json({ number: 3951, state: "open", user: { login: "owner" }, assignees: [], labels: ["shared:gate"] }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({}); + const result = await fetchLinkedIssueLabelsForPropagation({ + env, + repoFullName: "owner/repo", + linkedIssues: [3951], + installationId: 123, + prAuthorLogin: "contrib", + mappings, + }); + expect(result).toEqual([]); + }); + it("still propagates the reward label via trustMaintainerAuthoredIssueForReward when the issue is authored by an ADMIN_GITHUB_LOGINS fleet-operator", async () => { const mappings = [{ issueLabel: "gittensor:priority", prLabel: "gittensor:priority", removeOtherTypeLabels: false, trustMaintainerAuthoredIssueForReward: true }]; stubFetch((url) => {