<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Johan Lindell</title>
    <description>The latest articles on DEV Community by Johan Lindell (@lindell).</description>
    <link>https://dev.to/lindell</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F565689%2Fbba68c04-f87c-4ca6-8de2-b3c5e0a176ae.jpeg</url>
      <title>DEV Community: Johan Lindell</title>
      <link>https://dev.to/lindell</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lindell"/>
    <language>en</language>
    <item>
      <title>Bulk-Migrate GitHub Actions from Node 20 to 24</title>
      <dc:creator>Johan Lindell</dc:creator>
      <pubDate>Thu, 15 Jan 2026 17:08:36 +0000</pubDate>
      <link>https://dev.to/lindell/bulk-migrate-github-actions-from-node-20-to-24-3hm5</link>
      <guid>https://dev.to/lindell/bulk-migrate-github-actions-from-node-20-to-24-3hm5</guid>
      <description>&lt;p&gt;If you manage a GitHub organization, you know the drill: GitHub announces a Node.js version deprecation, and suddenly your inbox is full of warnings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/" rel="noopener noreferrer"&gt;With Node 20 hitting maintenance mode&lt;/a&gt;, you likely have dozens of &lt;code&gt;.github/workflows&lt;/code&gt; files that need a version bump. You &lt;em&gt;could&lt;/em&gt; clone each repo, edit the YAML, commit, push, and open a PR... fifty times.&lt;/p&gt;

&lt;p&gt;Or, you can do it all in one go with &lt;strong&gt;&lt;a href="https://github.com/lindell/multi-gitter" rel="noopener noreferrer"&gt;multi-gitter&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One-Shot Fix
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;multi-gitter&lt;/code&gt; is an open-source tool that allows you to run a script contextually inside every repository in your organization. It handles the cloning, branching, committing, and PR creation automatically.&lt;/p&gt;

&lt;p&gt;Here is how to bump your Node versions across your entire org in under 3 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Script
&lt;/h3&gt;

&lt;p&gt;First, create a simple script called &lt;code&gt;bump-node.sh&lt;/code&gt;. This script will run inside each repository. We'll use &lt;code&gt;sed&lt;/code&gt; to find and replace the version number in your workflow files.&lt;/p&gt;

&lt;p&gt;This script handles the syntax differences between macOS (BSD sed) and Linux (GNU sed) automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;TARGET_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;".github/workflows"&lt;/span&gt;
&lt;span class="nv"&gt;SEARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"node-version: 20"&lt;/span&gt;
&lt;span class="nv"&gt;REPLACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"node-version: 24"&lt;/span&gt;

