Un to dortmund #125
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: PR Automation | |
| on: | |
| pull_request: | |
| types: [opened, reopened, ready_for_review, synchronize, review_request_removed] | |
| permissions: | |
| pull-requests: write | |
| issues: write | |
| contents: read | |
| jobs: | |
| auto-label-and-assign: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get changed files | |
| id: files | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const response = await github.rest.pulls.listFiles({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| }); | |
| const files = response.data || []; | |
| const labels = new Set(); | |
| files.forEach(fileObj => { | |
| const file = fileObj.filename || ''; | |
| if (file.match(/\.(ts|tsx|jsx|js)$/)) labels.add('typescript'); | |
| if (file.match(/\.(css|scss|postcss)$/)) labels.add('styling'); | |
| if (file.match(/test|spec/)) labels.add('tests'); | |
| if (file.match(/package\.json|tsconfig|eslint|tailwind/)) labels.add('configuration'); | |
| if (file.match(/README|\.md$/)) labels.add('documentation'); | |
| if (file.match(/next\.config|public/)) labels.add('build'); | |
| }); | |
| core.setOutput('labels', Array.from(labels).join(',')); | |
| - name: Enforce reviewers config edit policy | |
| uses: actions/github-script@v7 | |
| env: | |
| MAINTAINER_ALLOWLIST: ${{ vars.MAINTAINER_ALLOWLIST }} | |
| with: | |
| script: | | |
| const allowlist = (process.env.MAINTAINER_ALLOWLIST || '') | |
| .split(',') | |
| .map(v => v.trim().toLowerCase()) | |
| .filter(Boolean); | |
| const actor = context.actor.toLowerCase(); | |
| const owner = context.repo.owner.toLowerCase(); | |
| const isMaintainer = allowlist.includes(actor) || actor === owner; | |
| if (isMaintainer) return; | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| per_page: 100 | |
| }); | |
| const changed = files.map(f => f.filename); | |
| if (changed.includes('.github/reviewers.json')) { | |
| core.setFailed('Only maintainers may modify .github/reviewers.json'); | |
| } | |
| - name: Auto-label PR | |
| if: steps.files.outputs.labels != '' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const labels = '${{ steps.files.outputs.labels }}'.split(',').filter(l => l); | |
| if (labels.length > 0) { | |
| github.rest.issues.addLabels({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| labels: labels | |
| }); | |
| } | |
| - name: Auto-assign PR to author | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| github.rest.issues.addAssignees({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| assignees: [context.payload.pull_request.user.login] | |
| }).catch(err => { | |
| console.log('Could not assign author: ' + err.message); | |
| }) | |
| - name: Auto-request reviewers from JSON config | |
| uses: actions/github-script@v7 | |
| env: | |
| MAINTAINER_ALLOWLIST: ${{ vars.MAINTAINER_ALLOWLIST }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| function toRegex(glob) { | |
| const escaped = glob | |
| .replace(/[.+^${}()|[\]\\]/g, '\\$&') | |
| .replace(/\*\*/g, '::DOUBLE_STAR::') | |
| .replace(/\*/g, '[^/]*') | |
| .replace(/::DOUBLE_STAR::/g, '.*'); | |
| return new RegExp(`^${escaped}$`); | |
| } | |
| const configPath = '.github/reviewers.json'; | |
| if (!fs.existsSync(configPath)) { | |
| core.info('No .github/reviewers.json found. Skipping reviewer requests.'); | |
| return; | |
| } | |
| let config; | |
| try { | |
| config = JSON.parse(fs.readFileSync(configPath, 'utf8')); | |
| } catch (e) { | |
| core.setFailed(`Invalid JSON in ${configPath}: ${e.message}`); | |
| return; | |
| } | |
| const defaultReviewers = Array.isArray(config.default_reviewers) ? config.default_reviewers : []; | |
| const pathRules = Array.isArray(config.path_reviewers) ? config.path_reviewers : []; | |
| const allowlist = (process.env.MAINTAINER_ALLOWLIST || '') | |
| .split(',') | |
| .map(v => v.trim().toLowerCase()) | |
| .filter(Boolean); | |
| const actor = context.actor.toLowerCase(); | |
| const ownerLower = context.repo.owner.toLowerCase(); | |
| const actorIsMaintainer = allowlist.includes(actor) || actor === ownerLower; | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| per_page: 100 | |
| }); | |
| const changed = files.map(f => f.filename); | |
| const collected = new Set(defaultReviewers.filter(Boolean)); | |
| for (const rule of pathRules) { | |
| if (!rule || !rule.glob || !Array.isArray(rule.reviewers)) continue; | |
| const re = toRegex(String(rule.glob)); | |
| if (changed.some(f => re.test(f))) { | |
| for (const reviewer of rule.reviewers) { | |
| if (reviewer) collected.add(reviewer); | |
| } | |
| } | |
| } | |
| const pr = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number | |
| }); | |
| const author = pr.data.user?.login; | |
| const alreadyRequested = new Set((pr.data.requested_reviewers || []).map(r => r.login)); | |
| const reviewers = Array.from(collected) | |
| .map(r => String(r).trim()) | |
| .filter(Boolean) | |
| .filter(r => r !== author) | |
| .filter(r => !alreadyRequested.has(r)) | |
| .slice(0, 15); | |
| const action = context.payload.action; | |
| const shouldEnforce = ['opened', 'reopened', 'ready_for_review', 'review_request_removed'].includes(action); | |
| if (!shouldEnforce) { | |
| core.info(`Skipping reviewer enforcement on action: ${action}`); | |
| return; | |
| } | |
| if (action === 'review_request_removed' && actorIsMaintainer) { | |
| core.info('Maintainer removed reviewer; skipping re-request.'); | |
| return; | |
| } | |
| if (reviewers.length === 0) { | |
| core.info('No new reviewers to request.'); | |
| return; | |
| } | |
| try { | |
| await github.rest.pulls.requestReviewers({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| reviewers | |
| }); | |
| core.info(`Requested reviewers: ${reviewers.join(', ')}`); | |
| } catch (e) { | |
| core.warning(`Could not request reviewers: ${e.message}`); | |
| } |