Skip to content

Commit bdba709

Browse files
committed
ci: auto-fill PR metadata
1 parent f7945e5 commit bdba709

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

.github/workflows/pr-metadata.yml

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+
- name: Add PR to configured projects
124+
if: env.TRIAGE_PROJECT_URLS != '' && env.PROJECT_TOKEN != ''
125+
env:
126+
GH_TOKEN: ${{ env.PROJECT_TOKEN }}
127+
PR_URL: ${{ github.event.pull_request.html_url }}
128+
shell: bash
129+
run: |
130+
set -euo pipefail
131+
IFS=',' read -ra urls <<< "${TRIAGE_PROJECT_URLS}"
132+
today="$(date -u +%F)"
133+
for url in "${urls[@]}"; do
134+
url="$(echo "$url" | xargs)"
135+
[ -z "$url" ] && continue
136+
if [[ "$url" =~ github.com/orgs/([^/]+)/projects/([0-9]+) ]]; then
137+
owner="${BASH_REMATCH[1]}"
138+
project_number="${BASH_REMATCH[2]}"
139+
elif [[ "$url" =~ github.com/users/([^/]+)/projects/([0-9]+) ]]; then
140+
owner="${BASH_REMATCH[1]}"
141+
project_number="${BASH_REMATCH[2]}"
142+
else
143+
echo "::warning::Unsupported project URL: $url"
144+
continue
145+
fi
146+
147+
project_json="$(gh project view "$project_number" --owner "$owner" --format json)"
148+
project_id="$(jq -r '.id' <<< "$project_json")"
149+
item_id="$(gh project item-add "$project_number" --owner "$owner" --url "$PR_URL" --format json --jq '.id')"
150+
start_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Start date") | .id')"
151+
target_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Target date") | .id')"
152+
153+
if [ -n "$item_id" ] && [ -n "$start_field_id" ]; then
154+
gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$start_field_id" --date "$today"
155+
fi
156+
if [ -n "$item_id" ] && [ -n "$target_field_id" ]; then
157+
gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$target_field_id" --date "$today"
158+
fi
159+
done

0 commit comments

Comments
 (0)