Skip to content

Commit 66ae1be

Browse files
authored
fix: address squad issue assignment review feedback
1 parent ea03fb1 commit 66ae1be

2 files changed

Lines changed: 52 additions & 26 deletions

File tree

.github/workflows/squad-issue-assign.yml

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ name: Squad Issue Assign
1919
# - Expose secrets, tokens, or internal routing logic in comments.
2020
# - Override or skip the post-check verification job.
2121
# - Escalate its own permissions (e.g., request write access to contents).
22-
# - Process label events from forks or untrusted senders without
23-
# verifying repository_owner matches the expected org.
22+
# - Process go:* label events unless the repository owner is Azure and
23+
# the sender has repository maintain/admin permission.
2424
# ────────────────────────────────────────────────────────────────────────
2525

2626
on:
@@ -33,17 +33,34 @@ permissions:
3333

3434
jobs:
3535
assign-issue:
36-
if: github.event.label.name == 'go:yes'
36+
if: github.event.label.name == 'go:yes' && github.repository_owner == 'Azure'
3737
runs-on: ubuntu-latest
38-
outputs:
39-
assignee: ${{ steps.assign.outputs.assignee }}
40-
squad-labels: ${{ steps.route.outputs.squad-labels }}
4138
steps:
39+
- name: Verify maintainer-triggered event
40+
uses: actions/github-script@v8
41+
with:
42+
script: |
43+
const sender = context.payload.sender.login;
44+
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
45+
owner: context.repo.owner,
46+
repo: context.repo.repo,
47+
username: sender
48+
});
49+
const allowedPermissions = new Set(['admin', 'maintain']);
50+
51+
if (!allowedPermissions.has(permission.permission)) {
52+
core.setFailed(
53+
`Only repository maintainers may apply go:* labels. ${sender} has ${permission.permission} permission.`
54+
);
55+
return;
56+
}
57+
58+
core.info(`Verified ${sender} as a ${permission.permission} collaborator`);
59+
4260
- uses: actions/checkout@v4
4361

4462
# Step 1: Assign issue to the maintainer who applied "go:yes"
4563
- name: Assign to label sender
46-
id: assign
4764
uses: actions/github-script@v8
4865
with:
4966
script: |
@@ -57,12 +74,10 @@ jobs:
5774
assignees: [sender]
5875
});
5976
60-
core.setOutput('assignee', sender);
6177
core.info(`Assigned issue #${issue_number} to ${sender} (go:yes sender)`);
6278
6379
# Step 2: Read triage analysis and route to squad areas
6480
- name: Route to squad areas
65-
id: route
6681
uses: actions/github-script@v8
6782
with:
6883
script: |
@@ -98,13 +113,28 @@ jobs:
98113
99114
// Fetch prior triage analysis comment
100115
let triageAnalysis = '';
101-
const { data: comments } = await github.rest.issues.listComments({
102-
owner: context.repo.owner,
103-
repo: context.repo.repo,
104-
issue_number: issue.number,
105-
per_page: 100
106-
});
107-
const triageComment = comments.find(c =>
116+
const perPage = 50;
117+
const commentCount = issue.comments || 0;
118+
const lastPage = Math.max(1, Math.ceil(commentCount / perPage));
119+
const pagesToFetch = new Set([lastPage]);
120+
if (commentCount > perPage && commentCount % perPage !== 0) {
121+
pagesToFetch.add(lastPage - 1);
122+
}
123+
124+
const commentPages = await Promise.all(
125+
[...pagesToFetch]
126+
.filter(page => page > 0)
127+
.sort((left, right) => left - right)
128+
.map(page => github.rest.issues.listComments({
129+
owner: context.repo.owner,
130+
repo: context.repo.repo,
131+
issue_number: issue.number,
132+
per_page: perPage,
133+
page
134+
}))
135+
);
136+
const comments = commentPages.flatMap(response => response.data).slice(-perPage);
137+
const triageComment = [...comments].reverse().find(c =>
108138
c.body && c.body.includes('Squad Triage')
109139
);
110140
if (triageComment) {
@@ -139,7 +169,7 @@ jobs:
139169
}
140170
}
141171
142-
// If no specific routing match, check if copilot is appropriate
172+
// If no specific routing match, fall back to ApiOpsLead
143173
if (matchedAreas.length === 0) {
144174
matchedAreas.push({
145175
workType: 'general',
@@ -169,8 +199,6 @@ jobs:
169199
labels: safeLabels
170200
});
171201
172-
core.setOutput('squad-labels', JSON.stringify(squadMemberLabels));
173-
174202
// Post assignment rationale comment (max: 1)
175203
const areaList = matchedAreas.map(a =>
176204
`- **${a.workType}** → ${a.routeTo} (\`${a.label}\`)`
@@ -213,8 +241,6 @@ jobs:
213241
script: |
214242
const fs = require('fs');
215243
const issue_number = context.payload.issue.number;
216-
const expectedAssignee = '${{ needs.assign-issue.outputs.assignee }}';
217-
const expectedLabels = JSON.parse('${{ needs.assign-issue.outputs.squad-labels }}');
218244
const sender = context.payload.sender.login;
219245
220246
// Verify: assignee equals the go:yes label sender
@@ -256,7 +282,7 @@ jobs:
256282
const invalidLabels = squadLabels.filter(l => !validMembers.has(l));
257283
if (invalidLabels.length > 0) {
258284
core.setFailed(
259-
`Invalid squad labels not in .squad/routing.md: [${invalidLabels.join(', ')}]`
285+
`Invalid squad labels not in .squad/routing-table.json or .squad/issue-routing.json: [${invalidLabels.join(', ')}]`
260286
);
261287
return;
262288
}

.squad/routing.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ Each entry has `label`, `action`, and `who` (squad member name).
1616

1717
### How Issue Assignment Works
1818

19-
1. When a GitHub issue gets the `squad` label, the **Lead** triages it — analyzing content, assigning the right `squad:{member}` label, and commenting with triage notes.
20-
2. When a `squad:{member}` label is applied, that member picks up the issue in their next session.
21-
3. Members can reassign by removing their label and adding another member's label.
22-
4. The `squad` label is the "inbox" — untriaged issues waiting for Lead review.
19+
1. A maintainer reviews the issue and applies a `go:*` decision label.
20+
2. When a maintainer applies `go:yes`, the issue assignment workflow assigns the issue to that maintainer.
21+
3. The assignment workflow reads the issue text, optionally uses the most recent `Squad Triage` comment from the last 50 issue comments, and applies `squad` plus matching `squad:{member}` labels from [`.squad/routing-table.json`](routing-table.json) and [`.squad/issue-routing.json`](issue-routing.json).
22+
4. The `squad` label marks the issue for squad routing, and each `squad:{member}` label identifies a matched follow-up area.
2323

2424
## Rules
2525

0 commit comments

Comments
 (0)