How to keep your project dependencies up to date?

If you are looking for a way to automatically keep your project dependencies up to date, I have a solution for you.

cover

You might be like me and have a one or two personal projects that you haven’t touched for a while. When you finally get back to them, you realize that there are a lot of dependencies that needs to be updated. It can be a tedious task to go through all of them and check if there are any new versions available. You might also find yourself in a situation where the dependencies are so out-dated, that it would be easier to start everything from scratch, rather than migrating to new versions.


TLDR; Using Dependabot automated pull requests for version upgrades, Playwright for E2E and visual regression testing and GitHub Actions to automatically approve and merge the pull requests, you can almost completely automate the process of keeping your project dependencies up to date.

The following guide is based on a Next.js project, but the same principles can be applied to other projects based on different programming languages and frameworks.


Let’s get started

1. Adding Dependabot to your repository

This is done easily by adding a dependabot.yml file inside the .github folder in your repository. Here is an example of how the file could look like:

# .github/dependabot.yml

version: 2
updates:
  - package-ecosystem: "npm"
    open-pull-requests-limit: 10
    directory: "/"
    schedule:
      interval: "daily"
      time: "08:00"
      timezone: "Europe/Helsinki"

To quickly go through the main parts of the file:

  • package-ecosystem specifies the package manager that Dependabot should look for updates for. In this case, it’s npm.
  • open-pull-requests-limit specifies the maximum number of open pull requests that Dependabot can have at the same time.
  • directory specifies the directory where Dependabot should look for updates. In this case, it’s the root directory.
  • schedule specifies how often Dependabot should check for updates. In this case, it’s daily at 8:00 AM in the Europe/Helsinki timezone.

2. Adding Playwright tests part of your CI pipeline

To make sure that the updates don’t break your application , you can add Playwright tests to your CI pipeline.


2.1. Install playwright

You should follow the official installation guide to install Playwright.


2.2. Create a test file
// tests/example.spec.ts
import {test, expect} from '@playwright/test';

const BASE_URL = 'http://localhost:3000';

test('verify title', async ({page}) => {
    await page.goto(BASE_URL);
    await expect(page).toHaveTitle(/Page title/);
});

In the test we are simply visiting the local development server and checking that the title of the page matches the regex /Page title/.


2.3. Adding visual regression tests

You can add visual regression tests to your Playwright tests by using the toHaveScreenshot matcher. Here is an example of how you could add a visual regression test to the previous test file:

test("regression test", async ({page}) => {
    await page.goto(BASE_URL)
    await expect(page).toHaveScreenshot('frontpage.png', {fullPage: true});
})

You can read more about the visual regression testing with screenshots from the official documentation here .


2.4. Running the tests with GitHub Actions

You can create a GitHub Actions workflow file that runs the tests on every pull request. Here is an example of how the file could look like:

# .github/workflows/playwright.yml

name: Playwright tests

on:
  pull_request:

jobs:
  playwright-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - name: Install dependencies
        run: npm i
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      - name: Build app
        run: npm run build
      - name: Run Playwright tests
        id: run-tests
        run: npm run test

3. Automatically approve and merge Dependabot pull requests

To automatically approve and merge the Dependabot pull requests , you can create a GitHub Actions workflow file that listens for new pull requests from Dependabot and automatically approves and merges them.

Here is an example of how the file could look like:

# .github/workflows/dependabot.yml

name: Dependabot auto-approve
on: pull_request

permissions:
  contents: write
  pull-requests: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: github.actor == 'dependabot[bot]'
    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v2
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - name: Approve a PR
        run: gh pr review --approve "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
      - name: Enable auto-merge for Dependabot PRs
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GH_TOKEN: ${{secrets.GITHUB_TOKEN}}

What do we have so far?

Here’s a quick recap what we have done so far:

  • Added Dependabot to your repository to automatically create pull requests for version upgrades.
  • Added Playwright tests to your CI pipeline to make sure that the updates don’t break your application.
  • Created a GitHub Actions workflow file that listens for new pull requests from Dependabot and automatically approves and merges them.

There are still a few caveats to this setup that you should be aware of:

  • The Dependabot workflow for approving and merging does not take into account whether the tests passed or not.
  • If you are on GitHub Free plan and are planning to keep the repository private, you are not able to use branch protection rules to require certain checks to pass before merging.

