-
-
Notifications
You must be signed in to change notification settings - Fork 41
feat: add automatic PR labeler workflow #115
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
base: main
Are you sure you want to change the base?
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,170 @@ | ||
| name: PR Labeler | ||
|
|
||
| on: | ||
| pull_request_target: | ||
| types: [opened, reopened, synchronize, edited] | ||
|
|
||
| jobs: | ||
| label-pr: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| issues: write | ||
| pull-requests: write | ||
| steps: | ||
| - name: Apply PR Labels | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| // Helper to parse all issue numbers out of a PR body | ||
| const getLinkedIssues = (body) => { | ||
| const issues = new Set(); | ||
| if (!body) return issues; | ||
|
|
||
| // Matches conventional keywords or just #N (e.g., "Fixes #123", "closes#123", "addresses #123", "#123") | ||
| const regex = /(?:fix(?:e[sd])?|clos(?:e[sd])?|resolv(?:e[sd])?|address(?:es|ed)?)?\s*#(\d+)/gi; | ||
| let match; | ||
| while ((match = regex.exec(body)) !== null) { | ||
| issues.add(match[1]); | ||
| } | ||
| return issues; | ||
| }; | ||
SIDDHANTCOOKIE marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+19
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this step is alredy done in another pr.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shall I move the assigned, unassigned, duplicate logic to sync-pr-labels file then we can roll out the updated workflow together? as having two for pr-labelling will cause api calls for the same thing again either I can pass the data from the sync-pr-labels workflow but again that would be just increasing complexity for no reason so I guess putting it in sync-pr-labels and rolling it out together we can divide equally, will be better? |
||
|
|
||
| const body = context.payload.pull_request.body; | ||
| const issueNumbers = getLinkedIssues(body); | ||
|
|
||
| const labelsEnsured = new Set(); | ||
| async function ensureLabel(name, color, description) { | ||
| if (labelsEnsured.has(name)) return; | ||
| try { | ||
| // Check if label exists | ||
| await github.rest.issues.getLabel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| name: name | ||
| }); | ||
| } catch (error) { | ||
| // If label doesn't exist, create it | ||
| if (error.status === 404) { | ||
| try { | ||
| await github.rest.issues.createLabel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| name: name, | ||
| color: color, | ||
| description: description | ||
| }); | ||
| } catch (createError) { | ||
| if (createError.status !== 422) { | ||
| throw createError; // 422 means already exists/created concurrently | ||
| } | ||
| } | ||
| } else { | ||
| throw error; // Rethrow unexpected getLabel errors | ||
| } | ||
| } | ||
| labelsEnsured.add(name); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Ensure our required labels exist | ||
| await ensureLabel('issue-assigned', '0e8a16', 'PR linked to an assigned issue'); | ||
| await ensureLabel('issue-unassigned', 'd4c5f9', 'PR linked to an unassigned issue'); | ||
| await ensureLabel('no-linked-issue', 'e11d21', 'PR lacks a linked issue'); | ||
| await ensureLabel('potential-duplicate', 'fbca04', 'Another open PR links to the same issue'); | ||
|
|
||
| const labelsToAdd = new Set(); | ||
| const labelsToRemove = new Set(['issue-assigned', 'issue-unassigned', 'no-linked-issue', 'potential-duplicate']); | ||
|
|
||
| if (issueNumbers.size === 0) { | ||
| // Rule 3: Missing Issue | ||
| labelsToAdd.add('no-linked-issue'); | ||
| } else { | ||
| // Rules 2: Assigned vs Unassigned Label | ||
| let isAssigned = false; | ||
| for (const issueNumber of Array.from(issueNumbers)) { | ||
| try { | ||
| const issue = await github.rest.issues.get({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: parseInt(issueNumber, 10) | ||
| }); | ||
| // Ignore if it's actually referencing another PR instead of an Issue | ||
| if (!issue.data.pull_request && issue.data.assignees && issue.data.assignees.length > 0) { | ||
| isAssigned = true; | ||
| } | ||
| } catch (e) { | ||
| console.log(`Could not fetch issue #${issueNumber}: ${e.message}`); | ||
| } | ||
| } | ||
|
|
||
| if (isAssigned) { | ||
| labelsToAdd.add('issue-assigned'); | ||
| } else { | ||
| labelsToAdd.add('issue-unassigned'); | ||
| } | ||
|
|
||
| // Rule 4: Duplicate PR Detection | ||
| let isDuplicate = false; | ||
| const pulls = await github.paginate(github.rest.pulls.list, { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| state: 'open' | ||
| }); | ||
|
|
||
| for (const issueNumber of Array.from(issueNumbers)) { | ||
| const relatedPulls = pulls.filter(pr => { | ||
| const prIssues = getLinkedIssues(pr.body); | ||
| return prIssues.has(issueNumber); | ||
| }); | ||
|
|
||
| if (relatedPulls.length > 1) { | ||
| // Sort chronologically by creation date | ||
| relatedPulls.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); | ||
|
|
||
| // If our current PR is not the earliest | ||
| if (relatedPulls[0].number !== context.payload.pull_request.number) { | ||
| isDuplicate = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (isDuplicate) { | ||
| labelsToAdd.add('potential-duplicate'); | ||
| } | ||
SIDDHANTCOOKIE marked this conversation as resolved.
Show resolved
Hide resolved
SIDDHANTCOOKIE marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Prepare to modify labels on the current PR | ||
| for (const l of Array.from(labelsToAdd)) { | ||
| labelsToRemove.delete(l); | ||
| } | ||
|
|
||
| if (labelsToAdd.size > 0) { | ||
| try { | ||
| await github.rest.issues.addLabels({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.issue.number, | ||
| labels: Array.from(labelsToAdd) | ||
| }); | ||
| } catch (e) { | ||
| console.log(`Failed to add labels: ${e.message}`); | ||
| } | ||
| } | ||
|
|
||
| // Clean up any stale labels | ||
| const currentLabels = context.payload.pull_request.labels.map(l => l.name); | ||
| for (const label of Array.from(labelsToRemove)) { | ||
| if (currentLabels.includes(label)) { | ||
| try { | ||
| await github.rest.issues.removeLabel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.issue.number, | ||
| name: label | ||
| }); | ||
| } catch (e) { | ||
| console.log(`Failed to remove ${label}: ${e.message}`); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.