chore(deps): bump the production-dependencies group across 1 directory with 13 updates #60
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 Triage | |
| on: | |
| pull_request_target: | |
| types: | |
| - opened | |
| - synchronize | |
| - reopened | |
| - ready_for_review | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| label: | |
| name: Label PR | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Apply labels | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const labels = new Set(); | |
| async function tryWrite(description, fn) { | |
| try { | |
| return await fn(); | |
| } catch (error) { | |
| if (error.status === 403 || error.status === 422) { | |
| core.warning(`${description} skipped: ${error.message}`); | |
| return null; | |
| } | |
| throw error; | |
| } | |
| } | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pr.number, | |
| per_page: 100, | |
| }); | |
| const linesChanged = files.reduce((sum, file) => sum + file.additions + file.deletions, 0); | |
| const sizeLabels = ["size/XS", "size/S", "size/M", "size/L", "size/XL"]; | |
| const sizeLabel = linesChanged < 10 | |
| ? "size/XS" | |
| : linesChanged < 50 | |
| ? "size/S" | |
| : linesChanged < 200 | |
| ? "size/M" | |
| : linesChanged < 500 | |
| ? "size/L" | |
| : "size/XL"; | |
| labels.add(sizeLabel); | |
| if (["dependabot[bot]", "renovate[bot]"].includes(pr.user.login)) { | |
| labels.add("bot"); | |
| } | |
| const areaMap = { | |
| "area/catalog": (file) => file.startsWith("src/catalog/") || file.includes("CATALOG_ONLY_PLAN"), | |
| "area/cli": (file) => file === "src/cli.ts" || file.startsWith("src/cli/"), | |
| "area/config": (file) => file.startsWith("src/config/"), | |
| "area/content": (file) => file.startsWith("src/content/"), | |
| "area/docs": (file) => file.startsWith("docs/") || file === "README.md" || file === "CHANGELOG.md", | |
| "area/github": (file) => file.startsWith(".github/"), | |
| "area/i18n": (file) => file.startsWith("src/i18n/"), | |
| "area/parsing": (file) => file.startsWith("src/parsing/"), | |
| "area/routing": (file) => file.startsWith("src/routing/"), | |
| "area/runtime": (file) => file.startsWith("src/runtime/") || file.startsWith("src/react/"), | |
| "area/storage": (file) => file.startsWith("src/storage/"), | |
| "area/tests": (file) => file.startsWith("tests/"), | |
| "area/translation": (file) => file.startsWith("src/translation/") || file.startsWith("src/glossary/"), | |
| }; | |
| for (const file of files) { | |
| for (const [label, matches] of Object.entries(areaMap)) { | |
| if (matches(file.filename)) labels.add(label); | |
| } | |
| } | |
| try { | |
| const { data: fullPr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pr.number, | |
| }); | |
| if (fullPr.mergeable === false) labels.add("needs-rebase"); | |
| } catch { | |
| // GitHub may not have computed mergeability yet. | |
| } | |
| const labelMetadata = { | |
| "bot": { color: "ededed", description: "Opened by automation" }, | |
| "needs-rebase": { color: "d93f0b", description: "PR has merge conflicts" }, | |
| "size/XS": { color: "3cbf00", description: "Fewer than 10 changed lines" }, | |
| "size/S": { color: "5dba3f", description: "10-49 changed lines" }, | |
| "size/M": { color: "fbca04", description: "50-199 changed lines" }, | |
| "size/L": { color: "ee9b00", description: "200-499 changed lines" }, | |
| "size/XL": { color: "d93f0b", description: "500+ changed lines" }, | |
| "area/catalog": { color: "5319e7", description: "Catalog-only runtime and integration" }, | |
| "area/cli": { color: "1d76db", description: "CLI" }, | |
| "area/config": { color: "006b75", description: "Configuration schema and options" }, | |
| "area/content": { color: "0e8a16", description: "Content collection wiring" }, | |
| "area/docs": { color: "0075ca", description: "Documentation" }, | |
| "area/github": { color: "000000", description: "GitHub workflows and repo management" }, | |
| "area/i18n": { color: "bfd4f2", description: "UI strings and locale catalogs" }, | |
| "area/parsing": { color: "c2e0c6", description: "File parsing and adapters" }, | |
| "area/routing": { color: "f9d0c4", description: "Route shims and URL rewriting" }, | |
| "area/runtime": { color: "d4c5f9", description: "Runtime APIs and middleware" }, | |
| "area/storage": { color: "fef2c0", description: "R2 cache and local storage" }, | |
| "area/tests": { color: "c5def5", description: "Tests and fixtures" }, | |
| "area/translation": { color: "fef2c0", description: "Translation pipeline and providers" }, | |
| }; | |
| const existingLabels = new Set(); | |
| for await (const response of github.paginate.iterator( | |
| github.rest.issues.listLabelsForRepo, | |
| { owner: context.repo.owner, repo: context.repo.repo, per_page: 100 }, | |
| )) { | |
| for (const label of response.data) existingLabels.add(label.name); | |
| } | |
| for (const [name, meta] of Object.entries(labelMetadata)) { | |
| if (!existingLabels.has(name)) { | |
| await tryWrite(`Create label ${name}`, () => | |
| github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name, | |
| color: meta.color, | |
| description: meta.description, | |
| }), | |
| ); | |
| } | |
| } | |
| const currentLabels = new Set((pr.labels || []).map((label) => label.name)); | |
| const managedLabels = new Set([ | |
| "bot", | |
| "needs-rebase", | |
| ...sizeLabels, | |
| ...Object.keys(areaMap), | |
| ]); | |
| for (const label of managedLabels) { | |
| if (!labels.has(label) && currentLabels.has(label)) { | |
| try { | |
| await tryWrite(`Remove label ${label}`, () => | |
| 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 toAdd = [...labels].filter((label) => !currentLabels.has(label)); | |
| if (toAdd.length > 0) { | |
| await tryWrite(`Add labels ${toAdd.join(", ")}`, () => | |
| github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| labels: toAdd, | |
| }), | |
| ); | |
| } | |
| if (context.payload.action === "opened" && (linesChanged > 500 || files.length > 20)) { | |
| await tryWrite("Create scope guard comment", () => | |
| github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: [ | |
| "<!-- polystella-scope-guard -->", | |
| "## Scope check", | |
| "", | |
| `This PR changes **${linesChanged.toLocaleString()} lines** across **${files.length} files**. PolyStella is maintained by a small team, so smaller focused PRs are more likely to be reviewed.`, | |
| "", | |
| "If this scope is intentional, no action is required.", | |
| ].join("\n"), | |
| }), | |
| ); | |
| } |