I was able to find a workaround for the above mentioned by creating a single workflow file, that listens for both the Dependabot and Playwright events. The workflow file then runs the Playwright tests and only approves and merges the Dependabot pull request if the tests pass.

It’s not dependant on branch protection rules nor any 3rd party actions. It will still work even if I decide to keep the repository private.


Here’s an example of a final workflow file that was able to achieve this for me personally and have been running successfully for a while now:

# .github/workflows/ci.yml
name: CI checks

on:
  push:
permissions:
  pull-requests: write
  contents: write
  statuses: read
  checks: read
  actions: read

jobs:
  lint:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: '.tool-versions'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  playwright-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    needs:
      - lint
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version-file: '.tool-versions'
          cache: 'npm'

      - name: Cache Node Modules
        id: cache-node-modules
        uses: actions/cache@v4
        with:
          path: node_modules
          key: node-modules-${{ hashFiles('package-lock.json') }}

      - name: Install dependencies
        if: steps.cache-node-modules.outputs.cache-hit != 'true'
        run: npm i

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: Next.js cache
        uses: actions/cache@v4
        with:
          # See here for caching with `yarn` https://github.com/actions/cache/blob/main/examples.md#node---yarn or you can leverage caching with actions/setup-node https://github.com/actions/setup-node
          path: |
            ~/.npm
            ${{ github.workspace }}/.next/cache
          # Generate a new cache whenever packages or source files change.
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
          # If source files changed but packages didn't, rebuild from a prior cache.
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
      - name: Build app
        run: npm run build

      - name: Run Playwright tests
        id: run-tests
        run: npm run test:ci

      - name: Upload Playwright report
        uses: actions/upload-artifact@v3
        if: failure() && steps.run-tests.conclusion == 'failure'
        with:
          name: playwright-report
          path: playwright-report
          retention-days: 3

      - name: Update screenshots in visual tests
        id: run-tests-update
        if: failure() && steps.run-tests.conclusion == 'failure'
        run: npm run test:update-snapshots

      - name: Upload updated Playwright screenshots
        uses: actions/upload-artifact@v3
        if: failure() && steps.run-tests-update.conclusion == 'success'
        with:
          name: playwright-screenshots-updated
          path: e2e/__screenshots__/**/*.png
          retention-days: 3

  dependabot-auto-approve-and-merge:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    if: github.actor == 'dependabot[bot]'
    needs:
      - playwright-tests
      - lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/github-script@v7
        id: get_pr_data
        with:
          script: |
            return (
              await github.rest.repos.listPullRequestsAssociatedWithCommit({
                commit_sha: context.sha,
                owner: context.repo.owner,
                repo: context.repo.repo,
              })
            ).data[0];
      - name: Pull Request data
        id: pr_data
        run: echo "{pull_request_number}=${{ fromJson(steps.get_pr_data.outputs.result).number }}" >> $GITHUB_OUTPUT
      - name: Approve and Merge PR
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          bash .github/approve-and-merge.sh ${{ steps.pr_data.outputs.pull_request_number }}

And here’s a quick shell script that you can use to approve and merge the pull request based on the step results:

# approve-and-merge.sh
PR_NUMBER=$1

PR_CHECK_COMMAND=$(gh pr checks $PR_NUMBER)
# Check lines containing lint or playwright-tests and containing the word pass
lint_check=$(echo "$COMMAND" | grep -c -E '^lint.*pass' | wc -l | awk '{$1=$1};1')
playwright_tests_check=$(echo "$COMMAND" | grep -c -E '^playwright-tests.*pass' | wc -l | awk '{$1=$1};1')


# Exit code based on the result
if [ $lint_check -eq 1 ] && [ $playwright_tests_check -eq 1 ]; then
    echo "Success: lint and playwright-tests both passed."
    gh pr review $PR_NUMBER --approve
    gh pr merge $PR_NUMBER --squash
    exit 0
else
    echo "Failure: lint and/or playwright-tests did not pass."
    exit 1
fi


If you managed to get this far, I hope you found this guide helpful and that you were able to automate the process of keeping your project dependencies up to date. If you have any questions or feedback , feel free to reach out to me here !