Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fresh-roles-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"opencode-magi": minor
---

Add pull request author association filters to review conditions.
48 changes: 45 additions & 3 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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": {
Expand All @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down
16 changes: 16 additions & 0 deletions src/config/index.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -90,6 +105,7 @@ export interface ReviewCondition {
labels?: ReviewConditionFilter
maxChangedFiles?: number
paths?: ReviewConditionFilter
roles?: ReviewConditionRoleFilter
}

export type ReviewConditions = [boolean, ReviewCondition][] | boolean
Expand Down
6 changes: 5 additions & 1 deletion src/config/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -76,6 +79,7 @@ describe("validate", () => {
labels: { include: ["ready"] },
maxChangedFiles: 10,
paths: { include: ["src/**"] },
roles: { exclude: ["NONE"] },
},
],
]
Expand Down
4 changes: 4 additions & 0 deletions src/tools/review/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.`,
Expand Down
16 changes: 16 additions & 0 deletions src/tools/review/review.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
}) => {
Expand All @@ -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,
Expand All @@ -545,6 +560,7 @@ describe("Review", () => {
},
labels: { exclude: ["do-not-review"], include: ["ready"] },
paths: { exclude: ["src/generated/**"], include: ["src/**"] },
roles: { exclude: ["NONE"], include: ["MEMBER"] },
},
],
]
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface ReviewFixture<T extends Review> {

export function createMetadata(): PullRequestMetadata {
return {
author_association: "NONE",
base: {
ref: "main",
repo: { clone_url: "https://github.com/magi-ai/opencode-magi.git" },
Expand Down
Loading