Skip to content

data(mobile): import smartdevices tablet and watch records #39

data(mobile): import smartdevices tablet and watch records

data(mobile): import smartdevices tablet and watch records #39

Workflow file for this run

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