Skip to content

Commit 34821f2

Browse files
authored
chore: add needs-attention field assignment for prioritization board (#33311)
### Issue # (if applicable) N/A ### Reason for this change Update Needs Attention field in the prioritization project board ### Description of changes Monitors project items daily to identify PRs that have been in their current status for extended periods. ### Describe any new or updated permissions being added N/A ### Description of how you validated changes Unit test is added. Integ test is not applicable. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 8c40341 commit 34821f2

File tree

8 files changed

+343
-10
lines changed

8 files changed

+343
-10
lines changed

.github/workflows/README.md

+10
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ Owner: CDK support team
7373
[issue-label-assign.yml](issue-label-assign.yml): Github action for automatically adding labels and/or setting assignees when an Issue or PR is opened or edited based on user-defined Area
7474
Owner: CDK support team
7575

76+
### P1 Bug Priority Assignment
77+
78+
[project-prioritization-bug.yml](project-prioritization-bug.yml): Github action for automatically adding P1 bugs to the prioritization project board
79+
Owner: CDK support team
80+
7681
## Scheduled Actions
7782

7883
### Issue Lifecycle Handling
@@ -118,3 +123,8 @@ Owner: CDK Support team
118123

119124
[project-prioritization-r5-assignment.yml](project-prioritization-r5-assignment.yml): GitHub action that runs every day to add PR's to the priority project board that satisfies R5 Priority.
120125
Owner: CDK Support team
126+
127+
### Needs Attention Status Update
128+
129+
[project-prioritization-needs-attention.yml](project-prioritization-needs-attention.yml): GitHub action that runs every day to update Needs Attention field in the prioritization project board.
130+
Owner: CDK Support team
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: PR Prioritization Needs Attention Status
2+
on:
3+
schedule:
4+
- cron: '0 7 * * 1-5' # Runs at 7AM every day during weekdays
5+
workflow_dispatch: # Manual trigger
6+
7+
jobs:
8+
update_project_status:
9+
if: github.repository == 'aws/aws-cdk'
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- name: Update Needs Attention Status
15+
uses: actions/github-script@v7
16+
with:
17+
github-token: ${{ secrets.PROJEN_GITHUB_TOKEN }}
18+
script: |
19+
const script = require('./scripts/prioritization/update-attention-status.js')
20+
await script({github})

scripts/@aws-cdk/script-tests/prioritization/assign-r5-priority.test.js

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const { PRIORITIES, LABELS, STATUS, ...PROJECT_CONFIG } = require('../../../../scripts/prioritization/project-config');
22
const {
3-
createMockPR,
43
createMockGithubForR5,
54
OPTION_IDS
65
} = require('./helpers/mock-data');

scripts/@aws-cdk/script-tests/prioritization/helpers/mock-data.js

+42
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,46 @@ exports.createMockGithubForR2 = ({
256256
return { graphql };
257257
};
258258

259+
/**
260+
* Creates mock GitHub GraphQL client with predefined responses for Needs Attention Status field assignment
261+
*/
262+
exports.createMockGithubForNeedsAttention = ({
263+
status = STATUS.READY,
264+
daysInStatus = 0,
265+
items = null
266+
}) => {
267+
const graphql = jest.fn();
268+
269+
const createItem = (itemStatus, days) => ({
270+
id: `item-${Math.random()}`,
271+
fieldValues: {
272+
nodes: [
273+
{
274+
field: { name: 'Status' },
275+
name: itemStatus,
276+
updatedAt: new Date(Date.now() - (days * 24 * 60 * 60 * 1000)).toISOString()
277+
}
278+
]
279+
}
280+
});
281+
282+
// First call - fetch project items
283+
graphql.mockResolvedValueOnce({
284+
organization: {
285+
projectV2: {
286+
items: {
287+
nodes: items ? items.map(item => createItem(item.status, item.daysInStatus))
288+
: [createItem(status, daysInStatus)],
289+
pageInfo: {
290+
hasNextPage: false,
291+
endCursor: null
292+
}
293+
}
294+
}
295+
}
296+
});
297+
298+
return { graphql };
299+
};
300+
259301
exports.OPTION_IDS = OPTION_IDS;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const { STATUS, NEEDS_ATTENTION_STATUS, ...PROJECT_CONFIG } = require('../../../../scripts/prioritization/project-config');
2+
const {
3+
createMockGithubForNeedsAttention,
4+
OPTION_IDS
5+
} = require('./helpers/mock-data');
6+
7+
const updateAttentionStatus = require('../../../../scripts/prioritization/update-attention-status');
8+
9+
describe('Needs Attention Status Assignment', () => {
10+
let mockGithub;
11+
12+
beforeEach(() => {
13+
jest.clearAllMocks();
14+
});
15+
16+
async function verifyProjectState(expectedAttentionStatus) {
17+
const calls = mockGithub.graphql.mock.calls;
18+
19+
if (!expectedAttentionStatus) {
20+
const attentionUpdateCall = calls.find(call =>
21+
call[1].input?.fieldId === PROJECT_CONFIG.attentionFieldId
22+
);
23+
expect(attentionUpdateCall).toBeUndefined();
24+
return;
25+
}
26+
27+
// Verify attention status update
28+
const attentionUpdateCall = calls.find(call =>
29+
call[1].input?.fieldId === PROJECT_CONFIG.attentionFieldId
30+
);
31+
expect(attentionUpdateCall[1].input.value.singleSelectOptionId)
32+
.toBe(expectedAttentionStatus);
33+
}
34+
35+
describe('Needs Attention Status Tests', () => {
36+
test('should set Extended status for items in status 7-14 days', async () => {
37+
mockGithub = createMockGithubForNeedsAttention({
38+
status: STATUS.READY,
39+
daysInStatus: 10
40+
});
41+
42+
await updateAttentionStatus({ github: mockGithub });
43+
await verifyProjectState(NEEDS_ATTENTION_STATUS.EXTENDED.name);
44+
});
45+
46+
test('should set Aging status for items in status 14-21 days', async () => {
47+
mockGithub = createMockGithubForNeedsAttention({
48+
status: STATUS.IN_PROGRESS,
49+
daysInStatus: 16
50+
});
51+
52+
await updateAttentionStatus({ github: mockGithub });
53+
await verifyProjectState(NEEDS_ATTENTION_STATUS.AGING.name);
54+
});
55+
56+
test('should set Stalled status for items in status >21 days', async () => {
57+
mockGithub = createMockGithubForNeedsAttention({
58+
status: STATUS.PAUSED,
59+
daysInStatus: 25
60+
});
61+
62+
await updateAttentionStatus({ github: mockGithub });
63+
await verifyProjectState(NEEDS_ATTENTION_STATUS.STALLED.name);
64+
});
65+
66+
test('should not set attention status for items under threshold', async () => {
67+
mockGithub = createMockGithubForNeedsAttention({
68+
status: STATUS.ASSIGNED,
69+
daysInStatus: 5
70+
});
71+
72+
await updateAttentionStatus({ github: mockGithub });
73+
await verifyProjectState(null);
74+
});
75+
76+
test('should not set attention status for non-monitored status', async () => {
77+
mockGithub = createMockGithubForNeedsAttention({
78+
status: STATUS.DONE,
79+
daysInStatus: 25
80+
});
81+
82+
await updateAttentionStatus({ github: mockGithub });
83+
await verifyProjectState(null);
84+
});
85+
86+
test('should handle multiple items with different statuses', async () => {
87+
mockGithub = createMockGithubForNeedsAttention({
88+
items: [
89+
{ status: STATUS.READY, daysInStatus: 10 },
90+
{ status: STATUS.IN_PROGRESS, daysInStatus: 16 },
91+
{ status: STATUS.PAUSED, daysInStatus: 25 },
92+
{ status: STATUS.DONE, daysInStatus: 30 }
93+
]
94+
});
95+
96+
await updateAttentionStatus({ github: mockGithub });
97+
98+
const calls = mockGithub.graphql.mock.calls;
99+
const attentionCalls = calls.filter(call =>
100+
call[1].input?.fieldId === PROJECT_CONFIG.attentionFieldId
101+
);
102+
103+
expect(attentionCalls).toHaveLength(3); // Only 3 items should be updated
104+
expect(attentionCalls[0][1].input.value.singleSelectOptionId)
105+
.toBe(NEEDS_ATTENTION_STATUS.EXTENDED.name);
106+
expect(attentionCalls[1][1].input.value.singleSelectOptionId)
107+
.toBe(NEEDS_ATTENTION_STATUS.AGING.name);
108+
expect(attentionCalls[2][1].input.value.singleSelectOptionId)
109+
.toBe(NEEDS_ATTENTION_STATUS.STALLED.name);
110+
});
111+
});
112+
});

scripts/prioritization/project-api.js

+46-1
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,55 @@ const updateProjectField = async ({
193193
);
194194
};
195195

196+
/**
197+
* Fetches all items from a GitHub Project with their status and update times
198+
* @param {Object} params - The parameters for fetching project items
199+
* @param {Object} params.github - The GitHub API client
200+
* @param {string} params.org - The organization name
201+
* @param {number} params.number - The project number
202+
* @param {string} [params.cursor] - The pagination cursor
203+
* @returns {Promise<Object>} Project items with their field values
204+
*/
205+
const fetchProjectItems = async ({ github, org, number, cursor }) => {
206+
return github.graphql(
207+
`
208+
query($org: String!, $number: Int!, $cursor: String) {
209+
organization(login: $org) {
210+
projectV2(number: $number) {
211+
items(first: 100, after: $cursor) {
212+
nodes {
213+
id
214+
fieldValues(first: 20) {
215+
nodes {
216+
... on ProjectV2ItemFieldSingleSelectValue {
217+
name
218+
field {
219+
... on ProjectV2SingleSelectField {
220+
name
221+
}
222+
}
223+
updatedAt
224+
}
225+
}
226+
}
227+
}
228+
pageInfo {
229+
hasNextPage
230+
endCursor
231+
}
232+
}
233+
}
234+
}
235+
}`,
236+
{ org, number, cursor }
237+
);
238+
};
239+
196240
module.exports = {
197241
updateProjectField,
198242
addItemToProject,
199243
fetchProjectFields,
200244
fetchOpenPullRequests,
201-
fetchProjectItem
245+
fetchProjectItem,
246+
fetchProjectItems
202247
};

scripts/prioritization/project-config.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,18 @@ const STATUS = {
2828
// Time threshold for R5
2929
const DAYS_THRESHOLD = 21;
3030

31-
const ATTENTION_STATUS = {
31+
const NEEDS_ATTENTION_STATUS = {
3232
STALLED: {
3333
name: '🚨 Stalled',
34-
threshold: 21,
35-
description: 'Critical attention required'
34+
threshold: 21
3635
},
3736
AGING: {
3837
name: '⚠️ Aging',
39-
threshold: 14,
40-
description: 'Requires immediate attention'
38+
threshold: 14
4139
},
4240
EXTENDED: {
4341
name: '🕒 Extended',
44-
threshold: 7,
45-
description: 'Taking longer than expected'
42+
threshold: 7
4643
}
4744
};
4845

@@ -63,6 +60,6 @@ module.exports = {
6360
LABELS,
6461
PRIORITIES,
6562
STATUS,
66-
ATTENTION_STATUS,
63+
NEEDS_ATTENTION_STATUS,
6764
DAYS_THRESHOLD
6865
};

0 commit comments

Comments
 (0)