loading...

Schedule GitHub Pull Request merges using Actions

gr2m profile image Gregor Martynus ・4 min read

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

  1. How to set pull request status to "pending" and
  2. 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

  1. Set the date for the merge
  2. 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

Posted on by:

gr2m profile

Gregor Martynus

@gr2m

Father of triplets Nico, Ada & Kian. Web developer with 20+ yrs experience. JavaScript Octokit Maintainer for GitHub. He/him

Discussion

markdown guide