Skip to content

Commit 619bf19

Browse files
Merge branch 'main' into LP-Halide-further-responses
2 parents 852d362 + 3b3e4c5 commit 619bf19

281 files changed

Lines changed: 12172 additions & 6090 deletions

File tree

Some content is hidden

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

.github/copilot-instructions.md

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

0 commit comments

Comments
 (0)