Create or Update PR Action

16 Apr 2021 in Tech

Following on from our theme of actions to commit and push changes, today’s action can commit and push any local edits and raise a pull request automatically.

Fact Sheet
Authorgr2m
Contributors8
Stars31
Repohttps://github.com/gr2m/create-or-update-pull-request-action
Marketplacehttps://github.com/marketplace/actions/create-or-update-pull-request

What does it do?

A GitHub Action to create or update a pull request based on local changes

You can use the action to add, commit and push changes to a remote branch and create a pull request for those changes automatically. It can add all your files in a single commit, add multiple commits to a single PR and automatically add labels and assignees to any pull requests that it raises.

Oh, and it’s built by Gregor which means I instantly trust it.

How does it work?

  1. If there any uncommitted changes, files matching the pattern provided in the path input supplied are added and committed. You can customise the commit with the commit-message and author inputs
  2. If there are any local commits (created by step 1, or by any other means) the action pushes changes to a remote branch with the name specified in the branch input
  3. Search for any open pull requests with branch as the HEAD commit using the search query head:${inputs.branch} type:pr is:open repo:${process.env.GITHUB_REPOSITORY}
  4. If there is an existing pull request, the action stops execution. Otherwise, a new pull request is created with the title and body inputs provided. Any labels and assignees provided as inputs are automatically added

Using the search API to check if there is already an open pull request for a branch is a fantastic solution. When I needed to do the same, I uses the pulls.list endpoint and passed in the head parameter like so:

js
let pr = (
await tools.github.pulls.list({
owner,
repo,
head: `${owner}:${automationBranchName}`,
})
).data[0];
if (!pr) {
// Create a PR
}

What I find interesting about this action is that it only uses the GitHub API for actions that it cannot perform using the git binary. There is a runShellCommand function that is used to run git fetch, git commit etc on the runner directly. When automating updates I’ve used the GitHub API to get and modify file contents to remove the requirement to use actions/checkout as part of your workflow, but in this context you’re likely to have a workspace so it makes sense to interact directly with the files.

Another interesting choice is that the action does not configure a remote or add authentication details, instead opting to specify both values directly in the git commit like so:

js
await runShellCommand(
`git push -f https://x-access-token:${process.env.GITHUB_TOKEN}@github.com/${process.env.GITHUB_REPOSITORY}.git HEAD:refs/heads/${inputs.branch}`
);

There’s nothing in the commit history to explain why, but if I had to hazard a guess it’s due to the fact that other processes can interact with git config files, so by passing the details directly to the command it reduces the chances of anything interfering.

Finally, this action provides an example of how to provide lists of values in inputs. The action uses a comma-separated list of values for labels and assignees to provide multiple values. There still isn’t consensus how to provide multiple values (some actions provide a JSON list and deserialise), but this is another vote for the “comma-separated” camp.

Common use cases

This action is most useful when you’ve got a task running on a schedule that fetches and commits updated data to the repo. If there are no changes, no commit or pull request will be created.

The README itself shows that it’s being used to keep track of Chinese Starbucks stores, download JSON schema updates periodically and even by the Node project itself to keep the license file up to date.

If you’re interested in learning more about this pattern, there’s a great post by Simon Willison on git scraping as a concept.