Our GitHub Dependabot app is a useful tool for unattended approval and merge of dependency updates.
Dependabot is a tool for automatic dependency management that was created initially as an external service before being acquired and integrated natively into GitHub. We have been using it extensively since its early versions to automatically upgrade versions of the packages used by our repositories.
We believe that continuous dependency upgrades allow our applications and packages to stay up to date with the latest features, bugs and security fixes by spreading the effort of doing so over a longer period of time. Conversely, delaying package updates postpones the effort to a time when upgrading may be too costly, or even impossible.
At NearForm, we maintain tens of open source npm packages and many other applications we build internally and for our customers. We also contribute to many open source projects, including a large number of repositories in the Fastify ecosystem.
Doing manual maintenance on all of them involves considerable effort, so we looked into ways to automate this process.
How Dependabot works
Dependabot is configured using a .github/dependabot.yml file in any repository. This file contains configuration options to choose which package ecosystems to include (e.g. npm, github-actions) and a set of configuration options to tune the schedule, ignores and so on. For a list of all configuration options, see the documentation.
A dependabot.yml file may look something like this:
When a dependency is out of date, Dependabot opens a pull request on the repository, which updates the files where the dependency is tracked.
A typical Depandabot PR will look like this:
The advantage of using a tool like Dependabot to automatically attempt to upgrade dependency versions is that by opening a pull request, all the checks in place in the repository will run against the updated versions. For example, these checks could run automated tests against the upgraded dependency, which provides a certain degree of confidence about whether the upgrade is safe or if manual changes are needed.
How we use Dependabot
Most of the repositories we contribute to contain npm packages. Because of their standalone nature they often have very high test coverage, meaning that a green build usually implies that the change made in a PR is safe to integrate into the base branch.
We usually configure the repositories so that a PR cannot be merged until:
Some required status checks have succeeded. These often include linting and automated tests.
The PR is approved by a contributor of the repository.
When both requirements are satisfied, the PR can be merged and a repository contributor usually does that manually.
GitHub has recently introduced the ability to automatically merge PRs whose requirements are satisfied without human intervention. We have used this feature in some repositories, but we haven’t found it extremely useful in the scope of the automation that we have set up for Dependabot PRs.
Automating Dependabot merges
Manually reviewing, approving and merging Dependabot PRs over a large number of repositories is a lot of effort, so we looked into ways to automate this.
Our first approach was to use a custom GitHub action that we could include in any workflow. We configured it in this way:
This would trigger the automerge job after the build job completed successfully and invoke the custom fastify/github-action-merge-dependabot action if the run was triggered by a pull request. It would also provide the built-in GITHUB_TOKEN secret to the action, which could use it to approve and merge the PR.
This approach worked flawlessly until recently, when GitHub introduced restrictions on the permissions of the GITHUB_TOKEN secret and the visibility of secrets in workflow runs triggered by Dependabot PRs.
From one day to the next, all our automatic approvals and merges were failing because of GITHUB_TOKEN loss of write permissions on the repositories.
workflow_run trigger to run a workflow as a consequence of another workflow finishing
We evaluated both options and deemed them unsuitable for our purposes for similar reasons: Both of them required changes to the rest of our workflows, and we didn’t want the automerge behavior to affect how the workflows should be written.
The pull_request_target option requires the checkout action to be provided with the commit SHA.
The workflow_run option requires the originating workflow to publish an artifact containing the number of the pull request, which is then read and processed by the second workflow.
Another alternative we evaluated was creating a GitHub App and using it to generate a temporary token with write permissions, to be provided to our custom action as a replacement for the now read-only GITHUB_TOKEN.
We tried using tibdex/github-app-token to do this but we faced the second issue in GitHub’s changes around Dependabot PRs: No secrets besides GITHUB_TOKEN are available.
Therefore, all we are left with is a GITHUB_TOKEN which has read-only permissions and can’t do what we need — approving and merging the pull request.
Using a GitHub App
With possibly hundreds of builds broken because of the automerge action failing, we were under pressure to find a solution, so we came up with the idea of implementing a GitHub App running on a server. Once installed on a repository or an organisation, the app would get write permissions on the repository and would be allowed to approve and merge pull requests.
The problems to solve were:
How to trigger approvals and merges from the app
How to do it in a secure way
This is what we came up with:
The GitHub App needs to be installed on the repository and running as a Web API on a server. It requests permissions to approve PRs and merge them.
A Dependabot PR executes a GitHub workflow which invokes the custom GitHub action with the read-only GITHUB_TOKEN.
The custom GitHub Action sends a HTTP request to the GitHub App including the GITHUB_TOKEN.
The GitHub App checks that the token has access to the repository and then approves and merges the PR.
This approach eventually worked, and we have put it in place to restore full automation of Dependabot in our repositories. We are sharing it as open source for anybody who wants to use it, leaving the option to self-host the GitHub app.
Using this approach, we are basically working around the security limitations imposed by GitHub around GITHUB_TOKEN permissions and visibility of secrets in workflow runs. We are basically trading a read-only token for one with write permissions, even though the latter never leaves the GitHub App.
We believe this is an acceptable compromise between functionality and security because:
The GitHub app can only operate on repositories it’s installed on.
The repository on which it operates is inferred from the GITHUB_TOKEN it is provided, which is scoped to just the repository where the workflow is running, thereby preventing use on another repository. Also, this token is valid only for the duration of the workflow run, thereby preventing reuse.
The operation of the GitHub App is limited to Dependabot PRs only, as the app does an internal check on the author of the PR to ensure it’s Dependabot.
In order to be merged, PRs must have satisfied the merge constraints, and if they do, we will merge it. It doesn’t make much difference whether it is us or an attacker merging.
Therefore, we believe that the surface of attack and the possible damage are so limited that the risk of a Dependabot PR being unintentionally merged is acceptably low.