There is currently no built-in feature to schedule a pull request merge on GitHub. But a little GitHub Action magic can fill in this gap.
In this post, I'll explain
- How to set pull request status to "pending" and
- How to run a script on a schedule using GitHub Actions.
If you want to explore the final code, check out the Schedule Action on GitHub.
How it works
In order to merge a pull request on a specified future date, you need two things
- Set the date for the merge
- Do the actual merge on the specified date
For the first step, the action will look for a string in the pull request description that looks like this
/schedule 2020-02-20
If it exists, it creates a pending status with an explanatory description.
The 2nd step is a script that runs on a special schedule
event. It will look for the string above in all open pull requests. If it finds any, it will merge the pull requests that are scheduled to be merged today.
Set the date for the merge
The workflow file for the first step will look like this, you can create it as .github/workflows/schedule-merge.yml
, the file name does not matter, only the directory.
name: Schedule Merge
on:
pull_request:
types:
- opened
- edited
jobs:
schedule:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: "npm ci"
- run: "node ./.github/actions/schedule-merge.js"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
The workflow is triggered each time a pull request is created or edited (such as a description update). Then it checks out the source code of your repository, it sets up Node and installs your dependencies as defined in ./package.json
. Then it runs the ./.github/actions/schedule.js
file that we still need to create, and passes in the GITHUB_TOKEN
.
A simplified ./.github/actions/schedule-merge.js
file would look like this (See gr2m/merge-schedule-action/lib/handle_pull_request.js for a complete version).
// make sure to `npm install @octokit/action`
const { Octokit } = require("@octokit/action");
scheduleMerge()
async function scheduleMerge() {
const octokit = new Octokit();
// retrieve the full `pull_request` event payload
const eventPayload = require(process.env.GITHUB_EVENT_PATH);
// find and parse the date string from the `/schedule ...` command
const datestring = getScheduleDateString(eventPayload.pull_request.body);
if (!datestring) {
console.log(`No /schedule command found`);
return;
}
// Create a check run using the REST API
// https://developer.github.com/v3/checks/runs/#create-a-check-run
const { data } = await octokit.request('POST /repos/:owner/:repo/check-runs', {
owner: eventPayload.repository.owner.login,
repo: eventPayload.repository.name,
name: "Merge Schedule",
head_sha: eventPayload.pull_request.head.sha,
status: "in_progress",
output: {
title: `Scheduled to me merged on ${datestring}`,
summary: "TO BE DONE: add useful summary"
}
});
console.log(`Check run created: ${data.html_url}`);
}
Now each time you create a pull request or update its description, the action will run and set a pending status if a /schedule ...
command is found.
Merge a scheduled pull request
You can create the workflow file for the 2nd step as .github/workflows/merge-scheduled.yml
. The cron
syntax is rather complicated, but the https://crontab.guru/ website can be a great help.
name: Merge Scheduled Pull Requests
on:
schedule:
# https://crontab.guru/every-day-8am
- cron: 0 8 * * *
jobs:
merge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: "npm ci"
- run: "node ./.github/actions/merge.js"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
A simplified ./.github/actions/merge.js
file would look like this (See gr2m/merge-schedule-action/lib/handle_schedule.js for a complete version).
const { Octokit } = require("@octokit/action");
merge()
async function merge() {
const octokit = new Octokit();
const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/");
const pullRequests = await octokit.paginate(
"GET /repos/:owner/:repo/pulls",
{
owner,
repo,
state: "open"
},
({ data }) => {
return data.filter(hasScheduleCommand).map(pullRequest => {
return {
number: pullRequest.number,
html_url: pullRequest.html_url,
scheduledDate: getScheduleDateString(pullRequest.body)
};
});
}
);
console.log(`${pullRequests.length} scheduled pull requests found`);
if (pullRequests.length === 0) {
return;
}
const duePullRequests = pullRequests.filter(
pullRequest => new Date(pullRequest.scheduledDate) < new Date()
);
console.log(`${duePullRequests.length} due pull requests found`);
if (duePullRequests.length === 0) {
return;
}
for await (const pullRequest of duePullRequests) {
await octokit.pulls.merge({
owner,
repo,
pull_number: pullRequest.number
});
console.log(`${pullRequest.html_url} merged`);
}
}
Combining the workflow files
If you like, you can combine the two workflow files above into one, then run the correct script based on the event. It would look like this
name: Merge Scheduled Pull Requests
on:
pull_request:
types:
- opened
- edited
schedule:
# https://crontab.guru/every-day-8am
- cron: 0 8 * * *
jobs:
schedule:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: "npm ci"
- run: "node ./.github/actions/schedule-merge.js"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
merge:
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: "npm ci"
- run: "node ./.github/actions/merge.js"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Learn more about the GitHub Action workflow syntax on GitHub help.
You can find my merge-schedule
action on the GitHub Marketplace: https://github.com/marketplace/actions/merge-schedule
Top comments (0)