Release Schedule #113
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | |
} |