Due to the sunsetting of Greenkeeper, I was recently forced to explore alternatives for automated dependency updates. Since Dependabot didn’t seem to work as expected, it struck me that with existing tools (e.g. npm-check-updates for JavaScript projects), it should be fairly straightforward to periodically check a project’s dependencies with a cron job. That would also provide more flexibility to customize the process where needed.

A while back I’d documented my journey to figure out automated GitHub Pages deployments with GitHub Actions, for which I’d created a script to make a GitHub repository automatically update itself – which is just what we need here as well. Since GitHub Actions also supports scheduled jobs, that article provides a solid foundation for our proposition above. Let’s start by tweaking that existing script:

#!/usr/bin/env bash

set -eu

repo_uri="https://x-access-token:$DEPENDENCIES_TOKEN@github.com/$GITHUB_REPOSITORY.git"
remote_name="origin"
main_branch="master"
target_branch="dependencies-latest"

cd "$GITHUB_WORKSPACE"

git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@bots.github.com"

# start out with a pristine target branch
git checkout -B "$target_branch"
git reset --hard "$remote_name/$main_branch"

./bin/update-dependencies

git commit -m "updated dependencies"
if [ "$?" != "0" ]; then
	echo "nothing to commit"
	exit 0
fi

git remote set-url "$remote_name" "$repo_uri"
git push --force-with-lease "$remote_name" "$target_branch"

Here we run ./bin/update-dependencies (a placeholder for something like npm-check-updates, i.e. ncu -u && git add package.json) to update our dependency declarations and then commit the result to the target branch. The script relies on environment variables provided by GitHub Actions as well as a personal access token (PAT), which we need to add to our repo’s secrets (via Settings → Secrets; named DEPENDENCIES_TOKEN here).

Now we can make GitHub Actions periodically execute that script (e.g. ./bin/check-dependencies) by creating a workflow description (e.g. .github/workflows/dependencies.yml):

name: dependencies

on:
  schedule:
    - cron: "0 5 * * 1" # every Monday at 5 AM

jobs:
    check:
        runs-on: ubuntu-latest
        steps:
        - uses: actions/checkout@v1
        - run: ./bin/check-dependencies
          env:
              DEPENDENCIES_TOKEN: ${{ secrets.DEPENDENCIES_TOKEN }}

(contab.guru is pretty helpful for figuring out cron schedule expressions.)

While that’s helpful already, we really want to be notified when dependencies were updated. Since we’re already using GitHub, we might as well create a pull request (PR) via their API. For that we’ll need to add our username (the one we used to create the PAT) to the workflow description – plus we’ll need Node for processing below:

- uses: actions/setup-node@v1
- run: ./bin/check-dependencies
  env:
      DEPENDENCIES_TOKEN: ${{ secrets.DEPENDENCIES_TOKEN }}
      DEPENDENCIES_USER: fnd # required due to personal access token

Then we add a few HTTP requests to the end of our script:

api_request() {
	method="$1"
	shift
	path="$1"
	shift
	curl -u "$DEPENDENCIES_USER:$DEPENDENCIES_TOKEN" -X "$method" "$@" \
			"https://api.github.com/repos/${GITHUB_REPOSITORY}${path}"
}

# determine whether an open pull request already exists
org=`echo "$GITHUB_REPOSITORY" | sed 's#/.*##'`
api_request GET "/pulls?state=open&head=$org:$target_branch" > prs.json
pr=`node -p -e 'let prs = require("./prs.json"); prs.length > 0 && prs[0].number'`
if [ "$pr" != "false" ]; then
	# add comment to existing PR
	api_request POST "/issues/$pr/comments" --data-binary @- <<EOF
{
	"body": "🤖 dependencies updated"
}
EOF
	exit 0
fi
# create pull request
api_request POST "/pulls" --data-binary @- <<EOF
{
	"title": "automated dependencies update",
	"head": "$target_branch",
	"base": "$main_branch"
}
EOF

(Note that we’re using Node here to interpret the API response.)

That’s it: Whenever dependencies are updated, a PR will be created or the existing one will be updated.

One caveat though: Since we’re using a personal access token to authenticate with GitHub’s API, the respective user won’t get an e-mail notification for PRs created/updated by our script. A workaround would be to use a friend’s PAT or create a technical user instead – neither of which seems very elegant. If there’s a better way, let me know in the comments below.