Skip to content

Commit 2f699bd

Browse files
authored
Merge pull request #2476 from ArmDeveloperEcosystem/main
production update
2 parents 4a8b5ea + ae0802b commit 2f699bd

File tree

156 files changed

+3829
-489
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

156 files changed

+3829
-489
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
name: Last Reviewed Date Backfill (One Time)
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
dry_run:
7+
description: "Log actions only (no writes)"
8+
type: boolean
9+
default: true
10+
11+
permissions:
12+
contents: read
13+
pull-requests: read
14+
repository-projects: write
15+
16+
jobs:
17+
backfill:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Backfill Last Reviewed Date
21+
uses: actions/github-script@v6
22+
with:
23+
github-token: ${{ secrets.PROJECT_TOKEN }}
24+
script: |
25+
const dryRun = core.getInput('dry_run') === 'true';
26+
27+
const orgLogin = 'ArmDeveloperEcosystem';
28+
const projectNumber = 4;
29+
30+
const ISO_CUTOFF = '2024-12-31';
31+
const toDate = (iso) => new Date(iso + 'T00:00:00.000Z');
32+
33+
// 1) project
34+
const proj = await github.graphql(
35+
`query($org:String!,$num:Int!){ organization(login:$org){ projectV2(number:$num){ id } } }`,
36+
{ org: orgLogin, num: projectNumber }
37+
);
38+
const projectId = proj.organization?.projectV2?.id;
39+
if (!projectId) throw new Error('Project not found');
40+
41+
// 2) fields
42+
const fields = (await github.graphql(
43+
`query($id:ID!){ node(id:$id){ ... on ProjectV2 {
44+
fields(first:50){ nodes{
45+
__typename
46+
... on ProjectV2Field{ id name dataType }
47+
... on ProjectV2SingleSelectField{ id name options{ id name } }
48+
} } } } }`, { id: projectId }
49+
)).node.fields.nodes;
50+
51+
const dateFieldId = (n)=>fields.find(f=>f.__typename==='ProjectV2Field'&&f.name===n&&f.dataType==='DATE')?.id||null;
52+
const statusField = fields.find(f=>f.__typename==='ProjectV2SingleSelectField' && f.name==='Status');
53+
const statusFieldId = statusField?.id;
54+
const publishId = dateFieldId('Publish Date');
55+
const lrdId = dateFieldId('Last Reviewed Date');
56+
57+
if (!statusFieldId || !lrdId) throw new Error('Missing Status or Last Reviewed Date field');
58+
59+
// writers
60+
const setDate = async (itemId, fieldId, iso) => {
61+
if (dryRun) return console.log(`[DRY RUN] setDate item=${itemId} -> ${iso}`);
62+
const m = `mutation($p:ID!,$i:ID!,$f:ID!,$d:Date!){
63+
updateProjectV2ItemFieldValue(input:{projectId:$p,itemId:$i,fieldId:$f,value:{date:$d}}){
64+
projectV2Item{ id }
65+
}}`;
66+
await github.graphql(m, { p: projectId, i: itemId, f: fieldId, d: iso });
67+
};
68+
69+
// helpers
70+
const getDate = (item,id)=>item.fieldValues.nodes.find(n=>n.__typename==='ProjectV2ItemFieldDateValue'&&n.field?.id===id)?.date||null;
71+
const getStatus = (item)=>{ const n=item.fieldValues.nodes.find(n=>n.__typename==='ProjectV2ItemFieldSingleSelectValue'&&n.field?.id===statusFieldId); return n?.name||null; };
72+
73+
// iterate
74+
async function* items(){ let cursor=null; for(;;){
75+
const r=await github.graphql(
76+
`query($org:String!,$num:Int!,$after:String){
77+
organization(login:$org){ projectV2(number:$num){
78+
items(first:100, after:$after){
79+
nodes{
80+
id
81+
content{ __typename ... on PullRequest{ number repository{ name } } }
82+
fieldValues(first:50){ nodes{
83+
__typename
84+
... on ProjectV2ItemFieldDateValue{ field{ ... on ProjectV2Field{ id name } } date }
85+
... on ProjectV2ItemFieldSingleSelectValue{ field{ ... on ProjectV2SingleSelectField{ id name } } name optionId }
86+
} }
87+
}
88+
pageInfo{ hasNextPage endCursor }
89+
}
90+
} } }`,
91+
{ org: orgLogin, num: projectNumber, after: cursor }
92+
);
93+
const page=r.organization.projectV2.items;
94+
for(const n of page.nodes) yield n;
95+
if(!page.pageInfo.hasNextPage) break;
96+
cursor=page.pageInfo.endCursor;
97+
} }
98+
99+
let updates=0;
100+
for await (const item of items()){
101+
if (item.content?.__typename !== 'PullRequest') continue;
102+
103+
const status = getStatus(item);
104+
if (status !== 'Done' && status !== 'Maintenance') continue;
105+
106+
const lrd = getDate(item, lrdId);
107+
if (lrd) continue; // already has a value
108+
109+
if (status === 'Done') {
110+
const publish = publishId ? getDate(item, publishId) : null;
111+
if (publish) {
112+
await setDate(item.id, lrdId, publish);
113+
updates++; console.log(`[Backfill][Done] Set LRD=${publish}`);
114+
} else {
115+
console.log(`[Skip][Done] No Publish Date; not setting LRD`);
116+
}
117+
}
118+
119+
if (status === 'Maintenance') {
120+
await setDate(item.id, lrdId, ISO_CUTOFF);
121+
updates++; console.log(`[Backfill][Maintenance] Set LRD=${ISO_CUTOFF}`);
122+
}
123+
}
124+
125+
console.log(`Backfill complete. Items updated: ${updates}. Dry run: ${dryRun}`);
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
name: Last Reviewed Cron
2+
3+
on:
4+
schedule:
5+
- cron: "0 9 * * *" # daily at 09:00 UTC
6+
workflow_dispatch:
7+
inputs:
8+
dry_run:
9+
description: "Log actions only (no writes)"
10+
type: boolean
11+
default: false
12+
13+
permissions:
14+
contents: read
15+
pull-requests: read
16+
repository-projects: write
17+
18+
jobs:
19+
sweep:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Move items based on Last Reviewed Date
23+
uses: actions/github-script@v6
24+
with:
25+
github-token: ${{ secrets.PROJECT_TOKEN }}
26+
script: |
27+
// Inputs
28+
const dryRun = core.getInput('dry_run') === 'true';
29+
30+
// ---- Config (edit if needed) ----
31+
const orgLogin = 'ArmDeveloperEcosystem';
32+
const projectNumber = 4;
33+
const STATUS_FIELD_NAME = 'Status';
34+
const STATUS_DONE = 'Done';
35+
const STATUS_MAINT = 'Maintenance';
36+
const LRD_FIELD_NAME = 'Last Reviewed Date';
37+
const PUBLISHED_URL_FIELD_NAME = 'Published URL';
38+
// ----------------------------------
39+
40+
// Dates
41+
const TODAY = new Date();
42+
const sixMonthsAgoISO = (() => {
43+
const d = new Date(TODAY);
44+
d.setUTCMonth(d.getUTCMonth() - 6);
45+
return d.toISOString().split('T')[0];
46+
})();
47+
const toDate = (iso) => new Date(iso + 'T00:00:00.000Z');
48+
49+
// Project
50+
const proj = await github.graphql(
51+
`query($org:String!,$num:Int!){
52+
organization(login:$org){
53+
projectV2(number:$num){ id }
54+
}
55+
}`,
56+
{ org: orgLogin, num: projectNumber }
57+
);
58+
const projectId = proj.organization?.projectV2?.id;
59+
if (!projectId) throw new Error('Project not found');
60+
61+
// Fields
62+
const fields = (await github.graphql(
63+
`query($id:ID!){
64+
node(id:$id){
65+
... on ProjectV2 {
66+
fields(first:50){
67+
nodes{
68+
__typename
69+
... on ProjectV2Field { id name dataType }
70+
... on ProjectV2SingleSelectField { id name options { id name } }
71+
}
72+
}
73+
}
74+
}
75+
}`, { id: projectId }
76+
)).node.fields.nodes;
77+
78+
const findDateFieldId = (name) =>
79+
fields.find(f => f.__typename === 'ProjectV2Field' && f.name === name && f.dataType === 'DATE')?.id || null;
80+
81+
const findTextFieldId = (name) => {
82+
const exact = fields.find(f => f.__typename === 'ProjectV2Field' && f.name === name && f.dataType === 'TEXT');
83+
if (exact) return exact.id;
84+
const ci = fields.find(f => f.__typename === 'ProjectV2Field' && (f.name?.toLowerCase?.() === name.toLowerCase()) && f.dataType === 'TEXT');
85+
return ci?.id || null;
86+
};
87+
88+
const statusField = fields.find(f => f.__typename === 'ProjectV2SingleSelectField' && f.name === STATUS_FIELD_NAME);
89+
const statusFieldId = statusField?.id || null;
90+
const doneId = statusField?.options?.find(o => o.name === STATUS_DONE)?.id || null;
91+
const maintId = statusField?.options?.find(o => o.name === STATUS_MAINT)?.id || null;
92+
93+
const lrdId = findDateFieldId(LRD_FIELD_NAME);
94+
const publishedUrlFieldId = findTextFieldId(PUBLISHED_URL_FIELD_NAME);
95+
96+
if (!statusFieldId || !doneId || !maintId || !lrdId) {
97+
throw new Error('Missing required project fields/options: Status/Done/Maintenance or Last Reviewed Date.');
98+
}
99+
100+
// Helpers
101+
const getDate = (item, fieldId) =>
102+
item.fieldValues.nodes.find(n =>
103+
n.__typename === 'ProjectV2ItemFieldDateValue' && n.field?.id === fieldId
104+
)?.date || null;
105+
106+
const getText = (item, fieldId) =>
107+
item.fieldValues.nodes.find(n =>
108+
n.__typename === 'ProjectV2ItemFieldTextValue' && n.field?.id === fieldId
109+
)?.text || null;
110+
111+
const getStatusName = (item) => {
112+
const n = item.fieldValues.nodes.find(n =>
113+
n.__typename === 'ProjectV2ItemFieldSingleSelectValue' && n.field?.id === statusFieldId
114+
);
115+
return n?.name || null;
116+
};
117+
118+
const setStatus = async (itemId, fieldId, optionId) => {
119+
if (dryRun) {
120+
console.log(`[DRY RUN] setStatus item=${itemId} -> option=${optionId}`);
121+
return;
122+
}
123+
const m = `
124+
mutation($p:ID!,$i:ID!,$f:ID!,$o:String!){
125+
updateProjectV2ItemFieldValue(input:{
126+
projectId:$p, itemId:$i, fieldId:$f, value:{ singleSelectOptionId:$o }
127+
}){
128+
projectV2Item { id }
129+
}
130+
}`;
131+
await github.graphql(m, { p: projectId, i: itemId, f: fieldId, o: optionId });
132+
};
133+
134+
async function* iterItems() {
135+
let cursor = null;
136+
for (;;) {
137+
const r = await github.graphql(
138+
`query($org:String!,$num:Int!,$after:String){
139+
organization(login:$org){
140+
projectV2(number:$num){
141+
items(first:100, after:$after){
142+
nodes{
143+
id
144+
content{
145+
__typename
146+
... on PullRequest {
147+
number
148+
repository { name }
149+
}
150+
}
151+
fieldValues(first:100){
152+
nodes{
153+
__typename
154+
... on ProjectV2ItemFieldDateValue {
155+
field { ... on ProjectV2Field { id name } }
156+
date
157+
}
158+
... on ProjectV2ItemFieldTextValue {
159+
field { ... on ProjectV2Field { id name } }
160+
text
161+
}
162+
... on ProjectV2ItemFieldSingleSelectValue {
163+
field { ... on ProjectV2SingleSelectField { id name } }
164+
name
165+
optionId
166+
}
167+
}
168+
}
169+
}
170+
pageInfo {
171+
hasNextPage
172+
endCursor
173+
}
174+
}
175+
}
176+
}
177+
}`,
178+
{ org: orgLogin, num: projectNumber, after: cursor }
179+
);
180+
181+
const page = r.organization.projectV2.items;
182+
for (const n of page.nodes) yield n;
183+
if (!page.pageInfo.hasNextPage) break;
184+
cursor = page.pageInfo.endCursor;
185+
}
186+
}
187+
188+
const lastTwoFromUrl = (url) => {
189+
if (!url) return '';
190+
try {
191+
const u = new URL(url);
192+
const segs = u.pathname.split('/').filter(Boolean);
193+
if (segs.length >= 2) return `${segs[segs.length - 2]}/${segs[segs.length - 1]}/`;
194+
if (segs.length === 1) return `${segs[0]}/`;
195+
return '';
196+
} catch { return ''; }
197+
};
198+
199+
// Movement counters & log
200+
let movedDoneToMaint = 0;
201+
let movedMaintToDone = 0;
202+
const moveLog = [];
203+
204+
// Sweep
205+
for await (const item of iterItems()) {
206+
if (item.content?.__typename !== 'PullRequest') continue; // PRs only
207+
208+
const itemId = item.id;
209+
const status = getStatusName(item);
210+
const lrd = getDate(item, lrdId);
211+
if (!status || !lrd) continue; // only move when LRD exists
212+
213+
const prNumber = item.content.number;
214+
const repoName = item.content.repository.name;
215+
216+
const publishedUrl = publishedUrlFieldId ? getText(item, publishedUrlFieldId) : null;
217+
const lastTwoSegments = lastTwoFromUrl(publishedUrl) || '(no-published-url)';
218+
219+
// Done -> Maintenance: LRD older/equal than 6 months ago
220+
if (status === STATUS_DONE && toDate(lrd) <= toDate(sixMonthsAgoISO)) {
221+
await setStatus(itemId, statusFieldId, maintId);
222+
movedDoneToMaint++;
223+
const line = `[Cron] Moved ${lastTwoSegments} → Maintenance (LRD ${lrd} ≤ ${sixMonthsAgoISO})`;
224+
console.log(line);
225+
moveLog.push(line);
226+
continue; // skip second rule for same item
227+
}
228+
229+
// Maintenance -> Done: LRD within last 6 months (strictly newer than threshold)
230+
if (status === STATUS_MAINT && toDate(lrd) > toDate(sixMonthsAgoISO)) {
231+
await setStatus(itemId, statusFieldId, doneId);
232+
movedMaintToDone++;
233+
const line = `[Cron] Moved ${lastTwoSegments} → Done (LRD ${lrd} > ${sixMonthsAgoISO})`;
234+
console.log(line);
235+
moveLog.push(line);
236+
}
237+
}
238+
239+
// Summary
240+
const totalMoves = movedDoneToMaint + movedMaintToDone;
241+
console.log(`Cron complete. Moved Done→Maintenance: ${movedDoneToMaint}, Maintenance→Done: ${movedMaintToDone}, Total: ${totalMoves}. Dry run: ${dryRun}`);
242+
243+
// Nice Job Summary in the Actions UI
244+
await core.summary
245+
.addHeading('Last Reviewed Cron Summary')
246+
.addTable([
247+
[{ data: 'Direction', header: true }, { data: 'Count', header: true }],
248+
['Done → Maintenance', String(movedDoneToMaint)],
249+
['Maintenance → Done', String(movedMaintToDone)],
250+
['Total moves', String(totalMoves)],
251+
])
252+
.addHeading('Details', 2)
253+
.addCodeBlock(moveLog.join('\n') || 'No moves', 'text')
254+
.write();

0 commit comments

Comments
 (0)