From 5da037e3f7298854a2bbf868f5c627d3b1efdd3f Mon Sep 17 00:00:00 2001 From: Godwin Rose Samuel Date: Tue, 21 Jan 2025 22:46:53 -0800 Subject: [PATCH] chore: add project prioritization automation actions --- .github/workflows/README.md | 15 + .../project-prioritization-assignment.yml | 22 ++ .../project-prioritization-r2-assignment.yml | 19 ++ .../project-prioritization-r5-assignment.yml | 18 ++ .../prioritization/assign-priority.test.js | 237 ++++++++++++++++ .../prioritization/assign-r2-priority.test.js | 115 ++++++++ .../prioritization/assign-r5-priority.test.js | 116 ++++++++ .../prioritization/helpers/mock-data.js | 257 ++++++++++++++++++ scripts/prioritization/README.md | 115 ++++++++ scripts/prioritization/assign-priority.js | 135 +++++++++ scripts/prioritization/assign-r2-priority.js | 152 +++++++++++ scripts/prioritization/assign-r5-priority.js | 145 ++++++++++ scripts/prioritization/project-api.js | 201 ++++++++++++++ scripts/prioritization/project-config.js | 65 +++++ 14 files changed, 1612 insertions(+) create mode 100644 .github/workflows/project-prioritization-assignment.yml create mode 100644 .github/workflows/project-prioritization-r2-assignment.yml create mode 100644 .github/workflows/project-prioritization-r5-assignment.yml create mode 100644 scripts/@aws-cdk/script-tests/prioritization/assign-priority.test.js create mode 100644 scripts/@aws-cdk/script-tests/prioritization/assign-r2-priority.test.js create mode 100644 scripts/@aws-cdk/script-tests/prioritization/assign-r5-priority.test.js create mode 100644 scripts/@aws-cdk/script-tests/prioritization/helpers/mock-data.js create mode 100644 scripts/prioritization/README.md create mode 100644 scripts/prioritization/assign-priority.js create mode 100644 scripts/prioritization/assign-r2-priority.js create mode 100644 scripts/prioritization/assign-r5-priority.js create mode 100644 scripts/prioritization/project-api.js create mode 100644 scripts/prioritization/project-config.js diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c4a166772274e..a963b61de4eb3 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -55,6 +55,11 @@ When approved this pushes the PR to the testing pipeline, thus starting the cli integ test build. Owner: Core CDK team +### Initial Priority Assignment + +[project-prioritization-assignment.yml](project-prioritization-assignment.yml): GitHub action for automatically adding PR's with priorities to the project priority board based on their labels. +Owner: CDK Support team + ## Issue Triggered ### Closed Issue Message @@ -103,3 +108,13 @@ Owner: Core CDK team [update-contributors.yml](update-contributors.yml): GitHub action that runs monthly to create a pull request for updating a CONTRIBUTORS file with the top contributors. Owner: Core CDK team + +### R2 Priority Assignment + +[project-prioritization-r2-assignment.yml](project-prioritization-r2-assignment.yml): GitHub action that runs every 6 hours to add PR's to the priority project board that satisfies R2 Priority. +Owner: CDK Support team + +### R5 Priority Assignment + +[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 diff --git a/.github/workflows/project-prioritization-assignment.yml b/.github/workflows/project-prioritization-assignment.yml new file mode 100644 index 0000000000000..f622f9b25f32a --- /dev/null +++ b/.github/workflows/project-prioritization-assignment.yml @@ -0,0 +1,22 @@ +name: PR Prioritization +on: + pull_request_target: + types: + - labeled + - opened + - reopened + - synchronize + - ready_for_review + +jobs: + prioritize: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Add PR to Project & Set Priority + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.PROJEN_GITHUB_TOKEN }} + script: | + const script = require('./scripts/prioritization/assign-priority.js') + await script({github, context}) diff --git a/.github/workflows/project-prioritization-r2-assignment.yml b/.github/workflows/project-prioritization-r2-assignment.yml new file mode 100644 index 0000000000000..d61c28b7f9175 --- /dev/null +++ b/.github/workflows/project-prioritization-r2-assignment.yml @@ -0,0 +1,19 @@ +name: PR Prioritization R2 Check +on: + schedule: + - cron: '0 */6 * * 1-5' # Runs every 6 hours during weekdays + workflow_dispatch: # Manual trigger + +jobs: + update_project_status: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check and assign R2 Priority to PRs + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.PROJEN_GITHUB_TOKEN }} + script: | + const script = require('./scripts/prioritization/assign-r2-priority.js') + await script({github}) diff --git a/.github/workflows/project-prioritization-r5-assignment.yml b/.github/workflows/project-prioritization-r5-assignment.yml new file mode 100644 index 0000000000000..300e12fa80b34 --- /dev/null +++ b/.github/workflows/project-prioritization-r5-assignment.yml @@ -0,0 +1,18 @@ +name: PR Prioritization R5 Check +on: + schedule: + - cron: '0 6 * * 1-5' # Runs at 6AM every day during weekdays + workflow_dispatch: # Manual trigger + +jobs: + update_project_status: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check and Assign R5 Priority to PRs + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.PROJEN_GITHUB_TOKEN }} + script: | + const script = require('./scripts/prioritization/assign-r5-priority.js') + await script({github}) \ No newline at end of file diff --git a/scripts/@aws-cdk/script-tests/prioritization/assign-priority.test.js b/scripts/@aws-cdk/script-tests/prioritization/assign-priority.test.js new file mode 100644 index 0000000000000..d77a9eaccc937 --- /dev/null +++ b/scripts/@aws-cdk/script-tests/prioritization/assign-priority.test.js @@ -0,0 +1,237 @@ +const { PRIORITIES, LABELS, STATUS, ...PROJECT_CONFIG} = require('../../../../scripts/prioritization/project-config'); +const { + createMockPR, + createMockGithub, + OPTION_IDS +} = require('./helpers/mock-data'); + +const assignPriority = require('../../../../scripts/prioritization/assign-priority'); + + +describe('Priority Assignment (R1, R3, R4)', () => { + let mockGithub; + let mockContext; + + beforeEach(() => { + mockGithub = createMockGithub(); + jest.clearAllMocks(); + }); + + async function verifyProjectState(expectedPriority, expectedStatus) { + const calls = mockGithub.graphql.mock.calls; + + if (!expectedPriority) { + const priorityUpdateCall = calls.find(call => + call[1].input?.fieldId === PROJECT_CONFIG.priorityFieldId + ); + expect(priorityUpdateCall).toBeUndefined(); + return; + } + + const priorityUpdateCall = calls.find(call => + call[1].input?.fieldId === PROJECT_CONFIG.priorityFieldId + ); + const statusUpdateCall = calls.find(call => + call[1].input?.fieldId === PROJECT_CONFIG.statusFieldId + ); + + // Verify priority was set correctly + expect(priorityUpdateCall[1].input.value.singleSelectOptionId) + .toBe(OPTION_IDS[expectedPriority]); + + // Verify status was set to Ready + expect(statusUpdateCall[1].input.value.singleSelectOptionId) + .toBe(OPTION_IDS[expectedStatus]); + } + + describe('R1 Priority Tests', () => { + test('should assign R1 and Ready status to non-draft PR with contribution/core label', async () => { + const pr = createMockPR({ + draft: false, + labels: [LABELS.CORE] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R1, STATUS.READY); + }); + + test('should assign R1 and Ready status to non-draft PR with contribution/core and needs-maintainer-review labels', async () => { + const pr = createMockPR({ + draft: false, + labels: [LABELS.CORE, LABELS.MAINTAINER_REVIEW] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R1, STATUS.READY); + }); + + test('should not add draft PR with contribution/core label to project', async () => { + const pr = createMockPR({ + draft: true, + labels: [LABELS.CORE] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(null); + }); + }); + + describe('R3 Priority Tests', () => { + test('should assign R3 and Ready status to non-draft PR with needs-maintainer-review label', async () => { + const pr = createMockPR({ + draft: false, + labels: [LABELS.MAINTAINER_REVIEW] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R3, STATUS.READY); + }); + + test('should not assign R3 to draft PR with needs-maintainer-review label', async () => { + const pr = createMockPR({ + draft: true, + labels: [LABELS.MAINTAINER_REVIEW] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(null); + }); + }); + + describe('R4 Priority Tests', () => { + test('should assign R4 and Ready status to PR with pr/reviewer-clarification-requested and needs-community-review labels', async () => { + const pr = createMockPR({ + draft: true, + labels: [ + LABELS.CLARIFICATION_REQUESTED, + LABELS.COMMUNITY_REVIEW + ] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R4, STATUS.READY); + }); + + test('should assign R4 and Ready status to PR with pr-linter/exemption-requested and needs-community-review labels', async () => { + const pr = createMockPR({ + draft: true, + labels: [ + LABELS.EXEMPTION_REQUESTED, + LABELS.COMMUNITY_REVIEW + ] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R4, STATUS.READY); + }); + + test('should assign R4 and Ready status to PR with pr/reviewer-clarification-requested and needs-maintainer-review labels', async () => { + const pr = createMockPR({ + draft: true, + labels: [ + LABELS.CLARIFICATION_REQUESTED, + LABELS.MAINTAINER_REVIEW + ] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R4, STATUS.READY); + }); + + test('should assign R4 and Ready status to PR with pr-linter/exemption-requested and needs-maintainer-review labels', async () => { + const pr = createMockPR({ + labels: [ + LABELS.EXEMPTION_REQUESTED, + LABELS.MAINTAINER_REVIEW + ] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R4, STATUS.READY); + }); + + test('should assign R4 and Ready status to PR with pr/reviewer-clarification-requested label and no review labels', async () => { + const pr = createMockPR({ + labels: [LABELS.CLARIFICATION_REQUESTED] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R4, STATUS.READY); + }); + + test('should assign R4 and Ready status to PR with pr-linter/exemption-requested label and no review labels', async () => { + const pr = createMockPR({ + draft: true, + labels: [LABELS.EXEMPTION_REQUESTED] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R4, STATUS.READY); + }); + }); + + describe('Priority Precedence Tests', () => { + test('should assign R1 over R3 when PR has both contribution/core and needs-maintainer-review labels', async () => { + const pr = createMockPR({ + draft: false, + labels: [ + LABELS.CORE, + LABELS.MAINTAINER_REVIEW + ] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R1, STATUS.READY); + }); + + test('should assign R1 over R4 when PR has both contribution/core and pr/reviewer-clarification-requested labels', async () => { + const pr = createMockPR({ + draft: false, + labels: [ + LABELS.CORE, + LABELS.CLARIFICATION_REQUESTED + ] + }); + + mockContext = { payload: { pull_request: pr } }; + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(PRIORITIES.R1, STATUS.READY); + }); + + test('should not assign any priority when no matching labels', async () => { + const pr = createMockPR({ + draft: false, + labels: [] + }); + + mockContext = { payload: { pull_request: pr } }; + + await assignPriority({ github: mockGithub, context: mockContext }); + await verifyProjectState(null); + }); + }); +}); \ No newline at end of file diff --git a/scripts/@aws-cdk/script-tests/prioritization/assign-r2-priority.test.js b/scripts/@aws-cdk/script-tests/prioritization/assign-r2-priority.test.js new file mode 100644 index 0000000000000..e6c4e1fa309a8 --- /dev/null +++ b/scripts/@aws-cdk/script-tests/prioritization/assign-r2-priority.test.js @@ -0,0 +1,115 @@ +const { PRIORITIES, LABELS, STATUS, ...PROJECT_CONFIG } = require('../../../../scripts/prioritization/project-config'); +const { + createMockGithubForR2, + OPTION_IDS +} = require('./helpers/mock-data'); + +const assignR2Priority = require('../../../../scripts/prioritization/assign-r2-priority'); + +describe('Priority Assignment (R2)', () => { + let mockGithub; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + async function verifyProjectState(expectedPriority, expectedStatus) { + const calls = mockGithub.graphql.mock.calls; + + if (!expectedPriority) { + const priorityUpdateCall = calls.find(call => + call[1].input?.fieldId === PROJECT_CONFIG.priorityFieldId + ); + expect(priorityUpdateCall).toBeUndefined(); + return; + } + + // Get the existing project item data from the mock response + const projectItemResponse = await mockGithub.graphql.mock.results[2].value; + const existingPRData = projectItemResponse.node.projectItems.nodes[0]; + + // Verify priority update + const priorityUpdateCall = calls.find(call => + call[1].input?.fieldId === PROJECT_CONFIG.priorityFieldId + ); + expect(priorityUpdateCall[1].input.value.singleSelectOptionId) + .toBe(OPTION_IDS[expectedPriority]); + + // Find any status update calls + const statusUpdateCall = calls.find(call => + call[1].input?.fieldId === PROJECT_CONFIG.statusFieldId + ); + + if (existingPRData) { + // For existing PR + // Get the existing status + const existingStatus = existingPRData.fieldValues.nodes + .find(node => node.field.name === 'Status')?.name; + + // Verify no status update was made + expect(statusUpdateCall).toBeUndefined(); + + // Verify expected status matches existing status + expect(existingStatus).toBe(expectedStatus); + } else { + // For new PR + expect(statusUpdateCall[1].input.value.singleSelectOptionId) + .toBe(OPTION_IDS[STATUS.READY]); + } + } + + describe('R2 Priority Tests', () => { + test('should assign R2 priority and Ready status to approved PR with failing checks', async () => { + mockGithub = createMockGithubForR2({ + approved: true, + checksState: 'FAILURE' + }); + + await assignR2Priority({ github: mockGithub }); + await verifyProjectState(PRIORITIES.R2, STATUS.READY); + }); + + test('should not assign R2 priority to PR without approval', async () => { + mockGithub = createMockGithubForR2({ + approved: false, + checksState: 'FAILURE' + }); + + await assignR2Priority({ github: mockGithub }); + await verifyProjectState(null); + }); + + test('should not assign R2 priority to PR with passing checks', async () => { + mockGithub = createMockGithubForR2({ + approved: true, + checksState: 'SUCCESS' + }); + + await assignR2Priority({ github: mockGithub }); + await verifyProjectState(null); + }); + + test('should update existing PR to R2 priority', async () => { + mockGithub = createMockGithubForR2({ + approved: true, + checksState: 'FAILURE', + existingPriority: PRIORITIES.R3, + existingStatus: STATUS.IN_PROGRESS + }); + + await assignR2Priority({ github: mockGithub }); + await verifyProjectState(PRIORITIES.R2, STATUS.IN_PROGRESS); + }); + + test('should not update if PR already has R2 priority', async () => { + mockGithub = createMockGithubForR2({ + approved: true, + checksState: 'FAILURE', + existingPriority: PRIORITIES.R2 + }); + + await assignR2Priority({ github: mockGithub }); + await verifyProjectState(null); + }); + }); +}); \ No newline at end of file diff --git a/scripts/@aws-cdk/script-tests/prioritization/assign-r5-priority.test.js b/scripts/@aws-cdk/script-tests/prioritization/assign-r5-priority.test.js new file mode 100644 index 0000000000000..1886cc7081b49 --- /dev/null +++ b/scripts/@aws-cdk/script-tests/prioritization/assign-r5-priority.test.js @@ -0,0 +1,116 @@ +const { PRIORITIES, LABELS, STATUS, ...PROJECT_CONFIG } = require('../../../../scripts/prioritization/project-config'); +const { + createMockPR, + createMockGithubForR5, + OPTION_IDS +} = require('./helpers/mock-data'); + +const assignR5Priority = require('../../../../scripts/prioritization/assign-r5-priority'); + +describe('Priority Assignment (R5)', () => { + let mockGithub; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + async function verifyProjectState(expectedPriority, expectedStatus) { + const calls = mockGithub.graphql.mock.calls; + + if (!expectedPriority) { + const priorityUpdateCall = calls.find(call => + call[1].input?.fieldId === PROJECT_CONFIG.priorityFieldId + ); + expect(priorityUpdateCall).toBeUndefined(); + return; + } + + const priorityUpdateCall = calls.find(call => + call[1].input?.fieldId === PROJECT_CONFIG.priorityFieldId + ); + const statusUpdateCall = calls.find(call => + call[1].input?.fieldId === PROJECT_CONFIG.statusFieldId + ); + + expect(priorityUpdateCall[1].input.value.singleSelectOptionId) + .toBe(OPTION_IDS[expectedPriority]); + + expect(statusUpdateCall[1].input.value.singleSelectOptionId) + .toBe(OPTION_IDS[expectedStatus]); + } + + describe('R5 Priority Tests', () => { + test('should assign R5 and Ready status to non-draft PR with needs-community-review label and no updates for 21 days', async () => { + mockGithub = createMockGithubForR5({ + draft: false, + labels: [LABELS.COMMUNITY_REVIEW] + }); + + await assignR5Priority({ github: mockGithub }); + await verifyProjectState(PRIORITIES.R5, STATUS.READY); + }); + + test('should not assign R5 to draft PR with needs-community-review label and no updates for 21 days', async () => { + mockGithub = createMockGithubForR5({ + draft: true, + labels: [LABELS.COMMUNITY_REVIEW] + }); + + await assignR5Priority({ github: mockGithub }); + await verifyProjectState(null); + }); + + test('should not assign R5 if PR updated within 21 days', async () => { + mockGithub = createMockGithubForR5({ + draft: false, + labels: [LABELS.COMMUNITY_REVIEW], + updatedAt: new Date(Date.now() - 20 * 24 * 60 * 60 * 1000).toISOString() + }); + + await assignR5Priority({ github: mockGithub }); + await verifyProjectState(null); + }); + + test('should not assign R5 if PR has needs-community-review and pr/reviewer-clarification-requested labels', async () => { + mockGithub = createMockGithubForR5({ + draft: false, + labels: [LABELS.COMMUNITY_REVIEW, LABELS.CLARIFICATION_REQUESTED] + }); + + await assignR5Priority({ github: mockGithub }); + await verifyProjectState(null); + }); + + test('should not assign R5 if PR has needs-community-review and pr-linter/exemption-requested labels', async () => { + mockGithub = createMockGithubForR5({ + draft: false, + labels: [LABELS.COMMUNITY_REVIEW, LABELS.EXEMPTION_REQUESTED] + }); + + await assignR5Priority({ github: mockGithub }); + await verifyProjectState(null); + }); + + test('should not assign R5 if PR does not have community review label', async () => { + mockGithub = createMockGithubForR5({ + draft: false, + labels: [] + }); + + await assignR5Priority({ github: mockGithub }); + await verifyProjectState(null); + }); + + test('should not update if PR already has R5 priority', async () => { + mockGithub = createMockGithubForR5({ + draft: false, + labels: [LABELS.COMMUNITY_REVIEW], + existingPriority: PRIORITIES.R5 + }); + + await assignR5Priority({ github: mockGithub }); + + await verifyProjectState(null); + }); + }); +}); diff --git a/scripts/@aws-cdk/script-tests/prioritization/helpers/mock-data.js b/scripts/@aws-cdk/script-tests/prioritization/helpers/mock-data.js new file mode 100644 index 0000000000000..418194e89ea31 --- /dev/null +++ b/scripts/@aws-cdk/script-tests/prioritization/helpers/mock-data.js @@ -0,0 +1,257 @@ +const { PRIORITIES, LABELS, STATUS, ...PROJECT_CONFIG } = require('../../../../../scripts/prioritization/project-config'); + +const OPTION_IDS = { + [PRIORITIES.R1]: 'r1-option-id', + [PRIORITIES.R2]: 'r2-option-id', + [PRIORITIES.R3]: 'r3-option-id', + [PRIORITIES.R4]: 'r4-option-id', + [PRIORITIES.R5]: 'r5-option-id', + [STATUS.READY]: 'ready-status-id', + [STATUS.IN_PROGRESS]: 'in_progress-status-id', + [STATUS.PAUSED]: 'paused-status-id', + [STATUS.ASSIGNED]: 'assigned-status-id', + [STATUS.DONE]: 'done-status-id' +}; + +const projectFields = { + organization: { + projectV2: { + fields: { + nodes: [ + { + id: PROJECT_CONFIG.priorityFieldId, + name: 'Priority', + options: Object.values(PRIORITIES).map(priority => ({ + id: OPTION_IDS[priority], + name: priority + })) + }, + { + id: PROJECT_CONFIG.statusFieldId, + name: 'Status', + options: Object.values(STATUS).map(status => ({ + id: OPTION_IDS[status], + name: status + })) + } + ] + } + } + } +}; + +const addItemToProject = { + addProjectV2ItemById: { + item: { id: 'new-item-id' } + } +} + +const updateFieldValueInProject = { + updateProjectV2ItemFieldValue: { + projectV2Item: { id: 'new-item-id' } + } +} + +/** + * Creates a mock PR with specified properties + */ +exports.createMockPR = ({ + number = 123, + node_id = 'PR_123', + draft = false, + labels = [], + updatedAt = new Date().toISOString(), + reviews = [], + checksState = 'SUCCESS' +}) => ({ + number, + node_id, + draft, + labels: labels.map(name => ({ name })), + updatedAt, + reviews: { nodes: reviews }, + commits: { + nodes: [{ + commit: { + statusCheckRollup: { state: checksState } + } + }] + } +}); + +/** + * Creates mock GitHub GraphQL client with predefined responses + */ +exports.createMockGithub = () => { + const graphql = jest.fn(); + + graphql + // First call - fetch project fields + .mockResolvedValueOnce(projectFields) + // Second call - add item to project + .mockResolvedValueOnce(addItemToProject) + // Third call - update priority + .mockResolvedValueOnce(updateFieldValueInProject) + // Fourth call - update status + .mockResolvedValueOnce(updateFieldValueInProject); + + return { graphql }; +}; + +/** + * Creates mock GitHub GraphQL client with predefined responses for R5 priority + */ +exports.createMockGithubForR5 = ({ + draft = false, + labels = [], + updatedAt = new Date(Date.now() - 22 * 24 * 60 * 60 * 1000).toISOString(), + existingPriority = null +}) => { + const graphql = jest.fn(); + + // Set up mock responses in sequence + graphql + // First call - fetch open PRs + .mockResolvedValueOnce({ + repository: { + pullRequests: { + nodes: [{ + id: 'PR_123', + number: 123, + draft, + updatedAt, + labels: { + nodes: labels.map(label => ({ name: label })) + } + }], + pageInfo: { + hasNextPage: false, + endCursor: null + } + } + } + }) + // Second call - fetch project fields + .mockResolvedValueOnce(projectFields) + // Third call - fetchProjectItem (check if PR is in project) + .mockResolvedValueOnce({ + node: { + projectItems: { + nodes: existingPriority ? [{ + id: 'existing-item-id', + project: { + id: PROJECT_CONFIG.projectId + }, + fieldValues: { + nodes: [{ + field: { name: 'Priority' }, + name: existingPriority + }] + } + }] : [] + } + } + }) + // Fourth call - add item to project + .mockResolvedValueOnce(addItemToProject) + // Fifth call - update priority + .mockResolvedValueOnce(updateFieldValueInProject) + // Sixth call - update status + .mockResolvedValueOnce(updateFieldValueInProject); + + return { graphql }; +}; + +/** + * Creates mock GitHub GraphQL client with predefined responses for R2 priority + */ +exports.createMockGithubForR2 = ({ + approved = false, + checksState = 'SUCCESS', + existingPriority = null, + existingStatus = STATUS.READY +}) => { + const graphql = jest.fn(); + + // Set up mock responses in sequence + graphql + // First call - fetch open PRs + .mockResolvedValueOnce({ + repository: { + pullRequests: { + nodes: [{ + id: 'PR_123', + number: 123, + reviews: { + nodes: approved ? [ + { state: 'APPROVED' } + ] : [] + }, + commits: { + nodes: [{ + commit: { + statusCheckRollup: { + state: checksState + } + } + }] + } + }], + pageInfo: { + hasNextPage: false, + endCursor: null + } + } + } + }) + // Second call - fetch project fields + .mockResolvedValueOnce(projectFields) + // Third call - check if PR is in project + .mockResolvedValueOnce({ + node: { + projectItems: { + nodes: existingPriority ? [{ + id: 'existing-item-id', + project: { + id: PROJECT_CONFIG.projectId + }, + fieldValues: { + nodes: [ + { + field: { name: 'Priority' }, + name: existingPriority + }, + { + field: { name: 'Status' }, + name: existingStatus + } + ] + } + }] : [] + } + } + }); + + // If PR exists and needs priority update + if (existingPriority && existingPriority !== PRIORITIES.R2) { + // Fourth call - update priority only + graphql.mockResolvedValueOnce({ + updateProjectV2ItemFieldValue: { + projectV2Item: { id: 'existing-item-id' } + } + }); + } + // If PR doesn't exist in project + else if (!existingPriority) { + // Fourth call - add to project + graphql.mockResolvedValueOnce(addItemToProject) + // Fifth call - update priority + .mockResolvedValueOnce(updateFieldValueInProject) + //Sixth call - update status + .mockResolvedValueOnce(updateFieldValueInProject); + } + + return { graphql }; +}; + +exports.OPTION_IDS = OPTION_IDS; \ No newline at end of file diff --git a/scripts/prioritization/README.md b/scripts/prioritization/README.md new file mode 100644 index 0000000000000..9d990b0610218 --- /dev/null +++ b/scripts/prioritization/README.md @@ -0,0 +1,115 @@ +# Prioritization Github Action workflow Automation + +## Setup + +Note: This configuration needs to be updated only when project fields are modified. + +### Prerequisites +1. GitHub CLI installed (`gh`) +2. Appropriate permissions to access AWS organization +3. GitHub token with `read:org` and `project` scopes + +### Project Configuration +To set up the prioritization automation, we need to get the field IDs of the project board and update the configuration. Follow these steps: + +1. Add the github token to `GH_TOKEN` environment variable: + + ```bash + export GH_TOKEN="YOUR GITHUB TOKEN" + +2. Retrieve project field IDs for the specific project: + + ```bash + # Get project and field IDs + gh api graphql -f query=' + query { + organization(login: "aws") { + projectV2(number: YOUR PROJECT NUMBER) { + id + fields(first: 20) { + nodes { + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + ' | jq '.data.organization.projectV2 as $project | { + projectId: $project.id, + fields: [ + $project.fields.nodes[] | + select(.name == "Priority" or .name == "Status" or .name == "Needs Attention") | + {name: .name, id: .id} + ] + }' + +3. Update configuration with the returned IDs: + + ```javascript + // project-config.js + module.exports = { + ... + projectNumber: 263, // Project Number + projectId: "xxx", // Project ID + priorityFieldId: "xxx", // Priority field ID + statusFieldId: "xxx", // Status field ID + attentionFieldId: "xxx", // Needs Attention field ID + }; + + +## Available Views +1. [Prioritized Backlog](https://github.com/orgs/aws/projects/263/views/1) : Overall view of all PRs with prioritization +2. [My Items](https://github.com/orgs/aws/projects/263/views/6) : Filtered view showing only PRs assigned to you + +## Common Labels and Categories + +### Priority Labels +`R1` -> Non-draft PRs from the team (`contribution/core`) +`R2` -> Approved PRs with failing/pending checks +`R3` -> Non-draft PRs that needs maintainer review (`pr/needs-maintainer-review`) +`R4` -> PRs that needs clarification or exemption (`pr/reviewer-clarification-requested, pr-linter/exemption-requested`), draft state allowed +`R5` -> Non-draft PRs that are in needs-community-review more than 21 days (`pr/needs-community-review`) + +### Work Status Labels +`Ready` -> Means the PR is ready to be picked up for review +`Assigned` -> Means a team member have picked the PR and assigned to themselves +`In progress` -> Currently being reviewed +`Paused` -> PR review is paused for some reason. Eg: security review +`Done` -> PR review is completed and merged or closed + +### Needs Attention Labels +`Extended` -> If the status being in 7-14 days. Taking longer than expected +`Aging` -> If the status being in 14-21 days. Requires immediate attention +`Stalled` -> If the status being in > 21 days. Critical attention required + +These `Needs Attention` states apply to items in the following status labels: +- Ready: Awaiting assignment +- Assigned: Awaiting start +- In Progress: Under review +- Paused: Blocked/On hold + +## Workflows + +### Prioritized Backlog Workflow +1. PRs are automatically categorized by priority (`R1-R5`) +2. Team members can select PRs from the Ready state +3. Status updates flow through: `Assigned` β†’ `In Progress` β†’ `Done/Paused` +4. Time-based monitoring labels are automatically applied in `Needs Attention` based on duration in each state + +### My Items Workflow +1. PRs appear when assigned to you +2. Update status as you progress with reviews +3. Track your active reviews and blocked items +4. Monitor time-based alerts for your assignments + +## Automation +- Priority labels are automatically assigned based on PR labels +- Time-based monitoring states are automatically updated daily +- Status changes trigger automatic label updates \ No newline at end of file diff --git a/scripts/prioritization/assign-priority.js b/scripts/prioritization/assign-priority.js new file mode 100644 index 0000000000000..b9c232484bfc8 --- /dev/null +++ b/scripts/prioritization/assign-priority.js @@ -0,0 +1,135 @@ +/** + * Handles the initial priority assignment for PRs when labels are added. This script + * processes R1 (team PRs with contribution/core label), R3 (PRs needing maintainer review), + * and R4 (PRs needing clarification or exemption) priorities. When a matching label + * is detected, the PR is added to the project board with appropriate priority and + * set to Ready status. + */ + + +const { PRIORITIES, LABELS, STATUS, ...PROJECT_CONFIG } = require('./project-config'); +const { + updateProjectField, + addItemToProject, + fetchProjectFields, +} = require('./project-api'); + +module.exports = async ({ github, context }) => { + const getPriority = (pr) => { + const labels = pr.labels.map((l) => l.name); + const isDraft = pr.draft === true; + + const hasExemptionOrClarification = labels.some(label => + [LABELS.CLARIFICATION_REQUESTED, LABELS.EXEMPTION_REQUESTED].includes(label) + ); + + // R1: Not draft + contribution/core + if (!isDraft && labels.includes(LABELS.CORE)) { + return PRIORITIES.R1; + } + + // R3: Not draft + needs-maintainer-review + no contribution/core + no exemption/clarification + if (!isDraft && + labels.includes(LABELS.MAINTAINER_REVIEW) && + !labels.includes(LABELS.CORE) && + !hasExemptionOrClarification) { + return PRIORITIES.R3; + } + + // R4: Three conditions (draft allowed) + if (hasExemptionOrClarification && ( + // Condition 1: With community review + labels.includes(LABELS.COMMUNITY_REVIEW) || + // Condition 2: With maintainer review + labels.includes(LABELS.MAINTAINER_REVIEW) || + // Condition 3: No community or maintainer review + (!labels.includes(LABELS.COMMUNITY_REVIEW) && + !labels.includes(LABELS.MAINTAINER_REVIEW)) + )) { + return PRIORITIES.R4; + } + + return null; + }; + + async function addToProject(pr) { + + // Check if PR qualifies for any priority + const priority = getPriority(pr); + if (!priority) { + console.log(`PR #${pr.number} doesn't qualify for any priority. Skipping.`); + return; + } + + console.log(`Processing PR #${pr.number} for ${priority} priority`); + + // Get project fields + const projectFields = await fetchProjectFields({ + github, + org: PROJECT_CONFIG.org, + number: PROJECT_CONFIG.projectNumber + }); + + const priorityField = projectFields.organization.projectV2.fields.nodes.find( + (field) => field.id === PROJECT_CONFIG.priorityFieldId + ); + + const statusField = projectFields.organization.projectV2.fields.nodes.find( + (field) => field.id === PROJECT_CONFIG.statusFieldId + ); + + try { + // Add PR to project + const addResult = await addItemToProject({ + github, + projectId: PROJECT_CONFIG.projectId, + contentId: pr.node_id, + }); + + const itemId = addResult.addProjectV2ItemById.item.id; + + // Set priority + const priorityOptionId = priorityField.options.find( + (option) => option.name === priority + )?.id; + + if (!priorityOptionId) { + console.error(`Priority option ${priority} not found in project settings`); + return; + } + + // Set Ready status + const readyOptionId = statusField.options.find( + (option) => option.name === STATUS.READY + )?.id; + + if (!readyOptionId) { + console.error('Ready status option not found in project settings'); + return; + } + + // Set Priority and Ready Status + await Promise.all([ + updateProjectField({ + github, + projectId: PROJECT_CONFIG.projectId, + itemId: itemId, + fieldId: PROJECT_CONFIG.priorityFieldId, + value: priorityOptionId, + }), + updateProjectField({ + github, + projectId: PROJECT_CONFIG.projectId, + itemId: itemId, + fieldId: PROJECT_CONFIG.statusFieldId, + value: readyOptionId, + }) + ]); + } catch (error) { + console.error(`Error processing PR #${pr.number}:`, error); + } + } + + const pr = context.payload.pull_request; + await addToProject(pr); +}; diff --git a/scripts/prioritization/assign-r2-priority.js b/scripts/prioritization/assign-r2-priority.js new file mode 100644 index 0000000000000..0d5379f53a8fa --- /dev/null +++ b/scripts/prioritization/assign-r2-priority.js @@ -0,0 +1,152 @@ +/** + * Processes open PRs every 6 hours during weekdays to identify and assign R2 priority. A PR qualifies + * for R2 when it has received approval but has failing or pending checks, regardless of its current + * priority or status. These PRs are either added to the project board with R2 priority and Ready status + * (if not already in board) or updated to R2 priority (if already in board with different priority). + */ + +const { PRIORITIES, LABELS, STATUS, ...PROJECT_CONFIG } = require("./project-config"); + +const { + updateProjectField, + addItemToProject, + fetchProjectFields, + fetchOpenPullRequests, + fetchProjectItem, +} = require('./project-api'); + + +module.exports = async ({ github }) => { + let allPRs = []; + let hasNextPage = true; + let cursor = null; + + // Fetch all PRs using pagination + while (hasNextPage) { + const result = await fetchOpenPullRequests({ + github, + owner: PROJECT_CONFIG.owner, + repo: PROJECT_CONFIG.repo, + cursor: cursor, + }); + + const pullRequests = result.repository.pullRequests; + allPRs = allPRs.concat(pullRequests.nodes); + + // Update pagination info + hasNextPage = pullRequests.pageInfo.hasNextPage; + cursor = pullRequests.pageInfo.endCursor; + } + + console.log(`Total PRs fetched: ${allPRs.length}`); + + // Get project fields + const projectFields = await fetchProjectFields({ + github, + org: PROJECT_CONFIG.org, + number: PROJECT_CONFIG.projectNumber + }); + + const priorityField = projectFields.organization.projectV2.fields.nodes.find( + (field) => field.id === PROJECT_CONFIG.priorityFieldId + ); + + const statusField = projectFields.organization.projectV2.fields.nodes.find( + (field) => field.id === PROJECT_CONFIG.statusFieldId + ); + + const r2OptionId = priorityField.options.find( + (option) => option.name === PRIORITIES.R2 + )?.id; + + const readyStatusId = statusField.options.find( + (option) => option.name === STATUS.READY + )?.id; + + for (const pr of allPRs) { + try { + + // Check PR status + const isApproved = pr.reviews.nodes.some( + (review) => review.state === "APPROVED" + ); + + // Skip if PR is not approved + if (!isApproved) { + continue; + } + + // Check status of checks + const checksState = pr.commits.nodes[0]?.commit.statusCheckRollup?.state; + const checksNotPassing = checksState !== "SUCCESS"; + + // Skip if PR checks is not passing + if (!checksNotPassing) { + continue; + } + + console.log(`Processing PR #${pr.number} for ${PRIORITIES.R2} priority consideration`); + + // Get all projects the PR added to + const result = await fetchProjectItem({ + github, + contentId: pr.id + }); + + // Filter our specific project + const projectItem = result.node.projectItems.nodes + .find(item => item.project.id === PROJECT_CONFIG.projectId); + + if (projectItem) { + // PR already in project + const currentPriority = projectItem.fieldValues.nodes + .find(fv => fv.field?.name === 'Priority')?.name; + + if (currentPriority === PRIORITIES.R2) { + console.log(`PR #${pr.number} already has ${PRIORITIES.R2} priority. Skipping.`); + continue; + } + + // Update priority only, maintain existing status + console.log(`Updating PR #${pr.number} from ${currentPriority} to ${PRIORITIES.R2} priority`); + await updateProjectField({ + github, + projectId: PROJECT_CONFIG.projectId, + itemId: projectItem.id, + fieldId: PROJECT_CONFIG.priorityFieldId, + value: r2OptionId, + }); + } else { + // Add new PR to project with R2 priority and Ready status + console.log(`Adding PR #${pr.number} to project with ${PRIORITIES.R2} priority`); + const addResult = await addItemToProject({ + github, + projectId: PROJECT_CONFIG.projectId, + contentId: pr.id, + }); + itemId = addResult.addProjectV2ItemById.item.id; + + // Set both priority and initial status for new items + await Promise.all([ + updateProjectField({ + github, + projectId: PROJECT_CONFIG.projectId, + itemId: itemId, + fieldId: PROJECT_CONFIG.priorityFieldId, + value: r2OptionId, + }), + updateProjectField({ + github, + projectId: PROJECT_CONFIG.projectId, + itemId: itemId, + fieldId: PROJECT_CONFIG.statusFieldId, + value: readyStatusId, + }) + ]); + } + } catch (error) { + console.error(`Error processing PR #${pr.number}:`, error); + continue; + } + } +}; diff --git a/scripts/prioritization/assign-r5-priority.js b/scripts/prioritization/assign-r5-priority.js new file mode 100644 index 0000000000000..bee4e1921a845 --- /dev/null +++ b/scripts/prioritization/assign-r5-priority.js @@ -0,0 +1,145 @@ +/** + * Monitors open PRs once daily during weekdays to identify stale community review requests. When a PR + * with the community review label hasn't been updated for the specified threshold + * period (default 21 days), it's assigned R5 priority. These PRs are added to the + * project board and set to Ready status to ensure visibility of long-pending + * community reviews. + */ + +const { PRIORITIES, LABELS, STATUS, DAYS_THRESHOLD, ...PROJECT_CONFIG } = require("./project-config"); + +const { + updateProjectField, + addItemToProject, + fetchProjectFields, + fetchOpenPullRequests, + fetchProjectItem, +} = require('./project-api'); + +const MS_PER_DAY = 1000 * 60 * 60 * 24; + +module.exports = async ({ github }) => { + let allPRs = []; + let hasNextPage = true; + let cursor = null; + + // Fetch all PRs using pagination + while (hasNextPage) { + const result = await fetchOpenPullRequests({ + github, + owner: PROJECT_CONFIG.owner, + repo: PROJECT_CONFIG.repo, + cursor: cursor, + }); + + const pullRequests = result.repository.pullRequests; + allPRs = allPRs.concat(pullRequests.nodes); + + // Update pagination info + hasNextPage = pullRequests.pageInfo.hasNextPage; + cursor = pullRequests.pageInfo.endCursor; + } + + console.log(`Total PRs fetched: ${allPRs.length}`); + + // Get project fields + const projectFields = await fetchProjectFields({ + github, + org: PROJECT_CONFIG.org, + number: PROJECT_CONFIG.projectNumber + }); + + const priorityField = projectFields.organization.projectV2.fields.nodes.find( + (field) => field.id === PROJECT_CONFIG.priorityFieldId + ); + + const statusField = projectFields.organization.projectV2.fields.nodes.find( + (field) => field.id === PROJECT_CONFIG.statusFieldId + ); + + const r5OptionId = priorityField.options.find( + (option) => option.name === PRIORITIES.R5 + )?.id; + + const readyStatusId = statusField.options.find( + (option) => option.name === STATUS.READY + )?.id; + + for (const pr of allPRs) { + const labels = pr.labels.nodes.map((l) => l.name); + const isDraft = pr.draft === true; + + // Skip draft PRs + if (isDraft) { + console.log(`Skipping draft PR #${pr.number}`); + continue; + } + + const hasExemptionOrClarification = labels.some(label => + [LABELS.CLARIFICATION_REQUESTED, LABELS.EXEMPTION_REQUESTED].includes(label) + ); + + // Skip if PR doesn't have community review label or has exemption/clarification + if (!labels.includes(LABELS.COMMUNITY_REVIEW) || hasExemptionOrClarification) { + continue; + } + + const lastUpdated = new Date(pr.updatedAt); + const daysSinceUpdate = (Date.now() - lastUpdated) / MS_PER_DAY; + + // Skip if PR update is within the days threshold + if (daysSinceUpdate <= DAYS_THRESHOLD) { + continue; + } + + console.log(`Processing PR #${pr.number} for ${PRIORITIES.R5} priority consideration`); + + try { + // Get all projects the PR added to + const result = await fetchProjectItem({ + github, + contentId: pr.id + }); + + // Filter specific project + const projectItem = result.node.projectItems.nodes + .find(item => item.project.id === PROJECT_CONFIG.projectId); + + // Skip if PR is already in project + if (projectItem) { + console.log(`PR #${pr.number} is already in project. Skipping.`); + continue; + } + + // Add new PR to project with R5 priority + console.log(`Adding PR #${pr.number} to project with ${PRIORITIES.R5} priority`); + + const addResult = await addItemToProject({ + github, + projectId: PROJECT_CONFIG.projectId, + contentId: pr.id, + }); + + // Set initial priority and status + await Promise.all([ + updateProjectField({ + github, + projectId: PROJECT_CONFIG.projectId, + itemId: addResult.addProjectV2ItemById.item.id, + fieldId: PROJECT_CONFIG.priorityFieldId, + value: r5OptionId, + }), + updateProjectField({ + github, + projectId: PROJECT_CONFIG.projectId, + itemId: addResult.addProjectV2ItemById.item.id, + fieldId: PROJECT_CONFIG.statusFieldId, + value: readyStatusId, + }) + ]); + } catch (error) { + console.error(`Error processing PR #${pr.number}:`, error); + continue; + } +} +} diff --git a/scripts/prioritization/project-api.js b/scripts/prioritization/project-api.js new file mode 100644 index 0000000000000..7b1d1ce006b7d --- /dev/null +++ b/scripts/prioritization/project-api.js @@ -0,0 +1,201 @@ +/** + * Updates a field value for an item in a GitHub Project. + * @param {Object} params - The parameters for updating the project field + * @param {Object} params.github - The GitHub API client + * @param {string} params.projectId - The ID of the project + * @param {string} params.itemId - The ID of the item to update + * @param {string} params.fieldId - The ID of the field to update + * @param {string} params.value - The new value for the field + * @returns {Promise} The GraphQL mutation response + */ +const updateProjectField = async ({ + github, + projectId, + itemId, + fieldId, + value, + }) => { + return github.graphql( + ` + mutation($input: UpdateProjectV2ItemFieldValueInput!) { + updateProjectV2ItemFieldValue(input: $input) { + projectV2Item { + id + } + } + } + `, + { + input: { + projectId, + itemId, + fieldId, + value: value ? { singleSelectOptionId: value } : null, + }, + } + ); + }; + +/** + * Adds an item (PR) to a GitHub Project. + * @param {Object} params - The parameters for adding an item to the project + * @param {Object} params.github - The GitHub API client + * @param {string} params.projectId - The ID of the project + * @param {string} params.contentId - The node ID of the PR to add + * @returns {Promise} The GraphQL mutation response with the new item's ID + */ + const addItemToProject = async ({ github, projectId, contentId }) => { + return github.graphql( + ` + mutation($input: AddProjectV2ItemByIdInput!) { + addProjectV2ItemById(input: $input) { + item { + id + } + } + } + `, + { + input: { + projectId, + contentId, + }, + } + ); + }; + +/** + * Fetches fields configuration for a GitHub Project. + * @param {Object} params - The parameters for fetching project fields + * @param {Object} params.github - The GitHub API client + * @param {string} params.org - The organization name + * @param {number} params.number - The project number + * @returns {Promise} The project fields data including field IDs and options + */ + const fetchProjectFields = async ({ github, org, number }) => { + return github.graphql( + ` + query($org: String!, $number: Int!) { + organization(login: $org) { + projectV2(number: $number) { + fields(first: 20) { + nodes { + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + `, + { org, number } + ); + }; + + +/** + * Fetches open pull requests from a repository with pagination support. + * Includes data needed for both R2 and R5 priority processing. + * @param {Object} params - The parameters for fetching pull requests + * @param {Object} params.github - The GitHub API client + * @param {string} params.owner - The repository owner + * @param {string} params.repo - The repository name + * @param {string} [params.cursor] - The pagination cursor + * @returns {Promise} The GraphQL mutation response + */ + const fetchOpenPullRequests = async ({ github, owner, repo, cursor }) => { + return github.graphql( + ` + query($owner: String!, $repo: String!, $cursor: String) { + repository(owner: $owner, name: $repo) { + pullRequests(first: 100, after: $cursor, states: OPEN) { + nodes { + id + number + updatedAt + reviews(last: 100) { + nodes { + state + } + } + commits(last: 1) { + nodes { + commit { + statusCheckRollup { + state + } + } + } + } + labels(first: 10) { + nodes { + name + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `, + { owner, repo, cursor } + ); + }; + + /** + * Fetches project item details for a specific PR + * @param {Object} params - The parameters for fetching project item + * @param {Object} params.github - The GitHub API client + * @param {string} params.contentId - PR node ID + * @returns {Promise} Project item details if PR is in project + */ + const fetchProjectItem = async ({ github, contentId }) => { + return github.graphql( + ` + query($contentId: ID!) { + node(id: $contentId) { + ... on PullRequest { + projectItems(first: 100) { + nodes { + id + project { + id + } + fieldValues(first: 8) { + nodes { + ... on ProjectV2ItemFieldSingleSelectValue { + name + field { + ... on ProjectV2SingleSelectField { + name + } + } + } + } + } + } + } + } + } + } + `, + { contentId } + ); + }; + + module.exports = { + updateProjectField, + addItemToProject, + fetchProjectFields, + fetchOpenPullRequests, + fetchProjectItem + }; \ No newline at end of file diff --git a/scripts/prioritization/project-config.js b/scripts/prioritization/project-config.js new file mode 100644 index 0000000000000..de80a31240f77 --- /dev/null +++ b/scripts/prioritization/project-config.js @@ -0,0 +1,65 @@ +const LABELS = { + CORE: 'contribution/core', + MAINTAINER_REVIEW: 'pr/needs-maintainer-review', + COMMUNITY_REVIEW: 'pr/needs-community-review', + CLARIFICATION_REQUESTED: 'pr/reviewer-clarification-requested', + EXEMPTION_REQUESTED: 'pr-linter/exemption-requested' +}; + +const PRIORITIES = { + R1: '🚨 R1', + R2: 'πŸ”₯ R2', + R3: '🎯 R3', + R4: 'πŸ’­ R4', + R5: 'πŸ“† R5' +}; + +const STATUS = { + READY: '⭐ Ready', + IN_PROGRESS: 'πŸ”„ In Progress', + PAUSED: '⏸️ Paused', + ASSIGNED: 'πŸ‘€ Assigned', + DONE: 'βœ… Done' +}; + +// Time threshold for R5 +const DAYS_THRESHOLD = 21; + +const ATTENTION_STATUS = { + STALLED: { + name: '🚨 Stalled', + threshold: 21, + description: 'Critical attention required' + }, + AGING: { + name: '⚠️ Aging', + threshold: 14, + description: 'Requires immediate attention' + }, + EXTENDED: { + name: 'πŸ•’ Extended', + threshold: 7, + description: 'Taking longer than expected' + } +}; + +/** + * Project configuration for GitHub project automation. + * Note: For projectId, priorityFieldId, statusFieldId, and attentionFieldId, + * refer to Setup section in README.md on how to retrieve these values using GraphQL query. + * These IDs need to be updated only when project fields are modified. + */ +module.exports = { + org: 'aws', + repo: 'aws-cdk', + projectNumber: 263, + projectId: 'PVT_kwDOACIPmc4Av_32', + priorityFieldId: 'PVTSSF_lADOACIPmc4Av_32zgmVmPs', + statusFieldId: 'PVTSSF_lADOACIPmc4Av_32zgmVmF8', + attentionFieldId: 'PVTSSF_lADOACIPmc4Av_32zgmZDdo', + LABELS, + PRIORITIES, + STATUS, + ATTENTION_STATUS, + DAYS_THRESHOLD +};