Add triage #97
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Sync PR Labels | |
| on: | |
| pull_request: | |
| types: [opened, reopened, synchronize, edited] | |
| pull_request_target: | |
| types: [opened, reopened, synchronize, edited] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| sync-labels: | |
| if: ${{ github.repository_owner == 'AOSSIE-Org' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Get PR details | |
| id: pr-details | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const pr = context.payload.pull_request; | |
| return { | |
| number: pr.number, | |
| body: pr.body || '', | |
| base: pr.base.ref, | |
| head: pr.head.ref | |
| }; | |
| # STEP 1: Issue-based labels | |
| - name: Extract linked issue number | |
| id: extract-issue | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const prBody = context.payload.pull_request.body || ''; | |
| // Match patterns: Fixes #123, Closes #123, Resolves #123, etc. | |
| const issuePatterns = [ | |
| /(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\s+#(\d+)/gi, | |
| /#(\d+)/g | |
| ]; | |
| let issueNumber = null; | |
| for (const pattern of issuePatterns) { | |
| const match = prBody.match(pattern); | |
| if (match) { | |
| const numbers = match.map(m => m.match(/\d+/)[0]); | |
| issueNumber = numbers[0]; | |
| break; | |
| } | |
| } | |
| core.setOutput('issue_number', issueNumber || ''); | |
| return issueNumber; | |
| - name: Apply issue-based labels | |
| if: steps.extract-issue.outputs.issue_number != '' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const issueNumber = '${{ steps.extract-issue.outputs.issue_number }}'; | |
| const prNumber = context.payload.pull_request.number; | |
| try { | |
| // Fetch issue labels | |
| const issue = await github.rest.issues.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: parseInt(issueNumber) | |
| }); | |
| const issueLabels = issue.data.labels.map(label => | |
| typeof label === 'string' ? label : label.name | |
| ); | |
| if (issueLabels.length > 0) { | |
| console.log(`Applying issue-based labels: ${issueLabels.join(', ')}`); | |
| // Add labels from issue | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: issueLabels | |
| }); | |
| } | |
| } catch (error) { | |
| console.log(`Error fetching issue #${issueNumber}: ${error.message}`); | |
| } | |
| - name: Mark no issue linked | |
| if: steps.extract-issue.outputs.issue_number == '' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const prNumber = context.payload.pull_request.number; | |
| console.log('No issue linked to this PR'); | |
| // Add "no-issue-linked" label | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: ['no-issue-linked'] | |
| }); | |
| # STEP 2: File-based labels | |
| - name: Get changed files | |
| id: changed-files | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const prNumber = context.payload.pull_request.number; | |
| // Get list of files changed in the PR | |
| const files = await github.rest.pulls.listFiles({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| const changedFiles = files.data.map(file => file.filename); | |
| core.setOutput('files', JSON.stringify(changedFiles)); | |
| return changedFiles; | |
| - name: Apply file-based labels | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const prNumber = context.payload.pull_request.number; | |
| const changedFiles = JSON.parse('${{ steps.changed-files.outputs.files }}'); | |
| const fileLabels = []; | |
| // Define file-based label mappings | |
| const labelMappings = { | |
| 'documentation': ['.md', 'README', 'CONTRIBUTING', 'LICENSE', '.txt'], | |
| 'frontend': ['.html', '.css', '.scss', '.jsx', '.tsx', '.vue'], | |
| 'backend': ['.py', '.java', '.go', '.rb', '.php', '.rs'], | |
| 'javascript': ['.js', '.ts', '.jsx', '.tsx'], | |
| 'python': ['.py'], | |
| 'configuration': ['.yml', '.yaml', '.json', '.toml', '.ini', '.env', '.config'], | |
| 'github-actions': ['.github/workflows/'], | |
| 'dependencies': ['package.json', 'requirements.txt', 'Gemfile', 'Cargo.toml', 'go.mod', 'pom.xml'], | |
| 'tests': ['test/', '__tests__/', '.test.', '.spec.', '_test.'], | |
| 'docker': ['Dockerfile', 'docker-compose', '.dockerignore'], | |
| 'ci-cd': ['.github/', '.gitlab-ci', 'Jenkinsfile', '.circleci'] | |
| }; | |
| // Check each file against label mappings | |
| for (const file of changedFiles) { | |
| for (const [label, patterns] of Object.entries(labelMappings)) { | |
| for (const pattern of patterns) { | |
| if (file.includes(pattern) || file.endsWith(pattern)) { | |
| if (!fileLabels.includes(label)) { | |
| fileLabels.push(label); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if (fileLabels.length > 0) { | |
| console.log(`Applying file-based labels: ${fileLabels.join(', ')}`); | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: fileLabels | |
| }); | |
| } else { | |
| console.log('No file-based labels matched'); | |
| } | |
| # STEP 3: PR size labels | |
| - name: Apply PR size label | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const prNumber = context.payload.pull_request.number; | |
| // Get PR details to calculate size | |
| const pr = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| const additions = pr.data.additions; | |
| const deletions = pr.data.deletions; | |
| const totalChanges = additions + deletions; | |
| console.log(`PR has ${additions} additions and ${deletions} deletions (${totalChanges} total changes)`); | |
| // Determine size label based on total changes | |
| let sizeLabel = ''; | |
| if (totalChanges <= 10) { | |
| sizeLabel = 'size/XS'; | |
| } else if (totalChanges <= 50) { | |
| sizeLabel = 'size/S'; | |
| } else if (totalChanges <= 200) { | |
| sizeLabel = 'size/M'; | |
| } else if (totalChanges <= 500) { | |
| sizeLabel = 'size/L'; | |
| } else { | |
| sizeLabel = 'size/XL'; | |
| } | |
| console.log(`Applying size label: ${sizeLabel}`); | |
| // Remove any existing size labels first | |
| const currentLabels = await github.rest.issues.listLabelsOnIssue({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber | |
| }); | |
| const sizeLabelsToRemove = currentLabels.data | |
| .map(label => label.name) | |
| .filter(name => name.startsWith('size/')); | |
| for (const label of sizeLabelsToRemove) { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| name: label | |
| }); | |
| } | |
| // Apply the new size label | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: [sizeLabel] | |
| }); | |
| # STEP 4: Contributor-based labels | |
| - name: Apply contributor-based labels | |
| uses: actions/github-script@v7 | |
| env: | |
| LABELLER_TOKEN: ${{ secrets.EXTERNAL_LABELLER_TOKEN || secrets.GITHUB_TOKEN }} | |
| with: | |
| github-token: ${{ env.LABELLER_TOKEN }} | |
| script: | | |
| const prNumber = context.payload.pull_request.number; | |
| const prAuthor = context.payload.pull_request.user.login; | |
| try { | |
| // Check if user is a first-time contributor | |
| const commits = await github.rest.repos.listCommits({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| author: prAuthor | |
| }); | |
| const contributorLabels = []; | |
| // Check if contributor is a member of the organization | |
| try { | |
| await github.rest.orgs.checkMembershipForUser({ | |
| org: context.repo.owner, | |
| username: prAuthor | |
| }); | |
| contributorLabels.push('member'); | |
| } catch (error) { | |
| // Not a member | |
| if (commits.data.length <= 1) { | |
| contributorLabels.push('first-time-contributor'); | |
| } else { | |
| contributorLabels.push('external-contributor'); | |
| } | |
| } | |
| // Check if PR author is a collaborator | |
| try { | |
| const permissionLevel = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: prAuthor | |
| }); | |
| if (permissionLevel.data.permission === 'admin' || permissionLevel.data.permission === 'maintain') { | |
| contributorLabels.push('maintainer'); | |
| } | |
| } catch (error) { | |
| console.log('Could not check collaborator status'); | |
| } | |
| if (contributorLabels.length > 0) { | |
| console.log(`Applying contributor-based labels: ${contributorLabels.join(', ')}`); | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: contributorLabels | |
| }); | |
| } | |
| } catch (error) { | |
| console.log(`Error applying contributor labels: ${error.message}`); | |
| } | |
| # Summary step | |
| - name: Label sync summary | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const prNumber = context.payload.pull_request.number; | |
| // Get current labels on PR | |
| const pr = await github.rest.issues.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber | |
| }); | |
| const currentLabels = pr.data.labels.map(label => label.name); | |
| console.log('='.repeat(50)); | |
| console.log('PR Label Sync Complete'); | |
| console.log('='.repeat(50)); | |
| console.log(`Current labels on PR #${prNumber}:`); | |
| console.log(currentLabels.join(', ') || 'No labels'); | |
| console.log('='.repeat(50)); |