Headless GitHub - create an pull request modifying file

Imagine you have an static site, where each page has corresponding markdown file in GitHub repository.

You want to somehow allow visitors to propose changes to your site.

But visitors are not IT nerds and do not know anything about Git and GitHub.

An alternative approach will be to have some kind of lambda function that will do the work and hide all complexity from the end user.

For thing to happen we need:

  1. Retrieve current file sha
  2. Create new branch
  3. Push changes to it
  4. Create an pull request

Retrieving file contents

File "contents" can be retrieved as easy as

curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/mac2000/demo/contents/README.md"

From an respond we need two things:

  • content - base64 endocded file content string
  • sha - file sha, we need it when we will call update endpoint, to point out that we are updating exiting file rather than creating new one

Here is how we may get content

curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/mac2000/demo/contents/README.md" | jq -r ".content" | base64 -d

Creating branch

This one is pretty straight forward

curl -X POST -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/mac2000/demo/git/refs" -d "{\"ref\":\"refs/heads/my-awesome-branch\",\"sha\":\"xxxxxxxxx\"}"

where:

  • my-awesome-branch - is our branch name
  • xxxxxxxxx - is an sha of commit from where we are creating branch, usually it will be an main branch last commit

Push changes

To save updated content we need encode it, aka: echo "My new content" | base64, pass previous sha (we did saved it when contents were retrieved) and pass branch name created in previous step

curl -X PUT -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/mac2000/demo/contents/README.md" -d "{\"message\":\"Update readme\",\"content\":\"$CONTENT_ENCODED\",\"sha\":\"$CONTENT_SHA\",\"branch\":\"my-awesome-branch\"}"

Notes:

  • it is important to not mess up with sha of file and branch, otherwise you will receive an error like this: "README.md does not match xxxxxxxxx"
  • we can pass commiter object to store user email in case we want to notify him after merge or to ask some questions

Create pull request

And the very last step is to create an pull request, aka

curl -X POST -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/mac2000/demo/pulls" -d "{\"title\":\"hello\",\"head\":\"my-awesome-branch\",\"base\":\"main\",\"body\":\"world\"}"

Note: pull request body is a good place to pass any meta data we want

Recap

With this in place we can have whole edit page experience or suggest changes on our website, without the need for end user to deal with GitHub at all.

Here is an snippet I have ended up with:

#!/usr/bin/env bash

# step 1: retrieve
CONTENT=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/mac2000/demo/contents/README.md")
CONTENT_DECODED=$(echo $CONTENT | jq -r .content | base64 -d)
CONTENT_SHA=$(echo $CONTENT | jq -r .sha)
echo "BEFORE: '$CONTENT_DECODED'"
CONTENT_DECODED="$CONTENT_DECODED mac was here"
echo "AFTER: '$CONTENT_DECODED'"
CONTENT_ENCODED=$(echo -n "$CONTENT_DECODED" | base64)

# step 2: create branch
BRANCH_NAME="update-readme-$(date +%s)"
MAIN_BRANCH_SHA=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/mac2000/demo/git/ref/heads/main" | jq -r .object.sha)
curl -X POST -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/mac2000/demo/git/refs" -d "{\"ref\":\"refs/heads/$BRANCH_NAME\",\"sha\":\"$MAIN_BRANCH_SHA\"}"

# step 3: push changes
curl -X PUT -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/mac2000/demo/contents/README.md" -d "{\"message\":\"Update readme\",\"content\":\"$CONTENT_ENCODED\",\"sha\":\"$CONTENT_SHA\",\"branch\":\"$BRANCH_NAME\"}"

# step 4: create pull request
curl -X POST -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/mac2000/demo/pulls" -d "{\"title\":\"hello\",\"head\":\"$BRANCH_NAME\",\"base\":\"main\",\"body\":\"world\"}"