Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(repo): add workflow to track beginning-contributor PRs needing C…
…I approval

Add a daily scheduled workflow that monitors beginning-contributor PRs
and categorizes them into two priority buckets:

1. **Pending Approval** — CI workflow is in `action_required` state,
   waiting for a maintainer to approve the run.
2. **Pending Build Result** — CI was approved but no build result has
   been reported back (stuck pipeline or infra issue).

The workflow creates/updates a tracking issue with labels
`automation`, `ci-pending`, `tracking` so maintainers have a
single dashboard for the backlog.

Closes #38196
  • Loading branch information
pahud committed Jun 30, 2026
commit 55ea47b4f31108754ac26329aeee13bb4a69c75e
5 changes: 5 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,10 @@ Owner: CDK Support team
[project-prioritization-added-on.yml](project-prioritization-added-on.yml): GitHub action that runs every day to update AddedOn field in the prioritization project board.
Owner: CDK Support team

### Monitor CI Pending Approval

[monitor-ci-pending-approval.yml](monitor-ci-pending-approval.yml): GitHub action that runs daily to track beginning-contributor PRs that need maintainer action — either CI approval or build result investigation. Updates a tracking issue with the current backlog.
Owner: Core CDK team

### Issue sync
[issue-sync.yml](issue-sync.yml): Github action that syncs issue metadat with the project board. More details can be found on the [project-sync](../../tools/@aws-cdk/project-sync) package.
194 changes: 194 additions & 0 deletions .github/workflows/monitor-ci-pending-approval.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
name: Monitor CI Pending Approval

on:
schedule:
# Run daily at 08:00 UTC
- cron: '0 8 * * *'
workflow_dispatch:

jobs:
check-prs:
# this workflow will always fail in forks; bail if this isn't running in the upstream
if: github.repository == 'aws/aws-cdk'
runs-on: ubuntu-latest
permissions:
issues: write
contents: read
environment: automation

steps:
- name: Check for PRs pending CI approval
id: check-prs
uses: actions/github-script@v7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the latest actions/github-script@v9 here ?

with:
github-token: ${{ secrets.PROJEN_GITHUB_TOKEN }}
script: |
const owner = 'aws';
const repo = 'aws-cdk';

// Search for beginning-contributor PRs
const { data: searchResults } = await github.rest.search.issuesAndPullRequests({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the Search API here or can we just use github.rest.issues.listForRepo?
Also, per_page: 100 on Search will only return the first page, so this will miss beginning-contributor PRs if they exceed 100.

q: `is:pr is:open repo:${owner}/${repo} label:beginning-contributor`,
per_page: 100
});

console.log(`Found ${searchResults.total_count} beginning-contributor PRs`);

const pendingApproval = [];
const pendingBuildResult = [];

for (const item of searchResults.items) {
const prNumber = item.number;

try {
// Get PR details
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});

// Skip draft PRs
if (pr.draft) {
continue;
}

// Check workflow runs for action_required (true pending approval)
const { data: workflowRuns } = await github.rest.actions.listWorkflowRunsForRepo({
owner,
repo,
head_sha: pr.head.sha,
per_page: 20
});

const hasActionRequired = workflowRuns.workflow_runs.some(run =>
run.conclusion === 'action_required'
);

if (hasActionRequired) {
pendingApproval.push({
number: prNumber,
title: pr.title,
author: pr.user.login,
url: pr.html_url,
created_at: pr.created_at
});
console.log(`PR #${prNumber}: Pending approval (action_required)`);
continue;
}

// Check if build has completed with a result
const buildRun = workflowRuns.workflow_runs.find(run =>
run.name === 'PR Build' || run.name === 'Codebuild PR Build'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid identifying the build workflow by run.name here since these are display name and can be renamed anytime. We can instead match on run.path which is more stable identifier.

);

// If no build run exists at all, or build hasn't reported back
const { data: status } = await github.rest.repos.getCombinedStatusForRef({
owner,
repo,
ref: pr.head.sha
});

if (status.state === 'pending' && status.total_count === 0 && !buildRun) {
// No CI activity at all — but also no action_required

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment and logging below seems inaccurate since buildRun only matches runs named PR Build or Codebuild PR Build, it's undefined whenever the build workflow hasn't produced a run, even if the PR has other CI activity. For example PR #38083 has other CI activity but no build-named run.

// This means approval was skipped but no build triggered
pendingBuildResult.push({
number: prNumber,
title: pr.title,
author: pr.user.login,
url: pr.html_url,
created_at: pr.created_at
});
console.log(`PR #${prNumber}: Pending build result (no CI activity)`);
continue;
}

