Skip to content

Commit

Permalink
chore: add needs-attention field assignment for prioritization board (#…
Browse files Browse the repository at this point in the history
…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*
  • Loading branch information
godwingrs22 authored Feb 6, 2025
1 parent 8c40341 commit 34821f2
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 10 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ Owner: CDK support team
[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
Owner: CDK support team

### P1 Bug Priority Assignment

[project-prioritization-bug.yml](project-prioritization-bug.yml): Github action for automatically adding P1 bugs to the prioritization project board
Owner: CDK support team

## Scheduled Actions

### Issue Lifecycle Handling
Expand Down Expand Up @@ -118,3 +123,8 @@ Owner: CDK Support team

[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.
Owner: CDK Support team

### Needs Attention Status Update

[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.
Owner: CDK Support team
20 changes: 20 additions & 0 deletions .github/workflows/project-prioritization-needs-attention.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: PR Prioritization Needs Attention Status
on:
schedule:
- cron: '0 7 * * 1-5' # Runs at 7AM every day during weekdays
workflow_dispatch: # Manual trigger

jobs:
update_project_status:
if: github.repository == 'aws/aws-cdk'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Update Needs Attention Status
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJEN_GITHUB_TOKEN }}
script: |
const script = require('./scripts/prioritization/update-attention-status.js')
await script({github})
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const { PRIORITIES, LABELS, STATUS, ...PROJECT_CONFIG } = require('../../../../scripts/prioritization/project-config');
const {
createMockPR,
createMockGithubForR5,
OPTION_IDS
} = require('./helpers/mock-data');
Expand Down
42 changes: 42 additions & 0 deletions scripts/@aws-cdk/script-tests/prioritization/helpers/mock-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,46 @@ exports.createMockGithubForR2 = ({
return { graphql };
};

/**
* Creates mock GitHub GraphQL client with predefined responses for Needs Attention Status field assignment
*/
exports.createMockGithubForNeedsAttention = ({
status = STATUS.READY,
daysInStatus = 0,
items = null
}) => {
const graphql = jest.fn();

const createItem = (itemStatus, days) => ({
id: `item-${Math.random()}`,
fieldValues: {
nodes: [
{
field: { name: 'Status' },
name: itemStatus,
updatedAt: new Date(Date.now() - (days * 24 * 60 * 60 * 1000)).toISOString()
}
]
}
});

// First call - fetch project items
graphql.mockResolvedValueOnce({
organization: {
projectV2: {
items: {
nodes: items ? items.map(item => createItem(item.status, item.daysInStatus))
: [createItem(status, daysInStatus)],
pageInfo: {
hasNextPage: false,
endCursor: null
}
}
}
}
});

return { graphql };
};

exports.OPTION_IDS = OPTION_IDS;
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const { STATUS, NEEDS_ATTENTION_STATUS, ...PROJECT_CONFIG } = require('../../../../scripts/prioritization/project-config');
const {
createMockGithubForNeedsAttention,
OPTION_IDS
} = require('./helpers/mock-data');

const updateAttentionStatus = require('../../../../scripts/prioritization/update-attention-status');

describe('Needs Attention Status Assignment', () => {
let mockGithub;

beforeEach(() => {
jest.clearAllMocks();
});

async function verifyProjectState(expectedAttentionStatus) {
const calls = mockGithub.graphql.mock.calls;

if (!expectedAttentionStatus) {
const attentionUpdateCall = calls.find(call =>
call[1].input?.fieldId === PROJECT_CONFIG.attentionFieldId
);
expect(attentionUpdateCall).toBeUndefined();
return;
}

// Verify attention status update
const attentionUpdateCall = calls.find(call =>
call[1].input?.fieldId === PROJECT_CONFIG.attentionFieldId
);
expect(attentionUpdateCall[1].input.value.singleSelectOptionId)
.toBe(expectedAttentionStatus);
}

describe('Needs Attention Status Tests', () => {
test('should set Extended status for items in status 7-14 days', async () => {
mockGithub = createMockGithubForNeedsAttention({
status: STATUS.READY,
daysInStatus: 10
});

await updateAttentionStatus({ github: mockGithub });
await verifyProjectState(NEEDS_ATTENTION_STATUS.EXTENDED.name);
});

test('should set Aging status for items in status 14-21 days', async () => {
mockGithub = createMockGithubForNeedsAttention({
status: STATUS.IN_PROGRESS,
daysInStatus: 16
});

await updateAttentionStatus({ github: mockGithub });
await verifyProjectState(NEEDS_ATTENTION_STATUS.AGING.name);
});

test('should set Stalled status for items in status >21 days', async () => {
mockGithub = createMockGithubForNeedsAttention({
status: STATUS.PAUSED,
daysInStatus: 25
});

await updateAttentionStatus({ github: mockGithub });
await verifyProjectState(NEEDS_ATTENTION_STATUS.STALLED.name);
});

test('should not set attention status for items under threshold', async () => {
mockGithub = createMockGithubForNeedsAttention({
status: STATUS.ASSIGNED,
daysInStatus: 5
});

await updateAttentionStatus({ github: mockGithub });
await verifyProjectState(null);
});

test('should not set attention status for non-monitored status', async () => {
mockGithub = createMockGithubForNeedsAttention({
status: STATUS.DONE,
daysInStatus: 25
});

await updateAttentionStatus({ github: mockGithub });
await verifyProjectState(null);
});

test('should handle multiple items with different statuses', async () => {
mockGithub = createMockGithubForNeedsAttention({
items: [
{ status: STATUS.READY, daysInStatus: 10 },
{ status: STATUS.IN_PROGRESS, daysInStatus: 16 },
{ status: STATUS.PAUSED, daysInStatus: 25 },
{ status: STATUS.DONE, daysInStatus: 30 }
]
});

await updateAttentionStatus({ github: mockGithub });

const calls = mockGithub.graphql.mock.calls;
const attentionCalls = calls.filter(call =>
call[1].input?.fieldId === PROJECT_CONFIG.attentionFieldId
);

expect(attentionCalls).toHaveLength(3); // Only 3 items should be updated
expect(attentionCalls[0][1].input.value.singleSelectOptionId)
.toBe(NEEDS_ATTENTION_STATUS.EXTENDED.name);
expect(attentionCalls[1][1].input.value.singleSelectOptionId)
.toBe(NEEDS_ATTENTION_STATUS.AGING.name);
expect(attentionCalls[2][1].input.value.singleSelectOptionId)
.toBe(NEEDS_ATTENTION_STATUS.STALLED.name);
});
});
});
47 changes: 46 additions & 1 deletion scripts/prioritization/project-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,55 @@ const updateProjectField = async ({
);
};

/**
* Fetches all items from a GitHub Project with their status and update times
* @param {Object} params - The parameters for fetching project items
* @param {Object} params.github - The GitHub API client
* @param {string} params.org - The organization name
* @param {number} params.number - The project number
* @param {string} [params.cursor] - The pagination cursor
* @returns {Promise<Object>} Project items with their field values
*/
const fetchProjectItems = async ({ github, org, number, cursor }) => {
return github.graphql(
`
query($org: String!, $number: Int!, $cursor: String) {
organization(login: $org) {
projectV2(number: $number) {
items(first: 100, after: $cursor) {
nodes {
id
fieldValues(first: 20) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
field {
... on ProjectV2SingleSelectField {
name
}
}
updatedAt
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}`,
{ org, number, cursor }
);
};

module.exports = {
updateProjectField,
addItemToProject,
fetchProjectFields,
fetchOpenPullRequests,
fetchProjectItem
fetchProjectItem,
fetchProjectItems
};
13 changes: 5 additions & 8 deletions scripts/prioritization/project-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,18 @@ const STATUS = {
// Time threshold for R5
const DAYS_THRESHOLD = 21;

const ATTENTION_STATUS = {
const NEEDS_ATTENTION_STATUS = {
STALLED: {
name: '🚨 Stalled',
threshold: 21,
description: 'Critical attention required'
threshold: 21
},
AGING: {
name: '⚠️ Aging',
threshold: 14,
description: 'Requires immediate attention'
threshold: 14
},
EXTENDED: {
name: '🕒 Extended',
threshold: 7,
description: 'Taking longer than expected'
threshold: 7
}
};

Expand All @@ -63,6 +60,6 @@ module.exports = {
LABELS,
PRIORITIES,
STATUS,
ATTENTION_STATUS,
NEEDS_ATTENTION_STATUS,
DAYS_THRESHOLD
};
Loading

0 comments on commit 34821f2

Please sign in to comment.