Skip to content

Release Schedule

Release Schedule #113

name: Release Schedule
on:
workflow_dispatch:
inputs:
dry:
type: boolean
description: 'Run in dry mode. This option will disable creating and closing issues'
schedule:
- cron: '30 16 * * MON'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
create-release-issue:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/pagerduty
id: pagerduty
with:
schedule-id: 'P3IIVC4'
token: ${{ secrets.PAGERDUTY_API_KEY_SID }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install packages for github-script
run: npm i date-fns
- name: Create Release Issue
uses: actions/github-script@v7
env:
RELEASE_CONDUCTOR: ${{ steps.pagerduty.outputs.user }}
SCHEDULE_START: ${{ steps.pagerduty.outputs.start }}
SCHEDULE_END: ${{ steps.pagerduty.outputs.end }}
SCHEDULE_ID: ${{ steps.pagerduty.outputs.id }}
PREVIOUS_SCHEDULE_START: ${{ steps.pagerduty.outputs.previous-schedule-start }}
PREVIOUS_SCHEDULE_END: ${{ steps.pagerduty.outputs.previous-schedule-end }}
PREVIOUS_SCHEDULE_ID: ${{ steps.pagerduty.outputs.previous-schedule-id }}
DRY: ${{ github.event.inputs.dry }}
with:
script: |
const {
eachDayOfInterval,
format,
isSaturday,
isSunday,
parseISO,
} = require('date-fns');
const {
RELEASE_CONDUCTOR,
SCHEDULE_START,
SCHEDULE_END,
SCHEDULE_ID,
PREVIOUS_SCHEDULE_START,
PREVIOUS_SCHEDULE_END,
PREVIOUS_SCHEDULE_ID,
DRY,
} = process.env;
core.info(`Running for schedule ${SCHEDULE_ID} from ${SCHEDULE_START} till ${SCHEDULE_END}`);
core.info(`Release conductor: ${RELEASE_CONDUCTOR}`);
const dry = DRY === 'true';
const today = new Date();
const start = parseISO(SCHEDULE_START);
const end = parseISO(SCHEDULE_END);
// Issue IDs
const id = `primer-release-schedule:${SCHEDULE_ID}`;
const previousId = `primer-release-schedule:${PREVIOUS_SCHEDULE_ID}`;
// Debug previous schedule
core.startGroup(`Previous schedule: ${previousId}`);
core.info(`Start: ${parseISO(PREVIOUS_SCHEDULE_START)}`);
core.info(`End: ${parseISO(PREVIOUS_SCHEDULE_END)}`)
core.endGroup();
// Debug current schedule
core.startGroup(`Current schedule: ${id}`);
core.info(`Start: ${start}`);
core.info(`End: ${end}`)
core.endGroup();
// Issue markup
const ISSUE_TITLE = 'Release Tracking';
const timeline = [
'## Timeline',
'',
'<!-- Provide updates for release activities, like cutting releases and different integration points -->',
'',
...eachDayOfInterval({ start, end })
// Only include business days in the timeline
.filter((day) => {
if (isSunday(day) || isSaturday(day)) {
return false;
}
return true;
}).map((day) => {
return `- ${format(day, 'EEEE, LLLL do')}`;
}),
'',
].join('\n');
const checklist = [
'## Checklist',
'',
'- [ ] Checks have passed on the integration Pull Request downstream',
'- [ ] Release tracking Pull Request has been merged',
'- [ ] Stable release available on npm',
'- [ ] Downstream repos have been updated to latest',
'',
].join('\n');
const notes = [
'## Notes',
'',
'<!-- Provide any notes for this release that may be helpful for a future conductor or for consumers -->',
''
].join('\n');
let ISSUE_BODY = `<!-- ${id} -->\n\n`;
ISSUE_BODY += `_This is a scheduled issue for tracking the release between ${format(start, 'EEEE, LLLL do')} and ${format(end, 'EEEE, LLLL do')}_\n\n`;
// Find the latest existing release issue
const iterator = github.paginate.iterator(
github.rest.issues.listForRepo,
{
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100,
}
);
let releaseIssue = null;
for await (const page of iterator) {
releaseIssue = page.data.find((issue) => {
return issue.title === ISSUE_TITLE;
});
if (releaseIssue) {
break;
}
}
// There is no previously open release tracking issue
if (!releaseIssue) {
ISSUE_BODY += '| Last week | Value |\n';
ISSUE_BODY += '| :-------- | :---- |\n';
ISSUE_BODY += '| Issue | |\n';
ISSUE_BODY += '| Conductor | |\n';
ISSUE_BODY += '| Release Pull Request | [Link](https://gh.io/AAksvvr) |\n';
ISSUE_BODY += '| Integration tests | [Link](https://gh.io/AAkr65h) |\n';
ISSUE_BODY += '\n';
ISSUE_BODY += timeline;
ISSUE_BODY += '\n';
ISSUE_BODY += checklist;
ISSUE_BODY += '\n';
ISSUE_BODY += notes;
const issue = {
owner: context.repo.owner,
repo: context.repo.repo,
title: ISSUE_TITLE,
body: ISSUE_BODY,
assignees: [RELEASE_CONDUCTOR],
};
if (dry) {
core.info('Creating issue:');
core.info(JSON.stringify(issue, null, 2));
} else {
await github.rest.issues.create(issue);
}
return;
}
core.info(`Found release issue: ${releaseIssue.html_url}`);
// We already have an issue open for the current release
if (releaseIssue.body.includes(id)) {
core.info(`A release issue already exists with id: ${id}`);
} else if (releaseIssue.body.includes(previousId)) {
// This is the previous release issue
const assignees = releaseIssue.assignees.map((assignee) => {
return assignee.login;
}).join(' ');
ISSUE_BODY += '| Last week | Value |\n';
ISSUE_BODY += '| :-------- | :---- |\n';
ISSUE_BODY += `| Issue | [${releaseIssue.title}](${releaseIssue.html_url}) |\n`;
ISSUE_BODY += `| Conductor | ${assignees} |\n`;
ISSUE_BODY += '| Release Pull Request | [Link](https://gh.io/AAksvvr) |\n';
ISSUE_BODY += '| Integration tests | [Link](https://gh.io/AAkr65h) |\n';
ISSUE_BODY += '\n';
ISSUE_BODY += timeline;
ISSUE_BODY += '\n';
ISSUE_BODY += checklist;
ISSUE_BODY += '\n';
ISSUE_BODY += notes;
const issue = {
owner: context.repo.owner,
repo: context.repo.repo,
title: ISSUE_TITLE,
body: ISSUE_BODY,
assignees: [RELEASE_CONDUCTOR],
};
// Create the current release issue
if (dry) {
core.info('Creating issue:');
core.info(JSON.stringify(issue, null, 2));
} else {
await github.rest.issues.create(issue);
}
// Close the previous release issue
if (dry) {
core.info(`Closing issue: ${releaseIssue.number}`);
} else {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: releaseIssue.number,
state: 'closed',
state_reason: 'completed',
});
}
} else {
// This is a release issue that we cannot identify
core.info(`Unable to match a current or previous release id for issue #${releaseIssue.number}`);
}