Announcing importmap-update: automated dependency updates for importmap-rails

https://thoughtbot.com/blog/announcing-importmap-update-automated-dependency-updates-for-importmap-rails

Dependabot helps you keep your gems updated, and it also supports other package managers such as NPM. However, it doesn’t know anything about the JavaScript packages in a Rails app that uses importmap-rails, and the feature request for that has been open since February 2023.

That means that when you dropped Node from your Rails app, you also dropped the tooling that would tell you a pinned package had fallen behind or picked up a published advisory.

importmap-update is a GitHub Action that closes that gap. It runs bin/importmap outdated and bin/importmap audit, opens pull requests for what it finds, and keeps those pull requests in sync on every run after that.

Why importmaps get left behind

An importmap-rails app records its JavaScript dependencies in config/importmap.rb. Running bin/importmap pin react resolves the package through a CDN like JSPM, downloads it into vendor/javascript, and writes a one-line pin with the version in a trailing comment:

pin "react" # @19.1.0

Your own asset pipeline serves the vendored copy from there. What lands in source control is that comment and the JavaScript file itself, so there’s no package.json and no lockfile for Dependabot to read. The information it would want is available, just somewhere else: bin/importmap outdated compares your pins against the npm registry, and bin/importmap audit checks them for known advisories.

Half of that is already wired up for you. Since Rails 7.2, new apps are generated with a .github/workflows/ci.yml that includes a scan_js job:

- name: Scan for security vulnerabilities in JavaScript dependencies
  run: bin/importmap audit

A published advisory against one of your pins will fail the build, which is the part you’d want to hear about first. It still stops at telling you, though. Someone has to look up the fixed version and pin it by hand.

Nothing runs bin/importmap outdated. Ordinary version drift stays invisible until it turns into an advisory, and by then you’re upgrading in a hurry.

Getting started

Allow GitHub Actions to create pull requests in your repository settings (Actions > General > Workflow permissions), then add a workflow:

# .github/workflows/importmap-updates.yml
name: Importmap updates
on:
  schedule:
    - cron: "0 9 * * 1"   # Mondays 09:00 UTC
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - uses: thoughtbot/importmap-update@v1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

That’s the whole setup. Configuration is optional, and the defaults match what you’d probably configure anyway.

Grouping

Opening a pull request for every outdated package gets noisy fast, and rolling all of them into one pull request makes for a diff nobody enjoys reviewing. The action splits updates into security, patch, minor, and major buckets, and you choose a strategy for each one in .github/importmap-updates.yml:

version: 1

grouping:
  security: { strategy: individual }   # one PR per vulnerable package
  patch:    { strategy: grouped }      # one PR for all patch bumps
  minor:    { strategy: grouped }      # one PR for all minor bumps
  major:    { strategy: individual }   # one PR per major bump

open_pull_requests_limit: 10
labels: [dependencies, javascript, importmap]
branch_prefix: "importmap-updates"

Every field is optional. Both open_pull_requests_limit and open-pull-requests-limit work, which saves you a round of debugging if you’re copying from a Dependabot config.

Security updates are handled differently

A vulnerable package becomes a security pull request even when the fix happens to be a major version bump. The body still records the bump kind, so reviewers know to expect breaking changes before they open the diff.

Security pull requests are also exempt from open_pull_requests_limit. When the budget is tight, we fill the remaining slots with major bumps first, then minor, then patch, since the bigger bumps are the ones somebody needs to set aside time for.

What happens on the next run

An open pull request goes stale as soon as another version ships. On each run, the action brings its own pull requests back in line with what’s actually outdated: the ones whose versions have moved get updated in place, and the ones whose packages aren’t outdated anymore get closed with a comment saying why. Pull requests it didn’t open are left alone, even on a branch matching your configured prefix.

Try it in dry-run mode first

Set dry-run: "true" and the action runs end to end without side effects. It reads your importmap, builds the plan, compares it against your open pull requests, and logs every operation it would have performed. That’s worth doing on a repository whose JavaScript dependencies haven’t been looked at in a while, before it opens 10 pull requests at once.

Alternatives

Depfu is the only one I could find. It has supported import maps since 2023, it’s free for public repos, and it’s a paid hosted service for private ones. If you’d rather have one service watching your gems and your JavaScript together, it’s worth a look.

Issues and pull requests are welcome on GitHub.

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.