-
Notifications
You must be signed in to change notification settings - Fork 0
feat(workflow): Add PR management automation #245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,336 @@ | ||||||
| # PR Management Workflow | ||||||
| # Handles PR automation for the hybrid workflow | ||||||
| name: PR Management | ||||||
|
|
||||||
| on: | ||||||
| pull_request: | ||||||
| types: [opened, edited, synchronize, ready_for_review] | ||||||
| branches: [dev, main] | ||||||
| pull_request_review: | ||||||
| types: [submitted] | ||||||
| workflow_dispatch: | ||||||
|
|
||||||
| permissions: | ||||||
| contents: read | ||||||
| pull-requests: write | ||||||
| issues: write | ||||||
|
|
||||||
| jobs: | ||||||
| auto-label: | ||||||
| name: Auto Label PR | ||||||
| runs-on: ubuntu-latest | ||||||
| if: github.event_name == 'pull_request' | ||||||
|
|
||||||
| steps: | ||||||
| - name: Label PR by Size | ||||||
| uses: actions/github-script@v7 | ||||||
| with: | ||||||
| script: | | ||||||
| const { data: pr } = await github.rest.pulls.get({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| pull_number: context.payload.pull_request.number | ||||||
| }); | ||||||
|
|
||||||
| const additions = pr.additions; | ||||||
| const deletions = pr.deletions; | ||||||
| const changes = additions + deletions; | ||||||
|
|
||||||
| // Remove existing size labels | ||||||
| const currentLabels = pr.labels.map(l => l.name); | ||||||
| const sizeLabels = ['size/XS', 'size/S', 'size/M', 'size/L', 'size/XL']; | ||||||
| for (const label of sizeLabels) { | ||||||
| if (currentLabels.includes(label)) { | ||||||
| await github.rest.issues.removeLabel({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| issue_number: pr.number, | ||||||
| name: label | ||||||
| }); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Add appropriate size label | ||||||
| let sizeLabel; | ||||||
| if (changes < 10) sizeLabel = 'size/XS'; | ||||||
| else if (changes < 50) sizeLabel = 'size/S'; | ||||||
| else if (changes < 200) sizeLabel = 'size/M'; | ||||||
| else if (changes < 500) sizeLabel = 'size/L'; | ||||||
| else sizeLabel = 'size/XL'; | ||||||
|
|
||||||
| await github.rest.issues.addLabels({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| issue_number: pr.number, | ||||||
| labels: [sizeLabel] | ||||||
| }); | ||||||
|
|
||||||
| // Add branch-specific labels | ||||||
| const targetBranch = pr.base.ref; | ||||||
| const sourceBranch = pr.head.ref; | ||||||
|
|
||||||
| if (targetBranch === 'main') { | ||||||
| await github.rest.issues.addLabels({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| issue_number: pr.number, | ||||||
| labels: ['target/main', 'needs-careful-review'] | ||||||
| }); | ||||||
| } else if (targetBranch === 'dev') { | ||||||
| await github.rest.issues.addLabels({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| issue_number: pr.number, | ||||||
| labels: ['target/dev'] | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| // Add feature type labels based on branch name | ||||||
| if (sourceBranch.startsWith('feat/')) { | ||||||
| await github.rest.issues.addLabels({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| issue_number: pr.number, | ||||||
| labels: ['type/feature'] | ||||||
| }); | ||||||
| } else if (sourceBranch.startsWith('fix/')) { | ||||||
| await github.rest.issues.addLabels({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| issue_number: pr.number, | ||||||
| labels: ['type/bugfix'] | ||||||
| }); | ||||||
| } else if (sourceBranch.startsWith('docs/')) { | ||||||
| await github.rest.issues.addLabels({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| issue_number: pr.number, | ||||||
| labels: ['type/documentation'] | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| pr-feedback: | ||||||
| name: PR Feedback | ||||||
| runs-on: ubuntu-latest | ||||||
| if: github.event_name == 'pull_request' && github.event.action == 'opened' | ||||||
|
|
||||||
| steps: | ||||||
| - name: Post PR Guidelines | ||||||
| uses: actions/github-script@v7 | ||||||
| with: | ||||||
| script: | | ||||||
| const pr = context.payload.pull_request; | ||||||
| const isToMain = pr.base.ref === 'main'; | ||||||
| const isFromDev = pr.head.ref === 'dev'; | ||||||
| const prSize = pr.additions + pr.deletions; | ||||||
|
|
||||||
| let comment = `## 👋 Welcome to the Hybrid Workflow!\n\n`; | ||||||
|
|
||||||
| if (isToMain && !isFromDev) { | ||||||
| comment += `⚠️ **Important**: This PR targets \`main\` directly. Please ensure:\n`; | ||||||
| comment += `- This is a critical fix or thoroughly tested feature\n`; | ||||||
| comment += `- All tests pass\n`; | ||||||
| comment += `- You've considered creating this PR to \`dev\` first\n\n`; | ||||||
| } | ||||||
|
|
||||||
| if (prSize > 500) { | ||||||
| comment += `📏 **Large PR detected** (${prSize} lines changed)\n`; | ||||||
| comment += `Consider breaking this into smaller, focused PRs for easier review.\n\n`; | ||||||
| } | ||||||
|
|
||||||
| comment += `### PR Guidelines\n`; | ||||||
| comment += `- Keep PRs focused on a single feature or fix\n`; | ||||||
| comment += `- Ensure all tests pass before review\n`; | ||||||
| comment += `- Update documentation if needed\n`; | ||||||
| comment += `- Respond to review feedback promptly\n\n`; | ||||||
|
|
||||||
| comment += `### Review Process\n`; | ||||||
| if (isToMain) { | ||||||
| comment += `PRs to \`main\` require:\n`; | ||||||
| comment += `- Approved review from maintainer\n`; | ||||||
| comment += `- All status checks passing\n`; | ||||||
| comment += `- No merge conflicts\n`; | ||||||
| } else { | ||||||
| comment += `PRs to \`dev\` are reviewed more quickly and merged frequently.\n`; | ||||||
| } | ||||||
|
|
||||||
| await github.rest.issues.createComment({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| issue_number: pr.number, | ||||||
| body: comment | ||||||
| }); | ||||||
|
|
||||||
| auto-merge-small-prs: | ||||||
| name: Auto-merge Small PRs to Dev | ||||||
| runs-on: ubuntu-latest | ||||||
| if: | | ||||||
| github.event_name == 'pull_request_review' && | ||||||
| github.event.review.state == 'approved' && | ||||||
| github.event.pull_request.base.ref == 'dev' | ||||||
|
|
||||||
| steps: | ||||||
| - name: Check Auto-merge Eligibility | ||||||
| uses: actions/github-script@v7 | ||||||
| with: | ||||||
| script: | | ||||||
| const pr = context.payload.pull_request; | ||||||
|
|
||||||
| // Check PR size | ||||||
| if (pr.additions + pr.deletions > 200) { | ||||||
| console.log('PR too large for auto-merge'); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| // Check if all checks passed | ||||||
| const { data: checks } = await github.rest.checks.listForRef({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| ref: pr.head.sha | ||||||
| }); | ||||||
|
|
||||||
| const allChecksPassed = checks.check_runs.every( | ||||||
| check => check.status === 'completed' && check.conclusion === 'success' | ||||||
| ); | ||||||
|
|
||||||
| if (!allChecksPassed) { | ||||||
|
||||||
| console.log('Not all checks have passed'); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| // Auto-merge eligible small PRs to dev | ||||||
| try { | ||||||
| await github.rest.pulls.merge({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| pull_number: pr.number, | ||||||
| commit_title: `Auto-merge: ${pr.title}`, | ||||||
| commit_message: pr.body || '', | ||||||
| merge_method: 'squash' | ||||||
| }); | ||||||
|
|
||||||
| console.log(`Successfully auto-merged PR #${pr.number}`); | ||||||
| } catch (error) { | ||||||
| console.log(`Failed to auto-merge: ${error.message}`); | ||||||
|
|
||||||
| // Post comment about why auto-merge failed | ||||||
| await github.rest.issues.createComment({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| issue_number: pr.number, | ||||||
| body: `🤖 Auto-merge was attempted but failed. Please merge manually.\n\nError: ${error.message}` | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| conflict-detection: | ||||||
| name: Detect Merge Conflicts | ||||||
| runs-on: ubuntu-latest | ||||||
| if: github.event_name == 'pull_request' && github.event.action == 'synchronize' | ||||||
|
|
||||||
| steps: | ||||||
| - name: Check for Conflicts | ||||||
| uses: actions/github-script@v7 | ||||||
| with: | ||||||
| script: | | ||||||
| const pr = context.payload.pull_request; | ||||||
|
|
||||||
| // Get PR details with mergeable state | ||||||
| const { data: pullRequest } = await github.rest.pulls.get({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| pull_number: pr.number | ||||||
| }); | ||||||
|
|
||||||
| if (pullRequest.mergeable === false) { | ||||||
| // Post conflict warning | ||||||
| await github.rest.issues.createComment({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| issue_number: pr.number, | ||||||
| body: `⚠️ **Merge conflicts detected!**\n\nThis PR has conflicts with the base branch that must be resolved.\n\n### To resolve:\n\`\`\`bash\ngit checkout ${pr.head.ref}\ngit fetch origin\ngit rebase origin/${pr.base.ref}\n# Fix conflicts\ngit push --force-with-lease\n\`\`\`\n\nNeed help? Check our [conflict resolution guide](../docs/CONFLICT_RESOLUTION.md).` | ||||||
|
||||||
| body: `⚠️ **Merge conflicts detected!**\n\nThis PR has conflicts with the base branch that must be resolved.\n\n### To resolve:\n\`\`\`bash\ngit checkout ${pr.head.ref}\ngit fetch origin\ngit rebase origin/${pr.base.ref}\n# Fix conflicts\ngit push --force-with-lease\n\`\`\`\n\nNeed help? Check our [conflict resolution guide](../docs/CONFLICT_RESOLUTION.md).` | |
| body: `⚠️ **Merge conflicts detected!**\n\nThis PR has conflicts with the base branch that must be resolved.\n\n### To resolve:\n\`\`\`bash\ngit checkout ${pr.head.ref}\ngit fetch origin\ngit rebase origin/${pr.base.ref}\n# Fix conflicts\ngit push --force-with-lease\n\`\`\`\n\nNeed help? Check our [conflict resolution guide](https://github.com/<owner>/<repo>/blob/main/docs/CONFLICT_RESOLUTION.md).` |
Copilot
AI
Jul 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The magic number (1000 * 60 * 60 * 24) for milliseconds in a day should be extracted to a named constant for better readability and maintainability.
| const daysSinceUpdate = (now - updatedAt) / (1000 * 60 * 60 * 24); | |
| const daysSinceUpdate = (now - updatedAt) / MILLISECONDS_IN_A_DAY; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The auto-merge logic only checks
check_runsbut ignores status checks from the legacy status API. This could cause auto-merge to proceed when some status checks haven't passed. Consider also checkinggithub.rest.repos.getCombinedStatusForRef()for complete validation.