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.