<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thoughtbot="https://thoughtbot.com/feeds/">
  <title>Giant Robots Smashing Into Other Giant Robots</title>
  <subtitle>Written by thoughtbot, your expert partner for design and development.
</subtitle>
  <id>https://robots.thoughtbot.com/</id>
  <link href="https://thoughtbot.com/blog"/>
  <link href="https://feed.thoughtbot.com" rel="self"/>
  <updated>2026-09-09T00:00:00+00:00</updated>
  <author>
    <name>thoughtbot</name>
  </author>
<entry>
  <title>Announcing importmap-update: automated dependency updates for importmap-rails</title>
  <link rel="alternate" href="https://thoughtbot.com/blog/announcing-importmap-update-automated-dependency-updates-for-importmap-rails"/>
  <author>
    <name>Neil Carvalho</name>
  </author>
  <id>https://thoughtbot.com/blog/announcing-importmap-update-automated-dependency-updates-for-importmap-rails</id>
  <published>2026-09-09T00:00:00+00:00</published>
  <updated>2026-09-08T12:49:07Z</updated>
  <content type="html">&lt;p&gt;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 &lt;a href="https://github.com/rails/importmap-rails"&gt;importmap-rails&lt;/a&gt;, and the
&lt;a href="https://github.com/dependabot/dependabot-core/issues/6675"&gt;feature request&lt;/a&gt; for that has been open since February 2023.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/marketplace/actions/importmap-update"&gt;importmap-update&lt;/a&gt; is a GitHub Action that closes that gap. It runs
&lt;code&gt;bin/importmap outdated&lt;/code&gt; and &lt;code&gt;bin/importmap audit&lt;/code&gt;, opens pull requests for what
it finds, and keeps those pull requests in sync on every run after that.&lt;/p&gt;
&lt;h2 id="why-importmaps-get-left-behind"&gt;
  
    Why importmaps get left behind
  
&lt;/h2&gt;

&lt;p&gt;An importmap-rails app records its JavaScript dependencies in
&lt;code&gt;config/importmap.rb&lt;/code&gt;. Running &lt;code&gt;bin/importmap pin react&lt;/code&gt; resolves the package
through a CDN like JSPM, downloads it into &lt;code&gt;vendor/javascript&lt;/code&gt;, and writes a
one-line pin with the version in a trailing comment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;pin&lt;/span&gt; &lt;span class="s2"&gt;"react"&lt;/span&gt; &lt;span class="c1"&gt;# @19.1.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;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
&lt;code&gt;package.json&lt;/code&gt; and no lockfile for Dependabot to read. The information it would
want is available, just somewhere else: &lt;code&gt;bin/importmap outdated&lt;/code&gt; compares your
pins against the npm registry, and &lt;code&gt;bin/importmap audit&lt;/code&gt; checks them for known
advisories.&lt;/p&gt;

&lt;p&gt;Half of that is already wired up for you. Since Rails 7.2, new apps are
generated with a &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; that includes a &lt;code&gt;scan_js&lt;/code&gt; job:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Scan for security vulnerabilities in JavaScript dependencies&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bin/importmap audit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Nothing runs &lt;code&gt;bin/importmap outdated&lt;/code&gt;. Ordinary version drift stays invisible
until it turns into an advisory, and by then you’re upgrading in a hurry.&lt;/p&gt;
&lt;h2 id="getting-started"&gt;
  
    Getting started
  
&lt;/h2&gt;

&lt;p&gt;Allow GitHub Actions to create pull requests in your repository settings
(Actions &amp;gt; General &amp;gt; Workflow permissions), then add a workflow:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/importmap-updates.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Importmap updates&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;   &lt;span class="c1"&gt;# Mondays 09:00 UTC&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;thoughtbot/importmap-update@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s the whole setup. Configuration is optional, and the defaults match what
you’d probably configure anyway.&lt;/p&gt;
&lt;h2 id="grouping"&gt;
  
    Grouping
  
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;.github/importmap-updates.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="na"&gt;grouping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;individual&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# one PR per vulnerable package&lt;/span&gt;
  &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;grouped&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;      &lt;span class="c1"&gt;# one PR for all patch bumps&lt;/span&gt;
  &lt;span class="na"&gt;minor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;grouped&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;      &lt;span class="c1"&gt;# one PR for all minor bumps&lt;/span&gt;
  &lt;span class="na"&gt;major&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;individual&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# one PR per major bump&lt;/span&gt;

&lt;span class="na"&gt;open_pull_requests_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;javascript&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;importmap&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;branch_prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;importmap-updates"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Every field is optional. Both &lt;code&gt;open_pull_requests_limit&lt;/code&gt; and
&lt;code&gt;open-pull-requests-limit&lt;/code&gt; work, which saves you a round of debugging if you’re
copying from a Dependabot config.&lt;/p&gt;
&lt;h2 id="security-updates-are-handled-differently"&gt;
  
    Security updates are handled differently
  
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Security pull requests are also exempt from &lt;code&gt;open_pull_requests_limit&lt;/code&gt;. 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.&lt;/p&gt;
&lt;h2 id="what-happens-on-the-next-run"&gt;
  
    What happens on the next run
  
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id="try-it-in-dry-run-mode-first"&gt;
  
    Try it in dry-run mode first
  
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;dry-run: "true"&lt;/code&gt; 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.&lt;/p&gt;
&lt;h2 id="alternatives"&gt;
  
    Alternatives
  
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://depfu.com"&gt;Depfu&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;Issues and pull requests are welcome on &lt;a href="https://github.com/thoughtbot/importmap-update"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;aside class="related-articles"&gt;&lt;h2&gt;If you enjoyed this post, you might also like:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://thoughtbot.com/blog/github-com-thoughtbot-cleanup"&gt;github.com/thoughtbot cleanup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thoughtbot.com/blog/this-week-in-open-source-2"&gt;This Week in Open Source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thoughtbot.com/blog/this-week-in-open-source-3"&gt;This week in open source&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/aside&gt;
</content>
  <summary>Dependabot keeps your gems current, but it doesn't know about the JavaScript packages pinned in your Rails importmap. We're releasing a GitHub Action that opens pull requests for the ones that are outdated or vulnerable.</summary>
  <thoughtbot:auto_social_share>true</thoughtbot:auto_social_share>
</entry>
</feed>
