Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions .github/actions/pagerduty/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: 'PagerDuty Schedule'
description: 'Get information about the given PagerDuty Schedule'
inputs:
schedule-id:
description: 'The id of the schedule'
required: true
token:
description: 'The API token used for making requests to PagerDuty'
required: true
outputs:
user:
description: 'The user who is on call for the final schedule'
value: ${{ steps.pagerduty.outputs.user }}
start:
description: 'The start date for the final schedule'
value: ${{ steps.pagerduty.outputs.start }}
end:
description: 'The end date for the final schedule'
value: ${{ steps.pagerduty.outputs.end }}
id:
description: 'The id for the final schedule'
value: ${{ steps.pagerduty.outputs.id }}
previous-schedule-start:
description: 'The start date for the previous final schedule'
value: ${{ steps.pagerduty.outputs.previous-schedule-start }}
previous-schedule-end:
description: 'The end date for the previous final schedule'
value: ${{ steps.pagerduty.outputs.previous-schedule-end }}
previous-schedule-id:
description: 'The id for the previous final schedule'
value: ${{ steps.pagerduty.outputs.previous-schedule-id }}
previous-schedule-user:
description: 'The individual on call for the previous final schedule'
value: ${{ steps.pagerduty.outputs.previous-schedule-user }}
runs:
using: 'composite'
steps:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Get PagerDuty Schedule
id: pagerduty
uses: actions/github-script@v6
with:
script: |
const { PAGERDUTY_API_KEY } = process.env;

const today = new Date()

// Get current schedule
const schedule = await getSchedule(today);

core.setOutput('user', schedule.user.summary);
core.setOutput('start', schedule.start);
core.setOutput('end', schedule.end);
core.setOutput('id', schedule.id);

const previousScheduleEnd = new Date(schedule.start);
previousScheduleEnd.setDate(previousScheduleEnd.getDate() - 1);

// Get previous schedule
const previousSchedule = await getSchedule(previousScheduleEnd);

core.setOutput('previous-schedule-start', previousSchedule.start);
core.setOutput('previous-schedule-end', previousSchedule.end);
core.setOutput('previous-schedule-id', previousSchedule.id);
core.setOutput('previous-schedule-user', previousSchedule.user.summary);

// Get a schedule
// @see https://developer.pagerduty.com/api-reference/3f03afb2c84a4-get-a-schedule
async function getSchedule(date) {
const url = new URL('https://api.pagerduty.com/schedules/${{ inputs.schedule-id }}')

url.searchParams.append('since', date.toISOString())
url.searchParams.append('until', date.toISOString())

const response = await fetch(url, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Token token=${{ inputs.token }}',
},
})

const data = await response.json()
if (!data.schedule) {
throw new Error('Unable to get schedule for id: ${{ inputs.schedule-id }}')
}

const [schedule] = data.schedule.final_schedule.rendered_schedule_entries
return schedule;
}
183 changes: 100 additions & 83 deletions .github/workflows/release-schedule.yml
Original file line number Diff line number Diff line change
@@ -1,60 +1,32 @@
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: '0 0 * * TUE'
- cron: '30 13 * * MON'

concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions: {}

jobs:
release-conductor:
if: ${{ github.repository == 'primer/react' }}
runs-on: ubuntu-latest
outputs:
conductor: ${{ steps.pagerduty.outputs.result }}
steps:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Fetch user from pagerduty schedule
id: pagerduty
uses: actions/github-script@v6
env:
PAGERDUTY_API_KEY: ${{ secrets.PAGERDUTY_API_KEY_SID }}
with:
result-encoding: string
script: |
const { PAGERDUTY_API_KEY } = process.env;

const today = new Date().toISOString().slice(0, 10); // format: 2022-11-24
const url = new URL('https://api.pagerduty.com/schedules/P3IIVC4');
url.searchParams.append('since', today);
url.searchParams.append('until', today);

const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Token token=${PAGERDUTY_API_KEY}`
}
});
const data = await response.json();
const conductor = data.schedule.final_schedule.rendered_schedule_entries[0].user.summary;

core.info(`${conductor} is release conductor`);

return conductor;

create-tracking-issue:
needs: release-conductor
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@v3
with:
Expand All @@ -64,51 +36,76 @@ jobs:
- name: Create Release Issue
uses: actions/github-script@v6
env:
RELEASE_CONDUCTOR: ${{ needs.release-conductor.outputs.conductor }}
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 = require('date-fns/eachDayOfInterval');
const startOfWeek = require('date-fns/startOfWeek');
const nextFriday = require('date-fns/nextFriday');
const format = require('date-fns/format');
const previousMonday = require('date-fns/previousMonday');

const { RELEASE_CONDUCTOR } = process.env;

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}`);

// Current schedule
const dry = DRY === 'true';
const today = new Date();
const start = startOfWeek(today, { weekStartsOn: 1 });
const end = nextFriday(start);

// Previous schedule
const previousStart = previousMonday(start);
const previousEnd = nextFriday(previousStart);
const start = parseISO(SCHEDULE_START);
const end = parseISO(SCHEDULE_END);

// Issue IDs
const id = `primer-release-schedule:${format(start, 'yyyy-MM-dd')}`;
const previousId = `primer-release-schedule:${format(previousStart, 'yyyy-MM-dd')}`;
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: ${previousStart}`);
core.info(`End: ${previousEnd}`)
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 }).map((day) => {
return `- ${format(day, 'EEEE, LLLL do')}`;
}),
...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 = [
Expand All @@ -129,8 +126,9 @@ jobs:

let ISSUE_BODY = `<!-- ${id} -->\n\n`;

ISSUE_BODY += `_This is a scheduled issue for tracking the release between ${format(start, 'EEEE do')} and ${format(end, 'EEEE do')}_\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,
{
Expand Down Expand Up @@ -167,25 +165,30 @@ jobs:
ISSUE_BODY += '\n';
ISSUE_BODY += notes;

await github.rest.issues.create({
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)) {
return;
}

// This is the previous release issue
if (releaseIssue.body.includes(previousId)) {
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(' ');
Expand All @@ -203,21 +206,35 @@ jobs:
ISSUE_BODY += '\n';
ISSUE_BODY += notes;

// Create the current release issue
await github.rest.issues.create({
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
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: releaseIssue.number,
state: 'closed',
state_reason: 'completed',
});
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}`);
}