fix(verify): correct mermaid pie colors (green/amber/red) #68
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-metadata | |
| # Keep PR triage fields useful without requiring manual cleanup on every branch. | |
| # This workflow only updates PR metadata; it never checks out or executes PR code. | |
| on: | |
| pull_request_target: | |
| types: [opened, reopened, synchronize, ready_for_review] | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| triage: | |
| runs-on: ubuntu-latest | |
| env: | |
| PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN || secrets.ENGINE_TOKEN }} | |
| TRIAGE_PROJECT_URLS: ${{ vars.TRIAGE_PROJECT_URLS }} | |
| steps: | |
| - name: Assign author and label by changed paths | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const issue_number = pr.number; | |
| const assignees = ['TechEngineBot']; | |
| if (!pr.user?.type?.endsWith('Bot')) { | |
| assignees.push(pr.user.login); | |
| } | |
| const FILE_SCAN_LIMIT = 500; | |
| const files = []; | |
| const fileIterator = github.paginate.iterator(github.rest.pulls.listFiles, { | |
| owner, | |
| repo, | |
| pull_number: issue_number, | |
| per_page: 100, | |
| }); | |
| for await (const { data } of fileIterator) { | |
| files.push(...data); | |
| if (files.length >= FILE_SCAN_LIMIT) break; | |
| } | |
| const filesWereCapped = (pr.changed_files || files.length) > files.length; | |
| const labels = new Set(); | |
| const title = pr.title.toLowerCase(); | |
| for (const file of files) { | |
| const path = file.filename; | |
| const isDataDump = path.startsWith('site/public/v1/') || path === 'site/public/openapi.json'; | |
| if (path.startsWith('site/') && !isDataDump) { | |
| labels.add('site'); | |
| } | |
| if (path.startsWith('data/') || isDataDump) { | |
| labels.add('data'); | |
| } | |
| if (path.startsWith('app/')) labels.add('app'); | |
| if (path.startsWith('.github/workflows/')) labels.add('ci'); | |
| if (path.startsWith('docs/') || path === 'README.md' || path.endsWith('.md')) labels.add('documentation'); | |
| } | |
| if (title.startsWith('feat') || labels.has('site') || labels.has('data') || labels.has('app')) { | |
| labels.add('enhancement'); | |
| } | |
| if (title.startsWith('fix')) { | |
| labels.add('bug'); | |
| } | |
| await github.rest.issues.addAssignees({ | |
| owner, | |
| repo, | |
| issue_number, | |
| assignees: [...new Set(assignees)], | |
| }); | |
| if (labels.size) { | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number, | |
| labels: [...labels], | |
| }); | |
| } | |
| const milestoneByLabel = new Map([ | |
| ['site', 'Homepage and site improvements'], | |
| ['data', 'Massive dataset rebuild (1989-2026)'], | |
| ]); | |
| for (const [label, title] of milestoneByLabel) { | |
| if (!labels.has(label)) continue; | |
| const milestones = await github.paginate(github.rest.issues.listMilestones, { | |
| owner, | |
| repo, | |
| state: 'open', | |
| per_page: 100, | |
| }); | |
| const milestone = milestones.find((item) => item.title === title); | |
| if (milestone) { | |
| await github.rest.issues.update({ | |
| owner, | |
| repo, | |
| issue_number, | |
| milestone: milestone.number, | |
| }); | |
| } | |
| break; | |
| } | |
| const changedLines = (pr.additions || 0) + (pr.deletions || 0); | |
| let priority = 'Medium'; | |
| if (labels.has('data') && (files.length >= 25 || changedLines >= 1000)) { | |
| priority = 'High'; | |
| } else if (labels.has('data') || labels.has('app') || title.startsWith('fix')) { | |
| priority = 'High'; | |
| } else if (labels.size === 1 && labels.has('documentation')) { | |
| priority = 'Low'; | |
| } else if (changedLines <= 25 && !labels.has('ci')) { | |
| priority = 'Low'; | |
| } | |
| core.exportVariable('TRIAGE_PRIORITY', priority); | |
| const trackingIssuesByLabel = new Map([ | |
| ['data', 1], | |
| ['site', 19], | |
| ]); | |
| const trackingIssues = []; | |
| for (const [label, issueNumber] of trackingIssuesByLabel) { | |
| if (labels.has(label)) trackingIssues.push(issueNumber); | |
| } | |
| if (trackingIssues.length) { | |
| let body = pr.body || ''; | |
| let updatedBody = body; | |
| for (const issueNumber of trackingIssues) { | |
| const closingLine = `Closes #${issueNumber}`; | |
| const closingPattern = new RegExp(`\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#${issueNumber}\\b`, 'i'); | |
| const refsPattern = new RegExp(`\\bRefs\\s+#${issueNumber}\\b`, 'i'); | |
| if (closingPattern.test(updatedBody)) continue; | |
| if (refsPattern.test(updatedBody)) { | |
| updatedBody = updatedBody.replace(refsPattern, closingLine); | |
| } else { | |
| updatedBody = `${updatedBody.trimEnd()}\n\n${closingLine}`; | |
| } | |
| } | |
| if (updatedBody !== body) { | |
| await github.rest.pulls.update({ | |
| owner, | |
| repo, | |
| pull_number: issue_number, | |
| body: updatedBody, | |
| }); | |
| } | |
| const changedByTopLevel = new Map(); | |
| const changedData = new Map([ | |
| ['brand', { added: 0, modified: 0, deleted: 0 }], | |
| ['soc', { added: 0, modified: 0, deleted: 0 }], | |
| ['smartphone', { added: 0, modified: 0, deleted: 0 }], | |
| ['tablet', { added: 0, modified: 0, deleted: 0 }], | |
| ['watch', { added: 0, modified: 0, deleted: 0 }], | |
| ['pda', { added: 0, modified: 0, deleted: 0 }], | |
| ['gpu', { added: 0, modified: 0, deleted: 0 }], | |
| ['cpu', { added: 0, modified: 0, deleted: 0 }], | |
| ]); | |
| for (const file of files) { | |
| const top = file.filename.split('/')[0] || '(root)'; | |
| changedByTopLevel.set(top, (changedByTopLevel.get(top) || 0) + 1); | |
| const parts = file.filename.split('/'); | |
| if (parts[0] === 'data' && changedData.has(parts[1])) { | |
| const bucket = changedData.get(parts[1]); | |
| const status = file.status === 'removed' | |
| ? 'deleted' | |
| : file.status === 'added' | |
| ? 'added' | |
| : 'modified'; | |
| bucket[status] += 1; | |
| } | |
| } | |
| const dataRows = [...changedData.entries()] | |
| .filter(([, counts]) => counts.added || counts.modified || counts.deleted) | |
| .map(([category, counts]) => `| ${category} | ${counts.added} | ${counts.modified} | ${counts.deleted} |`) | |
| .join('\n') || '| none | 0 | 0 | 0 |'; | |
| const topRows = [...changedByTopLevel.entries()] | |
| .sort((a, b) => b[1] - a[1]) | |
| .slice(0, 8) | |
| .map(([area, count]) => `| ${area} | ${count} |`) | |
| .join('\n'); | |
| const labelText = [...labels].sort().map((label) => `\`${label}\``).join(', ') || 'none'; | |
| const trackerComment = [ | |
| `<!-- techapi-tracking-issue-pr-${issue_number} -->`, | |
| `## Linked PR update: ${pr.title} #${issue_number}`, | |
| '', | |
| `- PR: ${pr.html_url}`, | |
| `- Branch: \`${pr.head.ref}\``, | |
| `- Author: @${pr.user.login}`, | |
| `- Labels: ${labelText}`, | |
| `- Priority: ${priority}`, | |
| `- Files changed: ${pr.changed_files || files.length}${filesWereCapped ? ` (sampled ${files.length} for metadata)` : ''}`, | |
| `- Lines changed: +${pr.additions || files.reduce((sum, file) => sum + file.additions, 0)} / -${pr.deletions || files.reduce((sum, file) => sum + file.deletions, 0)}`, | |
| '', | |
| '### Changed Data', | |
| '', | |
| '| Category | Added | Modified | Deleted |', | |
| '| --- | ---: | ---: | ---: |', | |
| dataRows, | |
| '', | |
| '### Changed Areas', | |
| '', | |
| '| Area | Files |', | |
| '| --- | ---: |', | |
| topRows || '| none | 0 |', | |
| '', | |
| '### Notes', | |
| '', | |
| '- This is an automatic tracking comment for the long-running issue.', | |
| ...(filesWereCapped ? ['- Large PR detected: changed-area tables are based on the first sampled files, while total file/line counts come from GitHub PR metadata.'] : []), | |
| '- PR validation details are posted on the PR by TechEngineBot.', | |
| '- The tracker issue remains open even when the PR uses `Closes #...` for GitHub Development linking.', | |
| ].join('\n'); | |
| for (const issueNumber of trackingIssues) { | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| per_page: 100, | |
| }); | |
| const marker = `<!-- techapi-tracking-issue-pr-${issue_number} -->`; | |
| const existing = comments.find((comment) => comment.body?.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| body: trackerComment, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| body: trackerComment, | |
| }); | |
| } | |
| } | |
| } | |
| - name: Add PR to configured projects | |
| if: env.TRIAGE_PROJECT_URLS != '' && env.PROJECT_TOKEN != '' | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ env.PROJECT_TOKEN }} | |
| PR_URL: ${{ github.event.pull_request.html_url }} | |
| shell: bash | |
| run: | | |
| set -uo pipefail | |
| IFS=',' read -ra urls <<< "${TRIAGE_PROJECT_URLS}" | |
| today="$(date -u +%F)" | |
| for url in "${urls[@]}"; do | |
| url="$(echo "$url" | xargs)" | |
| [ -z "$url" ] && continue | |
| if [[ "$url" =~ github.com/orgs/([^/]+)/projects/([0-9]+) ]]; then | |
| owner="${BASH_REMATCH[1]}" | |
| project_number="${BASH_REMATCH[2]}" | |
| elif [[ "$url" =~ github.com/users/([^/]+)/projects/([0-9]+) ]]; then | |
| owner="${BASH_REMATCH[1]}" | |
| project_number="${BASH_REMATCH[2]}" | |
| else | |
| echo "::warning::Unsupported project URL: $url" | |
| continue | |
| fi | |
| project_json="$(gh project view "$project_number" --owner "$owner" --format json)" || { | |
| echo "::warning::Could not read project $owner/$project_number. Skipping project metadata." | |
| continue | |
| } | |
| project_id="$(jq -r '.id' <<< "$project_json")" | |
| item_id="$(gh project item-add "$project_number" --owner "$owner" --url "$PR_URL" --format json --jq '.id')" || { | |
| echo "::warning::Could not add PR to project $owner/$project_number. Skipping project metadata." | |
| continue | |
| } | |
| status_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Status") | .id' || true)" | |
| status_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Status") | .options[] | select(.name == "In Progress") | .id' || true)" | |
| if [ -z "$status_option_id" ]; then | |
| status_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Status") | .options[] | select(.name == "Todo") | .id' || true)" | |
| fi | |
| start_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Start date") | .id' || true)" | |
| target_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Target date") | .id' || true)" | |
| priority_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Priority") | .id' || true)" | |
| priority_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq ".fields[] | select(.name == \"Priority\") | .options[] | select(.name == \"${TRIAGE_PRIORITY:-Medium}\") | .id" || true)" | |
| if [ -z "$priority_option_id" ]; then | |
| priority_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Priority") | .options[] | select(.name == "Medium") | .id' || true)" | |
| fi | |
| if [ -n "$item_id" ] && [ -n "$status_field_id" ] && [ -n "$status_option_id" ]; then | |
| gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$status_field_id" --single-select-option-id "$status_option_id" || echo "::warning::Could not set project status." | |
| fi | |
| if [ -n "$item_id" ] && [ -n "$start_field_id" ]; then | |
| gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$start_field_id" --date "$today" || echo "::warning::Could not set project start date." | |
| fi | |
| if [ -n "$item_id" ] && [ -n "$target_field_id" ]; then | |
| gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$target_field_id" --date "$today" || echo "::warning::Could not set project target date." | |
| fi | |
| if [ -n "$item_id" ] && [ -n "$priority_field_id" ] && [ -n "$priority_option_id" ]; then | |
| gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$priority_field_id" --single-select-option-id "$priority_option_id" || echo "::warning::Could not set project priority." | |
| fi | |
| done |