&lt;span class="c"&gt;# If the directory doesn't exist, stop here.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Find YAML files and replace the version string&lt;/span&gt;
&lt;span class="c"&gt;# We use a conditional check to ensure this works on both macOS and Linux&lt;/span&gt;
&lt;span class="c"&gt;# "sed --version" returns 0 on GNU (Linux), but fails on BSD (macOS).&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="c"&gt;# Linux (standard GNU sed)&lt;/span&gt;
    find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.yml"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.yaml"&lt;/span&gt; | xargs &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"s/&lt;/span&gt;&lt;span class="nv"&gt;$SEARCH&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$REPLACE&lt;/span&gt;&lt;span class="s2"&gt;/g"&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="c"&gt;# macOS (BSD sed)&lt;/span&gt;
    find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.yml"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.yaml"&lt;/span&gt; | xargs &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="s2"&gt;"s/&lt;/span&gt;&lt;span class="nv"&gt;$SEARCH&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$REPLACE&lt;/span&gt;&lt;span class="s2"&gt;/g"&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to also give it execution permission with &lt;code&gt;chmod +x ./bump-node.sh&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Command
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;multi-gitter&lt;/code&gt; to execute this script across your organization (e.g., &lt;code&gt;my-company-org&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multi-gitter run ./bump-node.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--org&lt;/span&gt; my-company-org &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--branch&lt;/span&gt; upgrade-to-node-24 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pr-title&lt;/span&gt; &lt;span class="s2"&gt;"Upgrade node version from 20 to 24"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pr-body&lt;/span&gt; &lt;span class="s2"&gt;"Bumping node version to 24 in GitHub action due to deprecation."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Result
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;multi-gitter&lt;/code&gt; will iterate through your repositories. If the script makes a change (i.e., it finds a file to update), the tool will automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fork/Clone the repo.&lt;/li&gt;
&lt;li&gt;Push the changes to the &lt;code&gt;upgrade-to-node-24&lt;/code&gt; branch.&lt;/li&gt;
&lt;li&gt;Open a Pull Request with your description.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can even use the &lt;code&gt;--interactive&lt;/code&gt; flag to review the changes in your terminal before the PRs are created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Maintenance tasks like version bumps shouldn't eat up your sprint. By treating your entire GitHub organization as a single monorepo for these changes, you save hours of context switching.&lt;/p&gt;

&lt;p&gt;Check out &lt;strong&gt;&lt;a href="https://github.com/lindell/multi-gitter" rel="noopener noreferrer"&gt;multi-gitter on GitHub&lt;/a&gt;&lt;/strong&gt; to install it and see more recipes for bulk-managing your repositories.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>github</category>
      <category>node</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Make bulk changes over multiple repositories</title>
      <dc:creator>Johan Lindell</dc:creator>
      <pubDate>Tue, 26 Jan 2021 06:57:16 +0000</pubDate>
      <link>https://dev.to/lindell/make-bulk-changes-over-multiple-repositories-27i9</link>
      <guid>https://dev.to/lindell/make-bulk-changes-over-multiple-repositories-27i9</guid>
      <description>&lt;p&gt;With the increasing popularity of microservice architecture and with many organizations having plenty of internal libraries, it's easy to come to a state where keeping all repositories in sync might be hard. Some have opted to move to monorepos, which have some &lt;a href="https://dev.to/maxpou/monorepo-is-it-worth-jumping-the-bandwagon-5dh1"&gt;advantages, but also disadvantages&lt;/a&gt; which makes many organizations not willing to, or unable to adopt this approach.&lt;/p&gt;

&lt;p&gt;One of the biggest problems with keeping separate repositories might be that smaller changes, such as a dependency update, or a change in a file like the &lt;a href="https://docs.github.com/en/github/building-a-strong-community/creating-a-pull-request-template-for-your-repository" rel="noopener noreferrer"&gt;pull request template&lt;/a&gt; might require a lot of tedious manual labor. But this should not have to be the case, we should be able to automate these tasks, even if they are spread out over multiple repositories.&lt;/p&gt;

&lt;h1&gt;
  
  
  Introducing multi-gitter
&lt;/h1&gt;

&lt;p&gt;The solution to the problem of making similar changes over multiple repositories is &lt;a href="https://github.com/lindell/multi-gitter" rel="noopener noreferrer"&gt;multi-gitter&lt;/a&gt;. It allows you to make changes by running a script or program, in the context of all repositories in your GitHub/GitLab organization, or in a list of repositories. For all repositories where a change was made, a pull request will be created. All pull requests can then be verified to be correct by a &lt;a href="https://en.wikipedia.org/wiki/Continuous_integration" rel="noopener noreferrer"&gt;Continuous integration&lt;/a&gt; pipeline and then potentially merged with another part of the tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Script it once, run it everywhere
&lt;/h3&gt;

&lt;p&gt;The script can do any type of change. As long as you can script something for one repository, you can do the change for any number of repositories. &lt;/p&gt;

&lt;p&gt;Example run, replacing apples with oranges using bash:&lt;br&gt;
&lt;a href="https://github.com/lindell/multi-gitter" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Faot9itxztps6knuk5v3a.gif" alt="multi-gitter demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Your changes, your tooling
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/lindell/multi-gitter" rel="noopener noreferrer"&gt;multi-gitter&lt;/a&gt; is agnostic to the tool you use to make these changes. A simple bash script might be enough for some of the times. But if you prefer to modify your file with a Node.js or Python script, that works excellently too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Replace the the pull request template file (if it exist)
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Absolut path to the file that should replace the one in the repo&lt;/span&gt;
&lt;span class="nv"&gt;REPLACE_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/test/pull_request_template.md
&lt;span class="c"&gt;# Relative from any repos root&lt;/span&gt;
&lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.github/pull_request_template.md

&lt;span class="c"&gt;# Don't replace this file if it does not already exist in the repo&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nv"&gt;$REPLACE_FILE&lt;/span&gt; &lt;span class="nv"&gt;$FILE&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Run the script with multi-gitter:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

multi-gitter run ./replace.sh &lt;span class="nt"&gt;-O&lt;/span&gt; my-org &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"update pr-template"&lt;/span&gt; &lt;span class="nt"&gt;-B&lt;/span&gt; update-pr-template


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Example: Replace text in a file with Node.js
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;promises&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./README.md&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apple&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./README.md&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Run the script with multi-gitter:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;p&gt;multi-gitter run &lt;span class="s2"&gt;"node &lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/script.js"&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; my-org &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"replace apples with oranges"&lt;/span&gt; &lt;span class="nt"&gt;-B&lt;/span&gt; fruit-replace&lt;/p&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Many ways to run&lt;br&gt;
&lt;/h1&gt;

&lt;p&gt;Since any type of script can be run to modify the repositories, multi-gitter does also need to accommodate for many ways of running that code, which it does. It does allow you to run the script in any collection of repositories, like everything in an organization, by specifying a list of repositories, all repositories owned by a user, or a combination of these. If you want to speed up the process, you can make the run parallel, or you might want to specify reviewers of the created pull requests if needed.&lt;/p&gt;

&lt;p&gt;One of the most common extra flags used is the &lt;code&gt;--dry-run&lt;/code&gt; flag. With it, you can try out the script, without actually making any changes. And by setting the log-level to be a bit more verbose with &lt;code&gt;--log-level=debug&lt;/code&gt; you can see every single line that would have been changed as a git diff.&lt;/p&gt;

&lt;h1&gt;
  
  
  Try it for yourself
&lt;/h1&gt;

&lt;p&gt;The best way to understand any tool is to play with it out yourself. You can &lt;a href="https://github.com/lindell/multi-gitter" rel="noopener noreferrer"&gt;find multi-gitter here&lt;/a&gt; to try it out. Maybe even help others by &lt;a href="https://github.com/lindell/multi-gitter#example-scripts" rel="noopener noreferrer"&gt;adding an example script &lt;/a&gt; that was useful to you.&lt;/p&gt;

</description>
      <category>github</category>
      <category>opensource</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
