Skip to content

Commit 28cf796

Browse files
Copilotpelikhan
andauthored
Remove pull_request_review from on.pull_request_reviewer hybrid routing (#33461)
* Plan pull_request_reviewer trigger update Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Remove pull_request_review from pull_request_reviewer routing Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Fix reviewer event merging and update reviewer lock triggers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent be4a934 commit 28cf796

11 files changed

Lines changed: 112 additions & 79 deletions

.github/workflows/agentic_commands.yml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
# necromancer -> necromancer [pull_request] reaction=eyes
3030
# needs-design -> approach-validator [issues,pull_request] reaction=eyes
3131
# pull-request reviewers:
32-
# design-decision-gate [pull_request,pull_request_review]
33-
# mattpocock-skills-reviewer [pull_request,pull_request_review]
34-
# pr-code-quality-reviewer [pull_request,pull_request_review]
35-
# test-quality-sentinel [pull_request,pull_request_review]
32+
# design-decision-gate [pull_request]
33+
# mattpocock-skills-reviewer [pull_request]
34+
# pr-code-quality-reviewer [pull_request]
35+
# test-quality-sentinel [pull_request]
3636
# ___ _ _
3737
# / _ \ | | (_)
3838
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -64,8 +64,6 @@ on:
6464
types: [created, edited]
6565
pull_request:
6666
types: [edited, labeled, opened, ready_for_review, reopened, review_requested]
67-
pull_request_review:
68-
types: [submitted]
6967
pull_request_review_comment:
7068
types: [created, edited]
7169
discussion:
@@ -99,7 +97,7 @@ jobs:
9997
env:
10098
GH_AW_SLASH_ROUTING: '{"ace":[{"workflow":"ace-editor","events":["pull_request_comment"],"ai_reaction":"eyes"}],"approach-validator":[{"workflow":"approach-validator","events":["issue_comment","pull_request_comment"],"ai_reaction":"eyes"}],"archie":[{"workflow":"archie","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes"}],"brave":[{"workflow":"brave","events":["issue_comment"],"ai_reaction":"eyes"}],"cloclo":[{"workflow":"cloclo","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes"}],"craft":[{"workflow":"craft","events":["issues"],"ai_reaction":"eyes"}],"design-decision-gate":[{"workflow":"design-decision-gate","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes"}],"grumpy":[{"workflow":"grumpy-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes"}],"matt":[{"workflow":"mattpocock-skills-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes"}],"mergefest":[{"workflow":"mergefest","events":["pull_request_comment"],"ai_reaction":"eyes"}],"nit":[{"workflow":"pr-nitpick-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes"}],"plan":[{"workflow":"plan","events":["discussion_comment","issue_comment"],"ai_reaction":"eyes"}],"poem-bot":[{"workflow":"poem-bot","events":["issues"],"ai_reaction":"eyes"}],"review":[{"workflow":"pr-code-quality-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes"}],"scout":[{"workflow":"scout","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes"}],"security-review":[{"workflow":"security-review","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes"}],"summarize":[{"workflow":"pdf-summary","events":["issue_comment","issues"],"ai_reaction":"eyes"}],"test-quality-sentinel":[{"workflow":"test-quality-sentinel","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes"}],"tidy":[{"workflow":"tidy","events":["pull_request_comment"],"ai_reaction":"eyes"}],"unbloat":[{"workflow":"unbloat-docs","events":["pull_request_comment"],"ai_reaction":"eyes"}]}'
10199
GH_AW_LABEL_ROUTING: '{"approach-proposal":[{"workflow":"approach-validator","events":["issues","pull_request"],"ai_reaction":"eyes"}],"ci-doctor":[{"workflow":"ci-doctor","events":["pull_request"],"ai_reaction":"eyes"}],"cloclo":[{"workflow":"cloclo","events":["discussion","issues","pull_request"],"ai_reaction":"eyes"}],"dev":[{"workflow":"dev","events":["discussion","issues","pull_request"],"ai_reaction":"eyes"}],"necromancer":[{"workflow":"necromancer","events":["pull_request"],"ai_reaction":"eyes"}],"needs-design":[{"workflow":"approach-validator","events":["issues","pull_request"],"ai_reaction":"eyes"}]}'
102-
GH_AW_REVIEWER_ROUTING: '[{"workflow":"design-decision-gate","events":["pull_request","pull_request_review"]},{"workflow":"mattpocock-skills-reviewer","events":["pull_request","pull_request_review"]},{"workflow":"pr-code-quality-reviewer","events":["pull_request","pull_request_review"]},{"workflow":"test-quality-sentinel","events":["pull_request","pull_request_review"]}]'
100+
GH_AW_REVIEWER_ROUTING: '[{"workflow":"design-decision-gate","events":["pull_request"]},{"workflow":"mattpocock-skills-reviewer","events":["pull_request"]},{"workflow":"pr-code-quality-reviewer","events":["pull_request"]},{"workflow":"test-quality-sentinel","events":["pull_request"]}]'
103101
with:
104102
script: |
105103
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');

.github/workflows/mattpocock-skills-reviewer.lock.yml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/pr-code-quality-reviewer.lock.yml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/test-quality-sentinel.lock.yml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

actions/setup/js/route_slash_command.cjs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
/// <reference types="@actions/github-script" />
33

44
const { REACTION_MAP } = require("./add_reaction.cjs");
5-
const { extractWorkflowId } = require("./generate_footer.cjs");
65
// Keep this aligned with the current default stable GitHub REST API version used by workflows.
76
// Update when GitHub advances the recommended version to avoid sunset/deprecation warnings.
87
const GITHUB_API_VERSION = "2022-11-28";
@@ -272,27 +271,12 @@ async function main() {
272271
return;
273272
}
274273

275-
if ((context.eventName === "pull_request" || context.eventName === "pull_request_review") && Array.isArray(reviewerRoutes) && reviewerRoutes.length > 0) {
274+
if (context.eventName === "pull_request" && Array.isArray(reviewerRoutes) && reviewerRoutes.length > 0) {
276275
const matches = reviewerRoutes.filter(route => Array.isArray(route.events) && route.events.includes(context.eventName));
277276
if (matches.length > 0) {
278277
let selected = matches;
279-
if (context.eventName === "pull_request") {
280-
if (!["ready_for_review", "review_requested"].includes(context.payload?.action ?? "")) {
281-
selected = [];
282-
}
283-
} else if (context.eventName === "pull_request_review") {
284-
const action = context.payload?.action ?? "";
285-
if (action !== "submitted") {
286-
selected = [];
287-
} else {
288-
const workflowId = extractWorkflowId(context.payload?.review?.body ?? "");
289-
if (workflowId) {
290-
selected = selected.filter(route => route.workflow === workflowId);
291-
} else {
292-
selected = [];
293-
core.info("No workflow marker found in pull request review body; skipping reviewer dispatch.");
294-
}
295-
}
278+
if (!["ready_for_review", "review_requested"].includes(context.payload?.action ?? "")) {
279+
selected = [];
296280
}
297281
if (selected.length > 0) {
298282
core.info(`Matched reviewer routes on '${context.eventName}': ${selected.map(route => route.workflow).join(", ")}.`);

actions/setup/js/route_slash_command.test.cjs

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ describe("route_slash_command", () => {
228228
it("dispatches reviewer lifecycle routes on pull_request ready_for_review", async () => {
229229
globals.context.eventName = "pull_request";
230230
globals.context.payload = { action: "ready_for_review", pull_request: { number: 12, state: "open" } };
231-
process.env.GH_AW_REVIEWER_ROUTING = JSON.stringify([{ workflow: "pr-reviewer", events: ["pull_request", "pull_request_review"] }]);
231+
process.env.GH_AW_REVIEWER_ROUTING = JSON.stringify([{ workflow: "pr-reviewer", events: ["pull_request"] }]);
232232

233233
await main();
234234

@@ -242,7 +242,7 @@ describe("route_slash_command", () => {
242242
it("dispatches reviewer lifecycle routes on pull_request review_requested", async () => {
243243
globals.context.eventName = "pull_request";
244244
globals.context.payload = { action: "review_requested", pull_request: { number: 12, state: "open" } };
245-
process.env.GH_AW_REVIEWER_ROUTING = JSON.stringify([{ workflow: "pr-reviewer", events: ["pull_request", "pull_request_review"] }]);
245+
process.env.GH_AW_REVIEWER_ROUTING = JSON.stringify([{ workflow: "pr-reviewer", events: ["pull_request"] }]);
246246

247247
await main();
248248

@@ -264,35 +264,14 @@ describe("route_slash_command", () => {
264264
expect(globals.core.info).toHaveBeenCalledWith(expect.stringContaining("Pull request is closed at workflow start"));
265265
});
266266

267-
it("routes pull_request_review using workflow marker in review body", async () => {
268-
globals.context.eventName = "pull_request_review";
269-
globals.context.payload = {
270-
action: "submitted",
271-
pull_request: { number: 9, state: "open" },
272-
review: { body: "Looks good\n<!-- gh-aw-workflow-id: pr-reviewer -->" },
273-
};
274-
process.env.GH_AW_REVIEWER_ROUTING = JSON.stringify([
275-
{ workflow: "pr-reviewer", events: ["pull_request_review"] },
276-
{ workflow: "other-reviewer", events: ["pull_request_review"] },
277-
]);
278-
279-
await main();
280-
281-
expect(dispatchCalls).toHaveLength(1);
282-
expect(dispatchCalls[0].workflow_id).toBe("pr-reviewer.lock.yml");
283-
const awContext = JSON.parse(dispatchCalls[0].inputs.aw_context);
284-
expect(awContext.reviewer_lifecycle_event).toBe("pull_request_review");
285-
});
286-
287-
it("ignores pull_request_review edited and dismissed actions", async () => {
288-
process.env.GH_AW_REVIEWER_ROUTING = JSON.stringify([{ workflow: "pr-reviewer", events: ["pull_request_review"] }]);
289-
for (const action of ["edited", "dismissed"]) {
267+
it("ignores pull_request actions outside reviewer lifecycle", async () => {
268+
process.env.GH_AW_REVIEWER_ROUTING = JSON.stringify([{ workflow: "pr-reviewer", events: ["pull_request"] }]);
269+
for (const action of ["opened", "synchronize"]) {
290270
dispatchCalls.length = 0;
291-
globals.context.eventName = "pull_request_review";
271+
globals.context.eventName = "pull_request";
292272
globals.context.payload = {
293273
action,
294274
pull_request: { number: 9, state: "open" },
295-
review: { body: "Looks good\n<!-- gh-aw-workflow-id: pr-reviewer -->" },
296275
};
297276
await main();
298277
expect(dispatchCalls).toHaveLength(0);

docs/src/content/docs/reference/safe-outputs-specification.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,8 +1696,7 @@ on:
16961696
- **PRR3**: Built-in slash command behavior for `on.pull_request_reviewer` MUST always be enabled and MUST NOT be replaced by a separate `on.slash_command` trigger definition. If both fields are present, reviewer-trigger command name and reviewer-trigger event set MUST take precedence.
16971697
- **PRR4**: Implementations MUST subscribe reviewer lifecycle routing to:
16981698
- `pull_request` actions `ready_for_review` and `review_requested`
1699-
- `pull_request_review` action `submitted` only
1700-
- **PRR5**: Implementations MUST ignore `pull_request_review` actions other than `submitted` (including `edited` and `dismissed`) for reviewer lifecycle routing.
1699+
- **PRR5**: Implementations MUST ignore `pull_request` actions outside reviewer lifecycle routing (for example `opened`, `edited`, `synchronize`, and `closed`).
17011700
- **PRR6**: When multiple `pull_request_reviewer` workflows are configured, slash command names MUST be unique case-insensitively.
17021701
- **PRR7**: Duplicate reviewer slash command names MUST fail compilation with an explicit validation error.
17031702

pkg/workflow/central_slash_command_workflow.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,17 +260,13 @@ func collectPullRequestReviewerRoutes(workflowDataList []*WorkflowData, mergedEv
260260
}
261261
routes = append(routes, reviewerLifecycleRoute{
262262
Workflow: wd.WorkflowID,
263-
Events: []string{"pull_request", "pull_request_review"},
263+
Events: []string{"pull_request"},
264264
})
265265
if mergedEvents["pull_request"] == nil {
266266
mergedEvents["pull_request"] = make(map[string]bool)
267267
}
268268
mergedEvents["pull_request"]["ready_for_review"] = true
269269
mergedEvents["pull_request"]["review_requested"] = true
270-
if mergedEvents["pull_request_review"] == nil {
271-
mergedEvents["pull_request_review"] = make(map[string]bool)
272-
}
273-
mergedEvents["pull_request_review"]["submitted"] = true
274270
}
275271
sort.Slice(routes, func(i, j int) bool {
276272
return routes[i].Workflow < routes[j].Workflow

pkg/workflow/central_slash_command_workflow_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,12 @@ func TestGenerateCentralSlashCommandWorkflow_IncludesPullRequestReviewerRoutes(t
169169
require.NoError(t, err)
170170
text := string(content)
171171
require.Contains(t, text, "GH_AW_REVIEWER_ROUTING")
172-
require.Contains(t, text, `"workflow":"pr-reviewer","events":["pull_request","pull_request_review"]`)
173-
require.Contains(t, text, "pull_request_review:")
174-
require.Contains(t, text, "types: [submitted]")
172+
require.Contains(t, text, `"workflow":"pr-reviewer","events":["pull_request"]`)
173+
require.NotContains(t, text, "pull_request_review:")
175174
require.Contains(t, text, "ready_for_review")
176175
require.Contains(t, text, "review_requested")
177176
require.Contains(t, text, "# pull-request reviewers:")
178-
require.Contains(t, text, "# pr-reviewer [pull_request,pull_request_review]")
177+
require.Contains(t, text, "# pr-reviewer [pull_request]")
179178
}
180179

181180
func TestGenerateCentralSlashCommandWorkflow_InfersReviewerCommandFromWorkflowIDWhenMissing(t *testing.T) {

pkg/workflow/compiler_safe_outputs.go

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,75 @@ func resolvePullRequestReviewerCommandName(reviewerTriggerValue any, workflowDat
7070
return strings.TrimSuffix(filepath.Base(markdownPath), ".md")
7171
}
7272

73+
func mergeCommandOtherEvents(existing map[string]any, incoming map[string]any) map[string]any {
74+
if len(existing) == 0 {
75+
return incoming
76+
}
77+
if len(incoming) == 0 {
78+
return existing
79+
}
80+
merged := maps.Clone(existing)
81+
for eventName, incomingValue := range incoming {
82+
if existingValue, hasExisting := merged[eventName]; hasExisting {
83+
merged[eventName] = mergeEventConfig(existingValue, incomingValue)
84+
continue
85+
}
86+
merged[eventName] = incomingValue
87+
}
88+
return merged
89+
}
90+
91+
func mergeEventConfig(existing any, incoming any) any {
92+
existingMap, existingOK := existing.(map[string]any)
93+
incomingMap, incomingOK := incoming.(map[string]any)
94+
if !existingOK || !incomingOK {
95+
return incoming
96+
}
97+
merged := maps.Clone(existingMap)
98+
maps.Copy(merged, incomingMap)
99+
100+
existingTypes, existingTypesOK := parseEventTypes(existingMap["types"])
101+
incomingTypes, incomingTypesOK := parseEventTypes(incomingMap["types"])
102+
if existingTypesOK && incomingTypesOK {
103+
seen := make(map[string]bool, len(existingTypes)+len(incomingTypes))
104+
combined := make([]string, 0, len(existingTypes)+len(incomingTypes))
105+
for _, eventType := range existingTypes {
106+
if !seen[eventType] {
107+
seen[eventType] = true
108+
combined = append(combined, eventType)
109+
}
110+
}
111+
for _, eventType := range incomingTypes {
112+
if !seen[eventType] {
113+
seen[eventType] = true
114+
combined = append(combined, eventType)
115+
}
116+
}
117+
merged["types"] = combined
118+
}
119+
120+
return merged
121+
}
122+
123+
func parseEventTypes(value any) ([]string, bool) {
124+
switch typed := value.(type) {
125+
case []string:
126+
return typed, true
127+
case []any:
128+
out := make([]string, 0, len(typed))
129+
for _, entry := range typed {
130+
entryStr, ok := entry.(string)
131+
if !ok {
132+
return nil, false
133+
}
134+
out = append(out, entryStr)
135+
}
136+
return out, true
137+
default:
138+
return nil, false
139+
}
140+
}
141+
73142
// parseOnSection handles parsing of the "on" section from frontmatter, extracting command triggers,
74143
// reactions, and stop-after configurations while detecting conflicts with other event types.
75144
func (c *Compiler) parseOnSection(frontmatter map[string]any, workflowData *WorkflowData, markdownPath string) error {
@@ -213,9 +282,6 @@ func (c *Compiler) parseOnSection(frontmatter map[string]any, workflowData *Work
213282
"pull_request": map[string]any{
214283
"types": []string{"ready_for_review", "review_requested"},
215284
},
216-
"pull_request_review": map[string]any{
217-
"types": []string{"submitted"},
218-
},
219285
}
220286
if _, hasConcurrency := frontmatter["concurrency"]; !hasConcurrency && workflowData.Concurrency == "" {
221287
workflowData.Concurrency = "concurrency:\n group: \"gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}-all-reviewers\"\n queue: max"
@@ -337,7 +403,7 @@ func (c *Compiler) parseOnSection(frontmatter map[string]any, workflowData *Work
337403
if hasCommand && len(otherEvents) > 0 {
338404
// We'll store this and handle it in applyDefaults
339405
workflowData.On = "" // This will trigger command handling in applyDefaults
340-
workflowData.CommandOtherEvents = otherEvents
406+
workflowData.CommandOtherEvents = mergeCommandOtherEvents(workflowData.CommandOtherEvents, otherEvents)
341407
} else if hasLabelCommand && len(otherEvents) > 0 {
342408
// Store other events for label-command merging in applyDefaults
343409
workflowData.On = "" // This will trigger label-command handling in applyDefaults

0 commit comments

Comments
 (0)