Fix/bug audit high medium #10
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: Triage | |
| on: | |
| issues: | |
| types: [opened, reopened] | |
| pull_request_target: | |
| types: [opened, reopened] | |
| permissions: {} | |
| jobs: | |
| pr-path-labels: | |
| if: github.event_name == 'pull_request_target' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # actions/labeler reads the repo's .github/labeler.yml | |
| pull-requests: write # ...and writes labels onto the PR | |
| steps: | |
| - uses: actions/labeler@v5 | |
| with: | |
| sync-labels: false # additive only; never strip manually-set labels | |
| triage: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write # labels/assignee/milestone on issues | |
| pull-requests: write # ...and the same on PRs (pull_request_target) | |
| steps: | |
| - uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // fix -> bug, feat -> enhancement, everything else -> chore/documentation | |
| const TYPE_LABELS = { | |
| feat: "enhancement", fix: "bug", docs: "documentation", | |
| perf: "chore", refactor: "chore", test: "chore", chore: "chore", ci: "chore", | |
| }; | |
| const item = context.payload.issue || context.payload.pull_request; | |
| const issue_number = item.number; | |
| const author = item.user.login; | |
| const repo = context.repo; | |
| // Scopes we recognise; anything else is ignored rather than auto-creating a | |
| // junk label. Each maps to the *existing* repo label(s) to look for first | |
| // (checked case-insensitively, in order) -- e.g. "ui" reuses the repo's | |
| // "UX/UI" label rather than creating a near-duplicate "ui" one. The last | |
| // entry is what gets created if none of the candidates exist yet. Keep in | |
| // sync with .github/labeler.yml. | |
| const AREA_LABEL_CANDIDATES = { | |
| api: ["API", "api"], | |
| ui: ["UX/UI", "ui"], | |
| worker: ["worker"], | |
| checks: ["checks"], | |
| db: ["db"], | |
| ci: ["CI/CD", "ci"], | |
| }; | |
| // Look up the repo's labels once so area labels above (and TYPE_LABELS | |
| // below) can reuse whatever already exists instead of assuming a name. | |
| const existingLabels = await github.paginate(github.rest.issues.listLabelsForRepo, { ...repo, per_page: 100 }); | |
| const labelsByLower = new Map(existingLabels.map((l) => [l.name.toLowerCase(), l.name])); | |
| async function resolveLabel(candidates) { | |
| for (const name of candidates) { | |
| const found = labelsByLower.get(name.toLowerCase()); | |
| if (found) return found; | |
| } | |
| const toCreate = candidates[candidates.length - 1]; | |
| try { | |
| await github.rest.issues.createLabel({ ...repo, name: toCreate, color: "ededed" }); | |
| labelsByLower.set(toCreate.toLowerCase(), toCreate); | |
| } catch (e) { | |
| // Race with another run creating the same label concurrently, or | |
| // missing perms -- addLabels below will warn if it's still missing. | |
| core.warning(`could not create label "${toCreate}": ${e.message}`); | |
| } | |
| return toCreate; | |
| } | |
| // 1. Labels from the "type(scope):" title prefix | |
| const m = (item.title || "").match(/^([a-z]+)(?:\(([^)]+)\))?!?:/i); | |
| const labels = []; | |
| if (m) { | |
| const type = TYPE_LABELS[m[1].toLowerCase()]; | |
| if (type) labels.push(await resolveLabel([type])); | |
| const scope = (m[2] || "").toLowerCase().trim(); | |
| const areaCandidates = AREA_LABEL_CANDIDATES[scope]; | |
| if (areaCandidates) labels.push(await resolveLabel(areaCandidates)); | |
| } | |
| if (labels.length) { | |
| // Don't let a labelling failure abort assignee/milestone below. | |
| try { | |
| await github.rest.issues.addLabels({ ...repo, issue_number, labels }); | |
| } catch (e) { | |
| core.warning(`labels skipped: ${e.message}`); | |
| } | |
| } | |
| // 2. Assign the author (only if nobody is assigned yet) | |
| if (!(item.assignees && item.assignees.length)) { | |
| try { | |
| const res = await github.rest.issues.addAssignees({ ...repo, issue_number, assignees: [author] }); | |
| // GitHub silently drops non-assignable users (non-collaborators) | |
| // and still returns 201, so confirm the author actually landed. | |
| const assigned = (res.data.assignees || []).some((a) => a.login === author); | |
| if (!assigned) { | |
| core.warning(`assignee skipped: ${author} is not assignable on this repo`); | |
| } | |
| } catch (e) { | |
| core.warning(`assignee skipped: ${e.message}`); | |
| } | |
| } | |
| // 3. Milestone = open milestone with the nearest due date (only if none set) | |
| if (!item.milestone) { | |
| const ms = await github.rest.issues.listMilestones({ ...repo, state: "open", per_page: 100 }); | |
| const withDue = ms.data | |
| .filter((x) => x.due_on) | |
| .sort((a, b) => new Date(a.due_on) - new Date(b.due_on)); | |
| const pick = withDue[0] || ms.data[0]; // fallback: any open milestone; undefined -> skip | |
| if (pick) { | |
| await github.rest.issues.update({ ...repo, issue_number, milestone: pick.number }); | |
| } | |
| } |