feat(data): add 52 AMD Ryzen CPUs from Wikipedia family tables #344
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 | ||
|
Check warning on line 1 in .github/workflows/pr-metadata.yml
|
||
| # 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, | ||
| }); | ||
| // GitHub cannot produce the file list for very large PRs (dump | ||
| // refreshes): 422 "diff is taking too long to generate", or a plain | ||
| // 504 when it gives up later. Label from whatever arrived. | ||
| const NO_FILE_LIST = new Set([422, 502, 503, 504]); | ||
| try { | ||
| for await (const { data } of fileIterator) { | ||
| files.push(...data); | ||
| if (files.length >= FILE_SCAN_LIMIT) break; | ||
| } | ||
| } catch (error) { | ||
| if (!NO_FILE_LIST.has(error.status)) throw error; | ||
| core.warning(`PR file list unavailable (${error.message}); path labels skipped`); | ||
| } | ||
| 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); | ||
| // Workstream trackers (2026-09-27): #1 roadmap/releases, #295 new | ||
| // records, #296 data accuracy, #297 engine/bot/automation, #19 site. | ||
| const TRACKERS = { roadmap: 1, collection: 295, accuracy: 296, automation: 297, site: 19 }; | ||
| const onlyDump = files.length > 0 && files.every((file) => | ||
| file.filename.startsWith('site/public/v1/') || file.filename === 'site/public/openapi.json'); | ||
| const trackingIssues = []; | ||
| if (pr.base.ref === 'main' || title.startsWith('chore(dump') || onlyDump) { | ||
| trackingIssues.push(TRACKERS.roadmap); | ||
| } else { | ||
| if (labels.has('data')) { | ||
| trackingIssues.push(labels.has('bug') || title.startsWith('fix') | ||
| ? TRACKERS.accuracy : TRACKERS.collection); | ||
| } | ||
| if (labels.has('site')) trackingIssues.push(TRACKERS.site); | ||
| if (!trackingIssues.length && (labels.has('ci') || labels.has('app'))) { | ||
| trackingIssues.push(TRACKERS.automation); | ||
| } | ||
| } | ||
| // Respect a tracker the author already linked by hand. | ||
| const anyTracker = new RegExp( | ||
| `\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#(?:${Object.values(TRACKERS).join('|')})\\b`, 'i'); | ||
| if (anyTracker.test(pr.body || '')) trackingIssues.length = 0; | ||
| 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 }], | ||
| ['laptop', { added: 0, modified: 0, deleted: 0 }], | ||
| ['monitor', { added: 0, modified: 0, deleted: 0 }], | ||
| ['software', { added: 0, modified: 0, deleted: 0 }], | ||
| ['website', { 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'); | ||
| // One tracker comment per release, not per PR (2026-09-27): regular PRs | ||
| // show up through the Closes link in the tracker's Development panel. | ||
| const commentTargets = pr.base.ref === 'main' ? trackingIssues : []; | ||
| for (const issueNumber of commentTargets) { | ||
| 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 | ||