diff --git a/.github/workflows/pr-size-label.yml b/.github/workflows/pr-size-label.yml new file mode 100644 index 00000000000..a36d2375ed2 --- /dev/null +++ b/.github/workflows/pr-size-label.yml @@ -0,0 +1,147 @@ +name: PR Size Label + +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + workflow_dispatch: + + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + Label: + runs-on: ubuntu-latest + + steps: + - name: Label PR by filtered changed lines + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner + const repo = context.repo.repo + const ignoredFilePatterns = [ + /(?:^|\/)\.cnb\/.+\.ya?ml$/i, + /(?:^|\/)\.gemini\/.+\.ya?ml$/i, + /(?:^|\/)\.gitee\/.+\.ya?ml$/i, + /(?:^|\/)\.github\/.+\.ya?ml$/i, + /(?:^|\/)\.idea\/.+\.xml$/i, + /(?:^|\/)\.editorconfig$/i, + /(?:^|\/)\.gitattributes$/i, + /(?:^|\/)\.gitignore$/i, + /(?:^|\/).+\.kts$/i, + /(?:^|\/).+\.md$/i, + /(?:^|\/)LICENSE(?:_.*)?$/i, + /(?:^|\/)gradlew(?:\.bat|\.ps1)?$/i, + /(?:^|\/)gradle\/wrapper\/gradle-wrapper\.properties$/i, + /(?:^|\/)lang\/.+\.properties$/i, + /(?:^|\/)l10n\/.+\.properties$/i, + /(?:^|\/)checkstyle\.xml$/i, + /(?:^|\/).+\.(?:png|jpe?g|gif|ico|svg|json|ttf|woff2?|webp|avif|jxl|heic|mp4|webm|mp3|ogg|wav|txt|toml)$/i + ] + + async function processPR(issue_number) { + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: issue_number, + per_page: 100, + }) + + const countedFiles = [] + const ignoredFiles = [] + let totalChanges = 0 + + for (const file of files) { + const ignored = ignoredFilePatterns.some(pattern => pattern.test(file.filename)) + if (ignored) { + ignoredFiles.push(file.filename) + continue + } + + const fileChanges = file.changes ?? ((file.additions || 0) + (file.deletions || 0)) + totalChanges += fileChanges + countedFiles.push(`${file.filename} (${fileChanges})`) + } + + let targetLabel + if (totalChanges <= 9) targetLabel = '1+' + else if (totalChanges <= 39) targetLabel = '10+' + else if (totalChanges <= 99) targetLabel = '40+' + else if (totalChanges <= 499) targetLabel = '100+' + else if (totalChanges <= 999) targetLabel = '500+' + else if (totalChanges <= 1999) targetLabel = '1000+' + else if (totalChanges <= 4999) targetLabel = '2000+' + else targetLabel = '5000+' + + const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { + owner, + repo, + issue_number, + per_page: 100, + }) + + const sizeLabels = currentLabels + .map(label => label.name) + .filter(name => /^\d+\+$/.test(name)) + + for (const name of sizeLabels) { + if (name !== targetLabel) { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number, + name, + }).catch(() => {}) + } + } + + if (!currentLabels.some(label => label.name === targetLabel)) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: [targetLabel], + }) + } + + core.info(`Pull request #${issue_number} has ${totalChanges} counted changed lines, applied ${targetLabel}.`) + core.info(`Ignored files: ${ignoredFiles.length ? ignoredFiles.join(', ') : 'none'}`) + core.info(`Counted files: ${countedFiles.length ? countedFiles.join(', ') : 'none'}`) + } + + if (context.eventName === 'workflow_dispatch') { + core.info('Manual trigger detected. Fetching all open pull requests...') + + const openPRs = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: 'open', + per_page: 100, + }) + + const unlabelledPRs = openPRs.filter(pr => { + return !pr.labels.some(label => /^\d+\+$/.test(label.name)) + }) + + core.info(`Found ${openPRs.length} open pull request(s), ${unlabelledPRs.length} of them lack a size label.`) + + for (const pr of unlabelledPRs) { + core.info(`----------------------------------------`) + core.info(`Processing unlabelled pull request #${pr.number}: ${pr.title}`) + await processPR(pr.number) + } + } else { + const pr = context.payload.pull_request + if (!pr) { + core.info('No pull request found in payload, skipping.') + return + } + await processPR(pr.number) + } \ No newline at end of file