Skip to content

Commit 9bc55ef

Browse files
committed
[Actions] Updated .github/actions/check-composite-action-versions/action.yml
1 parent 39fb720 commit 9bc55ef

1 file changed

Lines changed: 106 additions & 28 deletions

File tree

  • .github/actions/check-composite-action-versions

‎.github/actions/check-composite-action-versions/action.yml‎

Lines changed: 106 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ runs:
228228
'',
229229
...planSteps,
230230
'',
231-
'This item has been pre-approved (Workflow Status set directly to `Approved`) under the',
231+
'This item has been pre-approved (its status on the Workflow board set directly to `Approved`) under the',
232232
'narrow policy for composite-action pin-bump issues raised by this workflow - see',
233233
'`.github/actions/check-composite-action-versions/action.yml`.',
234234
].join('\n');
@@ -247,6 +247,15 @@ runs:
247247
// why that gate normally must never be set by automation. Best-effort: the "Workflow"
248248
// project is optional (see same doc), so any failure here just leaves the issue at
249249
// "Not Started" for a human/the orchestrator's normal fallback to pick up.
250+
const OVERWRITE_SETTLE_DELAY_MS = 5 * 60 * 1000;
251+
// The board's status field is either the built-in "Status" (once the board is converted)
252+
// or the older custom "Workflow Status". Only a single-select field that actually offers
253+
// an "Approved" option qualifies, so an unconverted board's built-in "Status"
254+
// (Todo/In Progress/Done) is passed over; when both qualify mid-conversion, "Status" wins.
255+
const selectStatusField = fields => {
256+
const approvable = fields.filter(f => f?.options?.some(o => o.name === 'Approved'));
257+
return approvable.find(f => f.name === 'Status') ?? approvable.find(f => f.name === 'Workflow Status');
258+
};
250259
try {
251260
const projectQuery = `
252261
query($owner: String!, $repo: String!) {
@@ -277,11 +286,15 @@ runs:
277286
if (!project) {
278287
core.warning('⚠️ No "Workflow" project linked to this repo - skipping pre-approval');
279288
} else {
280-
const statusField = (project.fields?.nodes ?? []).find(f => f?.name === 'Workflow Status');
281-
const approvedOption = statusField?.options?.find(o => o.name === 'Approved');
282-
if (!statusField || !approvedOption) {
283-
core.warning('⚠️ "Workflow Status" field or "Approved" option not found - skipping pre-approval');
289+
const statusField = selectStatusField(project.fields?.nodes ?? []);
290+
if (!statusField) {
291+
core.warning(
292+
'⚠️ No "Status" or "Workflow Status" single-select field with an "Approved" option found ' +
293+
'- skipping pre-approval'
294+
);
284295
} else {
296+
const approvedOption = statusField.options.find(o => o.name === 'Approved');
297+
core.info(`ℹ️ Using the "${statusField.name}" field for pre-approval`);
285298
const addResult = await github.graphql(
286299
`mutation($p: ID!, $c: ID!) {
287300
addProjectV2ItemById(input: { projectId: $p, contentId: $c }) { item { id } }
@@ -290,41 +303,106 @@ runs:
290303
);
291304
const itemId = addResult.addProjectV2ItemById.item.id;
292305
const readBackQuery = `
293-
query($i: ID!) {
306+
query($i: ID!, $n: String!) {
294307
node(id: $i) {
295308
... on ProjectV2Item {
296-
fieldValueByName(name: "Workflow Status") {
309+
fieldValueByName(name: $n) {
297310
... on ProjectV2ItemFieldSingleSelectValue { optionId }
298311
}
299312
}
300313
}
301314
}`;
302-
303-
// The item was just added to the project, so the field write below can be
304-
// accepted (no GraphQL error) without actually persisting yet - a known
305-
// eventual-consistency race in the Projects v2 API on freshly-added items.
306-
// Verify with a read-back and retry rather than trusting the mutation response.
307-
let verified = false;
308-
for (let attempt = 1; attempt <= 3 && !verified; attempt++) {
309-
await github.graphql(
310-
`mutation($p: ID!, $i: ID!, $f: ID!, $v: String!) {
311-
updateProjectV2ItemFieldValue(
312-
input: { projectId: $p, itemId: $i, fieldId: $f, value: { singleSelectOptionId: $v } }
313-
) { projectV2Item { id } }
314-
}`,
315-
{ p: project.id, i: itemId, f: statusField.id, v: approvedOption.id }
316-
);
317-
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
318-
const readBack = await github.graphql(readBackQuery, { i: itemId });
319-
verified = readBack.node?.fieldValueByName?.optionId === approvedOption.id;
320-
if (!verified) {
321-
core.info(`⚠️ Pre-approval write not yet visible on attempt ${attempt} - retrying`);
315+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
316+
const applyApprovedStatus = () => github.graphql(
317+
`mutation($p: ID!, $i: ID!, $f: ID!, $v: String!) {
318+
updateProjectV2ItemFieldValue(
319+
input: { projectId: $p, itemId: $i, fieldId: $f, value: { singleSelectOptionId: $v } }
320+
) { projectV2Item { id } }
321+
}`,
322+
{ p: project.id, i: itemId, f: statusField.id, v: approvedOption.id }
323+
);
324+
const isApprovedNow = async () =>
325+
(await github.graphql(readBackQuery, { i: itemId, n: statusField.name })).node?.fieldValueByName?.optionId === approvedOption.id;
326+
// Every write to this field is followed by a read-back rather than trusting the
327+
// mutation response, because a freshly-added item's field write can be accepted
328+
// (no GraphQL error) without actually persisting yet - a known eventual-consistency
329+
// race in the Projects v2 API. This retry+verify covers only that short
330+
// write-propagation delay - it does not and cannot detect the project's own
331+
// built-in "Item added to project" automation later overwriting this same field;
332+
// see the settle-and-recheck step below for that.
333+
const applyAndVerifyApproved = async (maxAttempts, onRetry) => {
334+
let ok = false;
335+
for (let attempt = 1; attempt <= maxAttempts && !ok; attempt++) {
336+
await applyApprovedStatus();
337+
await sleep(attempt * 1000);
338+
ok = await isApprovedNow();
339+
if (!ok && onRetry) {
340+
onRetry(attempt);
341+
}
322342
}
323-
}
343+
return ok;
344+
};
345+
346+
const verified = await applyAndVerifyApproved(3, attempt =>
347+
core.info(`⚠️ Pre-approval write not yet visible on attempt ${attempt} - retrying`)
348+
);
324349
325350
if (verified) {
326351
core.info('✅ Pre-approved on the Workflow board');
327352
core.notice(`Pre-approved issue #${issue.number} on the Workflow board`);
353+
354+
// The "Workflow" project has an enabled built-in "Item added to project"
355+
// automation that sets a default Status on any newly-added item. It fires
356+
// asynchronously off the same "item added" event as the write above, with
357+
// unbounded latency, so it can silently overwrite Approved with its own
358+
// default afterwards - see
359+
// https://github.com/funfair-tech/funfair-server-template/issues/1004. The
360+
// retry loop above cannot catch this: it only outraces Projects v2
361+
// write-propagation delay, on the order of seconds, not this automation, which
362+
// can fire minutes later. Settle for much longer than that, then re-check and
363+
// re-apply once if needed - not an unbounded loop, since the automation only
364+
// fires once per item.
365+
// Scoped to its own try/catch, separate from the outer one around the whole
366+
// pre-approval block: that block's core.notice above has already reported
367+
// pre-approval as successful, so a failure here must be reported as its own,
368+
// more specific warning rather than falling through to the generic
369+
// "Failed to pre-approve" message the outer catch would otherwise log.
370+
try {
371+
await sleep(OVERWRITE_SETTLE_DELAY_MS);
372+
if (!(await isApprovedNow())) {
373+
// A non-Approved read here isn't necessarily the automation's overwrite -
374+
// the issue may have been legitimately closed in the meantime. Check state
375+
// before re-applying, so a closed issue isn't force-reopened-in-spirit by
376+
// having Approved reapplied to it.
377+
const { data: currentIssue } = await github.rest.issues.get({
378+
owner: context.repo.owner,
379+
repo: context.repo.repo,
380+
issue_number: issue.number,
381+
});
382+
if (currentIssue.state !== 'open') {
383+
core.info(`ℹ️ Issue #${issue.number} is no longer open - skipping re-apply`);
384+
} else {
385+
core.warning(
386+
`⚠️ ${statusField.name} for issue #${issue.number} was reset after pre-approval ` +
387+
`(likely by the project's built-in "Item added to project" automation) - re-applying Approved`
388+
);
389+
if (await applyAndVerifyApproved(3)) {
390+
core.info(`✅ Re-applied Approved status for issue #${issue.number} after overwrite`);
391+
} else {
392+
core.warning(
393+
`⚠️ Attempted to re-apply Approved status for issue #${issue.number} but it ` +
394+
`still does not read back as Approved after 3 attempts - issue left for ` +
395+
`manual/orchestrator review`
396+
);
397+
}
398+
}
399+
}
400+
} catch (e) {
401+
core.warning(
402+
`⚠️ Pre-approval succeeded but the settle-and-recheck step failed for issue ` +
403+
`#${issue.number}: ${e.message} - issue left for manual/orchestrator review`
404+
);
405+
}
328406
} else {
329407
core.warning(
330408
`⚠️ Pre-approval mutation returned no error but did not persist after 3 attempts ` +

0 commit comments

Comments
 (0)