Skip to content

Commit 0b0e944

Browse files
committed
ci: auto-fill PR metadata
1 parent 7682e13 commit 0b0e944

1 file changed

Lines changed: 180 additions & 0 deletions

File tree

.github/workflows/pr-metadata.yml

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
name: pr-metadata
2+
3+
# Keep PR triage fields useful without requiring manual cleanup on every branch.
4+
# This workflow only updates PR metadata; it never checks out or executes PR code.
5+
on:
6+
pull_request_target:
7+
types: [opened, reopened, synchronize, ready_for_review]
8+
9+
permissions:
10+
contents: read
11+
issues: write
12+
pull-requests: write
13+
14+
jobs:
15+
triage:
16+
runs-on: ubuntu-latest
17+
env:
18+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN || secrets.ENGINE_TOKEN }}
19+
TRIAGE_PROJECT_URLS: ${{ vars.TRIAGE_PROJECT_URLS }}
20+
steps:
21+
- name: Assign author and label by changed paths
22+
uses: actions/github-script@v7
23+
with:
24+
script: |
25+
const pr = context.payload.pull_request;
26+
const owner = context.repo.owner;
27+
const repo = context.repo.repo;
28+
const issue_number = pr.number;
29+
30+
const assignees = ['TechEngineBot'];
31+
if (!pr.user?.type?.endsWith('Bot')) {
32+
assignees.push(pr.user.login);
33+
}
34+
35+
const files = await github.paginate(github.rest.pulls.listFiles, {
36+
owner,
37+
repo,
38+
pull_number: issue_number,
39+
per_page: 100,
40+
});
41+
42+
const labels = new Set();
43+
const trackers = new Set();
44+
const title = pr.title.toLowerCase();
45+
for (const file of files) {
46+
const path = file.filename;
47+
const isDataDump = path.startsWith('site/public/v1/') || path === 'site/public/openapi.json';
48+
if (path.startsWith('site/') && !isDataDump) {
49+
labels.add('site');
50+
trackers.add('#19');
51+
}
52+
if (path.startsWith('data/') || isDataDump) {
53+
labels.add('data');
54+
trackers.add('#1');
55+
}
56+
if (path.startsWith('app/')) labels.add('app');
57+
if (path.startsWith('.github/workflows/')) labels.add('ci');
58+
if (path.startsWith('docs/') || path === 'README.md' || path.endsWith('.md')) labels.add('documentation');
59+
}
60+
if (title.startsWith('feat') || labels.has('site') || labels.has('data') || labels.has('app')) {
61+
labels.add('enhancement');
62+
}
63+
if (title.startsWith('fix')) {
64+
labels.add('bug');
65+
}
66+
67+
await github.rest.issues.addAssignees({
68+
owner,
69+
repo,
70+
issue_number,
71+
assignees: [...new Set(assignees)],
72+
});
73+
74+
if (labels.size) {
75+
await github.rest.issues.addLabels({
76+
owner,
77+
repo,
78+
issue_number,
79+
labels: [...labels],
80+
});
81+
}
82+
83+
const milestoneByLabel = new Map([
84+
['site', 'Homepage and site improvements'],
85+
['data', 'Massive dataset rebuild (1989-2026)'],
86+
]);
87+
for (const [label, title] of milestoneByLabel) {
88+
if (!labels.has(label)) continue;
89+
const milestones = await github.paginate(github.rest.issues.listMilestones, {
90+
owner,
91+
repo,
92+
state: 'open',
93+
per_page: 100,
94+
});
95+
const milestone = milestones.find((item) => item.title === title);
96+
if (milestone) {
97+
await github.rest.issues.update({
98+
owner,
99+
repo,
100+
issue_number,
101+
milestone: milestone.number,
102+
});
103+
}
104+
break;
105+
}
106+
107+
if (trackers.size) {
108+
const marker = '<!-- techapi-tracking -->';
109+
const existing = pr.body || '';
110+
const refs = [...trackers].sort().map((ref) => `- Refs ${ref}`).join('\n');
111+
const block = `${marker}\n\n## Tracking\n${refs}`;
112+
const body = existing.includes(marker)
113+
? existing.replace(new RegExp(`${marker}[\\s\\S]*$`), block)
114+
: `${existing.trim()}\n\n${block}`.trim();
115+
await github.rest.pulls.update({
116+
owner,
117+
repo,
118+
pull_number: issue_number,
119+
body,
120+
});
121+
}
122+
123+
const changedLines = files.reduce((sum, file) => sum + file.additions + file.deletions, 0);
124+
let priority = 'Medium';
125+
if (labels.has('data') && (files.length >= 25 || changedLines >= 1000)) {
126+
priority = 'High';
127+
} else if (labels.has('data') || labels.has('app') || title.startsWith('fix')) {
128+
priority = 'High';
129+
} else if (labels.size === 1 && labels.has('documentation')) {
130+
priority = 'Low';
131+
} else if (changedLines <= 25 && !labels.has('ci')) {
132+
priority = 'Low';
133+
}
134+
core.exportVariable('TRIAGE_PRIORITY', priority);
135+
136+
- name: Add PR to configured projects
137+
if: env.TRIAGE_PROJECT_URLS != '' && env.PROJECT_TOKEN != ''
138+
env:
139+
GH_TOKEN: ${{ env.PROJECT_TOKEN }}
140+
PR_URL: ${{ github.event.pull_request.html_url }}
141+
shell: bash
142+
run: |
143+
set -euo pipefail
144+
IFS=',' read -ra urls <<< "${TRIAGE_PROJECT_URLS}"
145+
today="$(date -u +%F)"
146+
for url in "${urls[@]}"; do
147+
url="$(echo "$url" | xargs)"
148+
[ -z "$url" ] && continue
149+
if [[ "$url" =~ github.com/orgs/([^/]+)/projects/([0-9]+) ]]; then
150+
owner="${BASH_REMATCH[1]}"
151+
project_number="${BASH_REMATCH[2]}"
152+
elif [[ "$url" =~ github.com/users/([^/]+)/projects/([0-9]+) ]]; then
153+
owner="${BASH_REMATCH[1]}"
154+
project_number="${BASH_REMATCH[2]}"
155+
else
156+
echo "::warning::Unsupported project URL: $url"
157+
continue
158+
fi
159+
160+
project_json="$(gh project view "$project_number" --owner "$owner" --format json)"
161+
project_id="$(jq -r '.id' <<< "$project_json")"
162+
item_id="$(gh project item-add "$project_number" --owner "$owner" --url "$PR_URL" --format json --jq '.id')"
163+
start_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Start date") | .id')"
164+
target_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Target date") | .id')"
165+
priority_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Priority") | .id')"
166+
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")"
167+
if [ -z "$priority_option_id" ]; then
168+
priority_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Priority") | .options[] | select(.name == "Medium") | .id')"
169+
fi
170+
171+
if [ -n "$item_id" ] && [ -n "$start_field_id" ]; then
172+
gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$start_field_id" --date "$today"
173+
fi
174+
if [ -n "$item_id" ] && [ -n "$target_field_id" ]; then
175+
gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$target_field_id" --date "$today"
176+
fi
177+
if [ -n "$item_id" ] && [ -n "$priority_field_id" ] && [ -n "$priority_option_id" ]; then
178+
gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$priority_field_id" --single-select-option-id "$priority_option_id"
179+
fi
180+
done

0 commit comments

Comments
 (0)