From 1abec9f1598074e272d783046b91670f9bd1f3b0 Mon Sep 17 00:00:00 2001 From: hirotomoyamada Date: Tue, 28 Jul 2026 11:26:42 +0900 Subject: [PATCH] feat(config): add author association role filters --- .changeset/fresh-roles-filter.md | 5 ++++ schema.json | 48 ++++++++++++++++++++++++++++++-- src/config/index.type.ts | 16 +++++++++++ src/config/validate.test.ts | 6 +++- src/tools/review/check.ts | 4 +++ src/tools/review/review.test.ts | 16 +++++++++++ test/fixtures/review.ts | 1 + 7 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 .changeset/fresh-roles-filter.md diff --git a/.changeset/fresh-roles-filter.md b/.changeset/fresh-roles-filter.md new file mode 100644 index 0000000..9055432 --- /dev/null +++ b/.changeset/fresh-roles-filter.md @@ -0,0 +1,5 @@ +--- +"opencode-magi": minor +--- + +Add pull request author association filters to review conditions. diff --git a/schema.json b/schema.json index 8ed6ad8..fe9e941 100644 --- a/schema.json +++ b/schema.json @@ -192,6 +192,45 @@ "head": { "$ref": "#/$defs/safetyFilter" } } }, + "roleFilter": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "exclude": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "COLLABORATOR", + "CONTRIBUTOR", + "FIRST_TIME_CONTRIBUTOR", + "FIRST_TIMER", + "MANNEQUIN", + "MEMBER", + "NONE", + "OWNER" + ] + } + }, + "include": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "COLLABORATOR", + "CONTRIBUTOR", + "FIRST_TIME_CONTRIBUTOR", + "FIRST_TIMER", + "MANNEQUIN", + "MEMBER", + "NONE", + "OWNER" + ] + } + } + } + }, "safety": { "type": "object", "additionalProperties": false, @@ -200,7 +239,8 @@ "branches": { "$ref": "#/$defs/safetyBranchFilter" }, "labels": { "$ref": "#/$defs/safetyFilter" }, "maxChangedFiles": { "type": "integer", "minimum": 0 }, - "paths": { "$ref": "#/$defs/safetyFilter" } + "paths": { "$ref": "#/$defs/safetyFilter" }, + "roles": { "$ref": "#/$defs/roleFilter" } } }, "conditions": { @@ -224,7 +264,8 @@ "authors": { "$ref": "#/$defs/safetyFilter" }, "branches": { "$ref": "#/$defs/safetyBranchFilter" }, "labels": { "$ref": "#/$defs/safetyFilter" }, - "paths": { "$ref": "#/$defs/safetyFilter" } + "paths": { "$ref": "#/$defs/safetyFilter" }, + "roles": { "$ref": "#/$defs/roleFilter" } } }, "automationConditions": { @@ -269,7 +310,8 @@ "branches": { "$ref": "#/$defs/safetyBranchFilter" }, "edited": { "type": "boolean" }, "labels": { "$ref": "#/$defs/safetyFilter" }, - "paths": { "$ref": "#/$defs/safetyFilter" } + "paths": { "$ref": "#/$defs/safetyFilter" }, + "roles": { "$ref": "#/$defs/roleFilter" } } }, "automation": { diff --git a/src/config/index.type.ts b/src/config/index.type.ts index 34adfa1..0b51b51 100644 --- a/src/config/index.type.ts +++ b/src/config/index.type.ts @@ -79,6 +79,21 @@ export type ReviewConditionFilter = | { exclude: string[] } | { include: string[] } +export type ReviewConditionRole = + | "COLLABORATOR" + | "CONTRIBUTOR" + | "FIRST_TIME_CONTRIBUTOR" + | "FIRST_TIMER" + | "MANNEQUIN" + | "MEMBER" + | "NONE" + | "OWNER" + +export type ReviewConditionRoleFilter = + | { exclude: ReviewConditionRole[]; include: ReviewConditionRole[] } + | { exclude: ReviewConditionRole[] } + | { include: ReviewConditionRole[] } + export type ReviewConditionBranchFilter = | { base: ReviewConditionFilter; head: ReviewConditionFilter } | { base: ReviewConditionFilter } @@ -90,6 +105,7 @@ export interface ReviewCondition { labels?: ReviewConditionFilter maxChangedFiles?: number paths?: ReviewConditionFilter + roles?: ReviewConditionRoleFilter } export type ReviewConditions = [boolean, ReviewCondition][] | boolean diff --git a/src/config/validate.test.ts b/src/config/validate.test.ts index da246a1..6de5e5d 100644 --- a/src/config/validate.test.ts +++ b/src/config/validate.test.ts @@ -63,10 +63,13 @@ describe("validate", () => { branches: { base: { include: ["main"] } }, labels: { exclude: ["do-not-merge"] }, paths: { exclude: [".github/**"] }, + roles: { include: ["MEMBER"] }, }, ], ] - config.merge.automation.close = [[false, { edited: true }]] + config.merge.automation.close = [ + [false, { edited: true, roles: { include: ["COLLABORATOR"] } }], + ] config.review.safety = [ [ true, @@ -76,6 +79,7 @@ describe("validate", () => { labels: { include: ["ready"] }, maxChangedFiles: 10, paths: { include: ["src/**"] }, + roles: { exclude: ["NONE"] }, }, ], ] diff --git a/src/tools/review/check.ts b/src/tools/review/check.ts index ef42ff3..d3bf5e5 100644 --- a/src/tools/review/check.ts +++ b/src/tools/review/check.ts @@ -100,6 +100,10 @@ export function getConditionErrors( if (!matchesValues(condition.authors, [metadata.user.login])) errors.push(`Author does not match safety filter: ${metadata.user.login}.`) + if (!matchesValues(condition.roles, [metadata.author_association])) + errors.push( + `Role does not match safety filter: ${metadata.author_association}.`, + ) if (!matchesPatterns(branches.base, [metadata.base.ref])) errors.push( `Base branch does not match safety filter: ${metadata.base.ref}.`, diff --git a/src/tools/review/review.test.ts b/src/tools/review/review.test.ts index 6377a4e..0077a1d 100644 --- a/src/tools/review/review.test.ts +++ b/src/tools/review/review.test.ts @@ -524,6 +524,20 @@ describe("Review", () => { ) }) + test("blocks pull requests with unmatched author roles", async ({ + magiFixture: { magi }, + }) => { + const { config, octokitMocks, review } = createReviewFixture(magi) + const metadata = createMetadata() + + config.review.safety = [[true, { roles: { include: ["MEMBER"] } }]] + octokitMocks.get.mockResolvedValue({ data: metadata }) + + await expect(review.checkPr()).rejects.toThrow( + "PR is safety blocked. Role does not match safety filter: NONE.", + ) + }) + test("accepts pull requests that match every safety filter", async ({ magiFixture: { magi }, }) => { @@ -534,6 +548,7 @@ describe("Review", () => { metadata.head.ref = "feature/automation" metadata.labels = [{ name: "ready" }] as typeof metadata.labels metadata.user.login = "trusted" + metadata.author_association = "MEMBER" config.review.safety = [ [ true, @@ -545,6 +560,7 @@ describe("Review", () => { }, labels: { exclude: ["do-not-review"], include: ["ready"] }, paths: { exclude: ["src/generated/**"], include: ["src/**"] }, + roles: { exclude: ["NONE"], include: ["MEMBER"] }, }, ], ] diff --git a/test/fixtures/review.ts b/test/fixtures/review.ts index d721110..8bb441d 100644 --- a/test/fixtures/review.ts +++ b/test/fixtures/review.ts @@ -52,6 +52,7 @@ export interface ReviewFixture { export function createMetadata(): PullRequestMetadata { return { + author_association: "NONE", base: { ref: "main", repo: { clone_url: "https://github.com/magi-ai/opencode-magi.git" },