Repository navigation
feat(platform): use Fetchable server entries for analog({ server }) #2191
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: pr-scope-governance | ||
|
Check warning on line 1 in .github/workflows/pr-scope-governance.yml
|
||
| # pull_request_target so the GITHUB_TOKEN keeps write permissions on fork PRs. | ||
| # Safe because the job only checks out the trusted base sha and never runs PR | ||
| # code — it reads changed filenames from the API and applies labels/comments. | ||
| on: | ||
| pull_request_target: | ||
| types: [opened, reopened, edited, synchronize, ready_for_review] | ||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| pull-requests: write | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: true | ||
| jobs: | ||
| apply-scope-metadata: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event.pull_request.base.sha }} | ||
| - uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const fs = require('fs'); | ||
| const marker = '<!-- pr-scope-governance -->'; | ||
| const config = JSON.parse( | ||
| fs.readFileSync('.github/pr-scope-map.json', 'utf8') | ||
| ); | ||
| const pr = context.payload.pull_request; | ||
| const changedFiles = await github.paginate( | ||
| github.rest.pulls.listFiles, | ||
| { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: pr.number, | ||
| per_page: 100, | ||
| } | ||
| ); | ||
| const filenames = changedFiles.map((file) => file.filename); | ||
| const matches = config.areas.filter((area) => { | ||
| const prefixes = area.prefixes ?? []; | ||
| const files = area.files ?? []; | ||
| return filenames.some((filename) => { | ||
| return ( | ||
| prefixes.some((prefix) => filename.startsWith(prefix)) || | ||
| files.includes(filename) | ||
| ); | ||
| }); | ||
| }); | ||
| const matchedLabels = [...new Set(matches.map((area) => area.label))]; | ||
| const managedLabels = config.areas.map((area) => area.label); | ||
| for (const area of config.areas) { | ||
| try { | ||
| await github.rest.issues.getLabel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| name: area.label, | ||
| }); | ||
| } catch (error) { | ||
| if (error.status !== 404) { | ||
| throw error; | ||
| } | ||
| await github.rest.issues.createLabel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| name: area.label, | ||
| color: area.color, | ||
| description: area.description, | ||
| }); | ||
| } | ||
| } | ||
| const issueLabels = await github.paginate( | ||
| github.rest.issues.listLabelsOnIssue, | ||
| { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: pr.number, | ||
| per_page: 100, | ||
| } | ||
| ); | ||
| const currentManaged = issueLabels | ||
| .map((label) => label.name) | ||
| .filter((label) => managedLabels.includes(label)); | ||
| const labelsToAdd = matchedLabels.filter( | ||
| (label) => !currentManaged.includes(label) | ||
| ); | ||
| const labelsToRemove = currentManaged.filter( | ||
| (label) => !matchedLabels.includes(label) | ||
| ); | ||
| if (labelsToAdd.length > 0) { | ||
| await github.rest.issues.addLabels({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: pr.number, | ||
| labels: labelsToAdd, | ||
| }); | ||
| } | ||
| for (const label of labelsToRemove) { | ||
| try { | ||
| await github.rest.issues.removeLabel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: pr.number, | ||
| name: label, | ||
| }); | ||
| } catch (error) { | ||
| if (error.status !== 404) { | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
| const packageMatches = matches.filter( | ||
| (area) => area.kind === 'package' | ||
| ); | ||
| const comments = await github.paginate( | ||
| github.rest.issues.listComments, | ||
| { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: pr.number, | ||
| per_page: 100, | ||
| } | ||
| ); | ||
| const existingComment = comments.find( | ||
| (comment) => | ||
| comment.user?.type === 'Bot' && | ||
| comment.body?.includes(marker) | ||
| ); | ||
| if (packageMatches.length > 1) { | ||
| const scopes = packageMatches | ||
| .map((area) => `\`${area.scope}\``) | ||
| .join(', '); | ||
| const body = [ | ||
| marker, | ||
| `This PR touches multiple package scopes: ${scopes}.`, | ||
| '', | ||
| 'Please confirm the changes are closely related. `Squash merge` is highly preferred. If you recommend a non-squash merge, add a brief note explaining why the commit boundaries matter and why this PR should bypass focused changes per package.', | ||
| ].join('\n'); | ||
| if (existingComment) { | ||
| await github.rest.issues.updateComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: existingComment.id, | ||
| body, | ||
| }); | ||
| } else { | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: pr.number, | ||
| body, | ||
| }); | ||
| } | ||
| } else if (existingComment) { | ||
| await github.rest.issues.deleteComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: existingComment.id, | ||
| }); | ||
| } | ||
| core.info(`Changed files: ${filenames.join(', ')}`); | ||
| core.info(`Matched labels: ${matchedLabels.join(', ') || 'none'}`); | ||