if (buildRun && (buildRun.status === 'queued' || buildRun.status === 'in_progress')) {
console.log(`PR #${prNumber}: Build in progress, skipping`);
continue;
}

// Build ran but no commit status reported — stuck pipeline
if (status.state === 'pending' && status.total_count === 0 && buildRun && buildRun.conclusion !== null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we seen any PRs for such scenario?

pendingBuildResult.push({
number: prNumber,
title: pr.title,
author: pr.user.login,
url: pr.html_url,
created_at: pr.created_at
});
console.log(`PR #${prNumber}: Pending build result (build completed but no status)`);
}

} catch (error) {
console.error(`Error processing PR #${prNumber}:`, error.message);
}
}

return { pendingApproval, pendingBuildResult };

- name: Update tracking issue
uses: actions/github-script@v7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the latest actions/github-script@v9 here ?

with:
github-token: ${{ secrets.PROJEN_GITHUB_TOKEN }}
script: |
const results = ${{ steps.check-prs.outputs.result }};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interpolates the previous step's output directly into the source of this script. Can we instead pass the values via env to avoid any potential syntax errors:

Suggested change
const results = ${{ steps.check-prs.outputs.result }};
env:
CHECK_RESULT: ${{ steps.check-prs.outputs.result }}
with:
script: |
const results = JSON.parse(process.env.CHECK_RESULT);

const { pendingApproval, pendingBuildResult } = results;
const total = pendingApproval.length + pendingBuildResult.length;
const owner = 'aws';
const repo = 'aws-cdk';
const labels = ['automation', 'ci-pending', 'tracking'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding 3 new labels?


let issueBody = `## 📊 Beginning Contributor CI Report\n\n`;
issueBody += `\`\`\`\n`;
issueBody += `🔒 Pending Approval: ${pendingApproval.length} PR(s)\n`;
issueBody += `⏳ Pending Build Result: ${pendingBuildResult.length} PR(s)\n`;
issueBody += `\`\`\`\n\n`;

if (pendingApproval.length > 0) {
issueBody += `### 🔒 Pending Approval (${pendingApproval.length})\n\n`;
issueBody += `| PR | Title | Author | Created |\n`;
issueBody += `|----|-------|--------|----------|\n`;
for (const pr of pendingApproval) {
issueBody += `| [#${pr.number}](${pr.url}) | ${pr.title} | @${pr.author} | ${pr.created_at.split('T')[0]} |\n`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we escape/sanitize contributor-controlled fields before rendering the Markdown table? An unescaped title can break the table or inject misleading content into the tracking issue.

}
issueBody += `\n`;
}

if (pendingBuildResult.length > 0) {
issueBody += `### ⏳ Pending Build Result (${pendingBuildResult.length})\n\n`;
issueBody += `| PR | Title | Author | Created |\n`;
issueBody += `|----|-------|--------|----------|\n`;
for (const pr of pendingBuildResult) {
issueBody += `| [#${pr.number}](${pr.url}) | ${pr.title} | @${pr.author} | ${pr.created_at.split('T')[0]} |\n`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

}
issueBody += `\n`;
}

if (total === 0) {
issueBody += `✅ No PRs need attention at this time.\n\n`;
}

issueBody += `---\n`;
issueBody += `*This issue is automatically updated*\n`;
issueBody += `*Monitoring repository: ${owner}/${repo}*\n`;
issueBody += `*Last checked: ${new Date().toISOString()}*`;

const issueTitle = total > 0
? `CI Pending Approval - ${total} PR(s)`
: `CI Pending Approval - No PRs`;

// Tracking issue number — update this after the issue is created
const TRACKING_ISSUE_NUMBER = 9000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we create new single tracking issue ?


await github.rest.issues.update({
owner,
repo,
issue_number: TRACKING_ISSUE_NUMBER,
title: issueTitle,
body: issueBody,
labels: labels

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issues.update call will replace existing labels on the issues based on API doc. Is this intended?

});
console.log(`Updated tracking issue #${TRACKING_ISSUE_NUMBER}: ${pendingApproval.length} pending approval, ${pendingBuildResult.length} pending build result`);

console.log(`Result: ${pendingApproval.length} pending approval, ${pendingBuildResult.length} pending build result`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logging seems redundant.

Loading