Feat/home page #467
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_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: | |
| # STEP 0: Ensure all labels exist with correct colors | |
| - name: Upsert colored labels | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const labels = [ | |
| { name: 'documentation', color: '0075ca', description: 'Documentation updates' }, | |
| { name: 'enhancement', color: 'a2eeef', description: 'New feature or request' }, | |
| { name: 'frontend', color: '7057ff', description: 'Frontend changes' }, | |
| { name: 'javascript', color: 'f0e040', description: 'JavaScript/TypeScript changes' }, | |
| { name: 'dependencies', color: 'e4e669', description: 'Dependency updates' }, | |
| { name: 'configuration', color: 'f9d0c4', description: 'Config file changes' }, | |
| { name: 'good first issue', color: '7cfc00', description: 'Good for newcomers' }, | |
| { name: 'first-time-contributor', color: 'ff9500', description: 'First time contributor' }, | |
| { name: 'no-issue-linked', color: 'd93f0b', description: 'PR has no linked issue' }, | |
| { name: 'size/XS', color: '3cbf00', description: '1-10 lines changed' }, | |
| { name: 'size/S', color: '5d9801', description: '11-50 lines changed' }, | |
| { name: 'size/M', color: 'ffd700', description: '51-200 lines changed' }, | |
| { name: 'size/L', color: 'ff8c00', description: '201-500 lines changed' }, | |
| { name: 'size/XL', color: 'e11d48', description: '500+ lines changed' }, | |
| { name: 'ci-cd', color: '00c0ef', description: 'CI/CD changes' }, | |
| { name: 'github-actions', color: '0052cc', description: 'GitHub Actions changes' }, | |
| { name: 'backend', color: 'c5def5', description: 'Backend changes' }, | |
| { name: 'python', color: 'ffe066', description: 'Python changes' }, | |
| { name: 'tests', color: 'bfd4f2', description: 'Test changes' }, | |
| { name: 'docker', color: '0db7ed', description: 'Docker changes' }, | |
| { name: 'member', color: '006b75', description: 'Org member' }, | |
| { name: 'external-contributor', color: 'e6e6e6', description: 'External contributor' }, | |
| { name: 'maintainer', color: 'b60205', description: 'Maintainer' }, | |
| ]; | |
| for (const label of labels) { | |
| try { | |
| await github.rest.issues.updateLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label.name, | |
| color: label.color, | |
| description: label.description | |
| }); | |
| } catch (e) { | |
| if (e.status === 404) { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label.name, | |
| color: label.color, | |
| description: label.description | |
| }); | |
| } else { | |
| throw e; | |
| } | |
| } | |
| } | |
| # STEP 1: Issue-based labels | |
| - 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 | |
| }; | |
| - 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 { | |
| 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(', ')}`); | |
| 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'); | |
| 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; | |
| 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 = []; | |
| 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'] | |
| }; | |
| 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; | |
| 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)`); | |
| 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}`); | |
| 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 | |
| }); | |
| } | |
| 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 { | |
| const commits = await github.rest.repos.listCommits({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| author: prAuthor | |
| }); | |
| const contributorLabels = []; | |
| try { | |
| await github.rest.orgs.checkMembershipForUser({ | |
| org: context.repo.owner, | |
| username: prAuthor | |
| }); | |
| contributorLabels.push('member'); | |
| } catch (error) { | |
| if (commits.data.length <= 1) { | |
| contributorLabels.push('first-time-contributor'); | |
| } else { | |
| contributorLabels.push('external-contributor'); | |
| } | |
| } | |
| 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; | |
| 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)); |