Skip to content

[codex] fix metadata sync fallback and docs alignment #102

[codex] fix metadata sync fallback and docs alignment

[codex] fix metadata sync fallback and docs alignment #102

name: Checklist Finalization
on:
issues:
types: [closed]
pull_request_target:
types: [closed]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
finalize-issue-checklists:
if: github.event_name == 'issues' && github.event.issue.state == 'closed' && github.event.issue.state_reason != 'not_planned'
runs-on: ubuntu-latest
steps:
- name: Finalise issue checklist sections
uses: actions/github-script@v7
with:
script: |
const marker = '<!-- checklist-finalisation -->';
const issue = context.payload.issue;
const body = issue.body || '';
const updated = finaliseChecklistSections(body, [
/^##\s+Definition of Ready \(DoR\)\s*$/im,
/^##\s+Definition of Done \(DoD\)\s*$/im,
]);
if (updated === body) {
core.info(`Issue #${issue.number} already has finalised checklist sections.`);
return;
}
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: updated,
});
await addOrUpdateMarkerComment(issue.number, marker, '✅ Issue checklists finalised after closure.');
core.info(`Finalised checklist sections on issue #${issue.number}.`);
function sectionBounds(bodyText, headingRegex) {
const text = (bodyText || '').replace(/\r\n/g, '\n');
const match = text.match(headingRegex);
if (!match || match.index === undefined) {
return null;
}
const start = match.index + match[0].length;
const remainder = text.slice(start);
const nextHeading = remainder.match(/^##\s+.+$/m);
const end = nextHeading ? start + nextHeading.index : text.length;
return { start, end, text };
}
function completeChecklistItems(sectionText) {
return sectionText.replace(/(^|\n)(\s*-\s*)\[\s\](\s*)/g, '$1$2[x]$3');
}
function finaliseChecklistSections(bodyText, headings) {
let nextBody = (bodyText || '').replace(/\r\n/g, '\n');
let changed = false;
for (const headingRegex of headings) {
const bounds = sectionBounds(nextBody, headingRegex);
if (!bounds) {
continue;
}
const section = nextBody.slice(bounds.start, bounds.end);
const updatedSection = completeChecklistItems(section);
if (updatedSection !== section) {
nextBody = `${nextBody.slice(0, bounds.start)}${updatedSection}${nextBody.slice(bounds.end)}`;
changed = true;
}
}
return changed ? nextBody : bodyText;
}
async function addOrUpdateMarkerComment(issueNumber, markerText, message) {
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100,
});
const existing = comments.find((comment) =>
comment.user?.type === 'Bot' && comment.body?.includes(markerText),
);
const body = [markerText, message].join('\n');
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
return;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body,
});
}
finalize-pr-checklists:
if: github.event_name == 'pull_request_target' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Finalise PR checklist sections
uses: actions/github-script@v7
with:
script: |
const marker = '<!-- checklist-finalisation -->';
const pr = context.payload.pull_request;
const body = pr.body || '';
const updated = finaliseChecklistSections(body, [
/^###\s+Checklist\s+\(Global DoD\s*\/\s*PR\)\s*$/im,
]);
if (updated === body) {
core.info(`PR #${pr.number} already has finalised checklist sections.`);
return;
}
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: updated,
});
await addOrUpdateMarkerComment(pr.number, marker, '✅ PR checklists finalised after merge.');
core.info(`Finalised checklist sections on PR #${pr.number}.`);
function sectionBounds(bodyText, headingRegex) {
const text = (bodyText || '').replace(/\r\n/g, '\n');
const match = text.match(headingRegex);
if (!match || match.index === undefined) {
return null;
}
const start = match.index + match[0].length;
const remainder = text.slice(start);
const nextHeading = remainder.match(/^##\s+.+$/m);
const end = nextHeading ? start + nextHeading.index : text.length;
return { start, end, text };
}
function completeChecklistItems(sectionText) {
return sectionText.replace(/(^|\n)(\s*-\s*)\[\s\](\s*)/g, '$1$2[x]$3');
}
function finaliseChecklistSections(bodyText, headings) {
let nextBody = (bodyText || '').replace(/\r\n/g, '\n');
let changed = false;
for (const headingRegex of headings) {
const bounds = sectionBounds(nextBody, headingRegex);
if (!bounds) {
continue;
}
const section = nextBody.slice(bounds.start, bounds.end);
const updatedSection = completeChecklistItems(section);
if (updatedSection !== section) {
nextBody = `${nextBody.slice(0, bounds.start)}${updatedSection}${nextBody.slice(bounds.end)}`;
changed = true;
}
}
return changed ? nextBody : bodyText;
}
async function addOrUpdateMarkerComment(issueNumber, markerText, message) {
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100,
});
const existing = comments.find((comment) =>
comment.user?.type === 'Bot' && comment.body?.includes(markerText),
);
const body = [markerText, message].join('\n');
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
return;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body,
});
}