Unverified 提交 d13e0738 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

feat: add workflow to draft stale PRs after 7 days of inactivity (#2586)

## Summary - Adds a new daily GitHub Actions workflow (`draft-stale-prs.yml`) that automatically converts open PRs to draft state when there has been no activity for 7 days - Checks four activity signals: last commit push, issue comments, review comments, and reopen events - Leaves a comment explaining why the PR was converted to draft ## Test plan - [ ] Trigger the workflow manually via `workflow_dispatch` and verify it runs without errors - [ ] Verify it skips PRs that are already in draft state - [ ] Verify it correctly identifies stale PRs with no activity in 7 days 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2586" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > CI/workflow-only change that affects PR state and adds comments; main risk is unintended drafting due to activity detection edge cases or API/rate-limit behavior. > > **Overview** > Adds a new scheduled/manual GitHub Actions workflow (`draft-stale-prs.yml`) that scans open PRs and converts *non-draft* PRs to draft when there has been no activity for 7+ days. > > The workflow computes “last activity” from the latest commit, non-bot issue comments, review comments, review submissions, and `reopened`/`ready_for_review` timeline events, then posts an explanatory comment after converting; it also caps processing to 30 PRs and aborts on rate-limit/permission errors. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c9fc2ae9527e48f4ab4452ce0f2ecb2c0eb5f2fa. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
上级 56c2b0fc
name: Draft stale PRs
on:
schedule:
- cron: "0 0 * * *" # daily at midnight UTC
workflow_dispatch:
permissions:
contents: read
pull-requests: write
issues: write
jobs:
draft-stale-prs:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const allPrs = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'asc',
});
// Pre-filter to non-draft, stale PRs and cap to avoid rate limit exhaustion
const prs = allPrs.filter(pr => !pr.draft && new Date(pr.updated_at) < sevenDaysAgo).slice(0, 30);
for (const pr of prs) {
try {
// Check the latest commit date using the PR head SHA
const headCommit = await github.rest.git.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: pr.head.sha,
});
const lastCommitDate = new Date(headCommit.data.committer.date);
// Check comments (issue comments + review comments)
// Note: issues.listComments doesn't support sort/direction params,
// returns in ascending order. Fetch all and filter out bot comments.
const allComments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
});
const humanComments = allComments.filter(
c => !c.user?.login?.endsWith('[bot]')
);
const lastCommentDate = humanComments.length > 0
? new Date(humanComments[humanComments.length - 1].created_at)
: new Date(0);
const allReviewComments = await github.paginate(github.rest.pulls.listReviewComments, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
const lastReviewCommentDate = allReviewComments.length > 0
? new Date(allReviewComments[allReviewComments.length - 1].created_at)
: new Date(0);
// Check timeline events for re-opens and ready_for_review
// Note: must use listEventsForTimeline (not listEvents) because
// ready_for_review is only available through the Timeline Events API.
const events = await github.paginate(github.rest.issues.listEventsForTimeline, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 100,
});
const lastReopenOrReadyDate = events
.filter(e => e.event === 'reopened' || e.event === 'ready_for_review')
.reduce((latest, e) => {
const d = new Date(e.created_at);
return d > latest ? d : latest;
}, new Date(0));
// Check review submissions (approvals, request-changes, dismissals)
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
const lastReviewDate = reviews.length > 0
? new Date(reviews[reviews.length - 1].submitted_at)
: new Date(0);
const lastActivity = new Date(Math.max(
lastCommitDate.getTime(),
lastCommentDate.getTime(),
lastReviewCommentDate.getTime(),
lastReopenOrReadyDate.getTime(),
lastReviewDate.getTime(),
));
if (lastActivity < sevenDaysAgo) {
// Convert to draft using GraphQL (REST API doesn't support this)
await github.graphql(`
mutation($pullRequestId: ID!) {
convertPullRequestToDraft(input: { pullRequestId: $pullRequestId }) {
pullRequest { id }
}
}
`, { pullRequestId: pr.node_id });
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: "We flipped this PR to draft since there hasn't been any work in the last 7 days. Please mark it as ready for review when ready!",
});
console.log(`Converted PR #${pr.number} to draft: ${pr.title} (last activity: ${lastActivity.toISOString()})`);
}
} catch (error) {
core.warning(`Failed to process PR #${pr.number}: ${error.message}`);
if (error.status === 403 || error.status === 429) {
throw error;
}
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论