Tech debt grows relentlessly. Even on services you don't touch, dependencies become outdated, creating a constant maintenance burden. Manually upgrading dozens of services is slow and error-prone. What if you could automate a significant part of that process?
Remember your last big Spring Boot or Java version upgrade? How did it go? Did you spend hours renaming javax
to jakarta
packages across 30 or maybe 50 different services? Or perhaps you lost a whole day figuring out why a simple dependency bump broke the build? If any of this sounds familiar, you're in the right place.
What is OpenRewrite?
OpenRewrite is an open-source refactoring engine that automates code modifications at scale, enabling consistent and reliable refactoring across large codebases and significantly reducing manual effort.
OpenRewrite works by making changes to Lossless Semantic Trees (LSTs) that represent your source code and printing the modified trees back into source code. You can then review the changes in your code and commit the results. Modifications to the LST are performed in Visitors and visitors are aggregated into Recipes. OpenRewrite recipes make minimally invasive changes to your source code that honor the original formatting.
You can check this project out on GitHub
When would you use OpenRewrite?
You might be thinking, "This sounds great in theory, but what are the real-world use cases?" Let's recall some notable migrations many of us have faced recently:
Spring Boot 2.x to 3.x migration
This migration was especially remarkably by the seemingly simple task of renaming javax.*
to jakarta.*
namespaces. However, this change had to be applied to every single service using Spring Boot. In a typical Java and Spring ecosystem, that means changing it everywhere.
OpenRewrite offers a recipe that automates this entire process.
https://docs.openrewrite.org/recipes/java/spring/boot3/upgradespringboot_3_0
JUnit 4 to JUnit 5 Migration
Imagine you need to migrate from JUnit 4 to JUnit 5, but your codebase still has a few outdated annotations scattered around. As a part of this migration you'd need to rename @BeforeClass
to @BeforeAll
or @AfterClass
to @AfterAll
.
It doesn't sound too complicated, but it's tedious work that can be fully automated with an OpenRewrite recipe.
https://docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-from-junit-4-to-junit-5
Java Version Upgrades:
Or maybe you're upgrading to Java 21 and want to replace the deprecated new URL(String)
constructor with the URI.create(String).toURL()
across your entire codebase.
There's a recipe for that too: https://docs.openrewrite.org/recipes/java/migrate/upgradetojava21
In other words, we developers are constantly challenged with keeping our dependencies up to date. OpenRewrite is here to rescue us—or at least, to significantly reduce the pain.
Our experience of using OpenRewrite
What worked for us
Let's start with what went well, as we found the tool both useful and promising.
Keeping pom.xml
in shape
In the OpenRewrite ecosystem, the magic comes from its recipes. For us, the first no-brainer was the Apache Maven best practices recipe.
It was immediately clear that we had no other tool in our stack that could consistently keep our pom.xml
files in good shape.
As a simple but welcome feature, this recipe reorders the sections of a pom.xml
to follow a standard pattern. This helps with readability, especially in large files.
But its real power lies elsewhere: the recipe can find and remove duplicate or unused dependencies, improving the long-term stability of a service.
As a note of caution, I admit it can be scary at first to accept an automated PR that removes dependencies. Make sure you have good test coverage before trusting any automated tool to this extent.
Refactoring test libraries
Of course, we wanted to see it work on actual Java code. As always, the safest place to try a new tool is on your tests, so that's exactly what we did. We were already in the process of standardizing on AssertJ, so we introduced three relevant recipes:
- org.openrewrite.java.testing.assertj.Assertj
- org.openrewrite.java.testing.mockito.MockitoBestPractices
- org.openrewrite.java.testing.testcontainers.TestContainersBestPractices We ran these without specific expectations and were pleasantly surprised when they spotted and fixed several sore spots in our test code.
What didn't work
But of course, the juicy part is always where things go wrong.
In the following paragraphs, we will look at a few examples that we were not entirely satisfied with.
Complex, custom refactoring
We tried to use a recipe to fully migrate our tests from Hamcrest to AssertJ, but it simply ignored our custom matchers.
While some recipes are more powerful than others, our general feeling is that OpenRewrite struggles with highly complex or bespoke refactorings on its own.
Running Java recipes on Kotlin projects
It may seem obvious that Java recipes should only be run on Java projects. However, like many companies, we have a mix of Java and Kotlin projects, so we simply ran the recipes against all of our team's services to see what would happen. It turns out that it partially works, but it fails in enough cases to produce strange changes and broken PRs.
This makes things tricky if you want to run a uniform set of recipes across all repositories using a tool like GitHub Actions, which we'll cover next.
Newest recipes are often commercial
This might be obvious if you're already familiar with OpenRewrite, but it's worth mentioning. If you want to use OpenRewrite for Spring Boot upgrades, you'll find that the recipe for the latest version might be under a commercial license. For example, if Spring Boot 3.5 is the latest release, the open-source recipe might only support up to version 3.4. This makes perfect sense from a business perspective, but it's something to keep in mind. In short: the OpenRewrite engine is open-source, but the most cutting-edge recipes are often licensed separately.
Our automation setup with GitHub Actions
There are a few ways to run OpenRewrite recipes. If you're using Maven, you can add the rewrite-maven-plugin
directly to your pom.xml
. This can be configured to run during your local build or only on CI.
Example:
<project>
<build>
<plugins>
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>6.18.0</version>
<configuration>
<exportDatatables>true</exportDatatables>
<activeRecipes>
<recipe>org.openrewrite.staticanalysis.JavaApiBestPractices</recipe>
</activeRecipes>
</configuration>
<dependencies>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-static-analysis</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
Alternatively, you can run the plugin directly from the command line:
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-static-analysis:RELEASE -Drewrite.activeRecipes=org.openrewrite.staticanalysis.JavaApiBestPractices -Drewrite.exportDatatables=true
This gives you the flexibility to run it manually, as part of a CI pipeline, or on a nightly schedule.
We chose the third option: run it via a scheduled GitHub Action on a daily basis and automatically create a PR if any changes are detected.
Why not just use the Maven plugin?
The main drawback of adding the plugin to your pom.xml
is that it significantly slows down every build, even when there are no changes to be made. You could run it as a CI-only check, but that creates a frustrating workflow: the CI build would fail, and a developer would have to run the command locally to generate the changes and push another commit. This kind of friction hurts tool adoption.
Running OpenRewrite on a schedule mimics the behavior of Dependabot or Renovate. Developers don't have to actively run anything; they simply review and merge the auto-generated PRs. Ideally, only a small portion of these PRs will have failures.
Another benefit of the GitHub Action approach is centralization. We can update the recipes in a single, shared workflow file and have that change apply to all our repositories without touching a single pom.xml
. And since the result is always a PR and not a direct commit, it's a completely safe operation.
The GitHub workflow in detail
Imagine you have 20 repositories. Modifying the pom.xml
in every one of them just to add or change a recipe would be painful and would quickly lead to abandoning the tool. With a centralized GitHub Action, however, each repository only needs a small trigger file:
name: OpenRewrite Scheduled PR
on:
schedule:
- cron: '0 7 * * MON-FRI'
workflow_dispatch: # Allows manual triggering
jobs:
call-openrewrite-workflow:
uses: your-organisation/your-repository/.github/workflows/reusable-openrewrite-auto-pr.yml@main
secrets: inherit
This file references a reusable workflow, which contains the actual logic and OpenRewrite configuration. Now, whenever we add or remove a recipe, we only modify the central workflow, and all repositories pick up the change on their next scheduled run.
name: Reusable OpenRewrite Auto PR Workflow
on:
workflow_call:
inputs:
ADDITIONAL_MVN_COMMAND_TO_APPLY:
description: 'Additional OpenRewrite command to apply'
required: false
type: string
default: ''
env:
PR_BRANCH_NAME: openrewrite/auto-improvements
jobs:
check-branch:
name: Check if branch exists
runs-on: ...
outputs:
should_run: ${{ steps.check_branch.outputs.should_run }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if branch exists
id: check_branch
run: |
if git ls-remote --heads origin ${{ env.PR_BRANCH_NAME }} | grep -q ${{ env.PR_BRANCH_NAME }}; then
echo "Branch ${{ env.PR_BRANCH_NAME }} already exists. Skipping workflow."
echo "should_run=false" >> $GITHUB_OUTPUT
else
echo "Branch does not exist. Proceeding with workflow."
echo "should_run=true" >> $GITHUB_OUTPUT
fi
openrewrite:
name: Apply OpenRewrite recommendations
needs: check-branch
if: needs.check-branch.outputs.should_run == 'true'
runs-on: ...
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
cache: 'maven'
settings-path: ${{ github.workspace }}
- name: Run OpenRewrite via Maven
run: |
./mvnw --batch-mode -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-testing-frameworks:RELEASE \
-Drewrite.activeRecipes=org.openrewrite.staticanalysis.JavaApiBestPractices,org.openrewrite.maven.BestPractices
${{ inputs.ADDITIONAL_MVN_COMMAND_TO_APPLY }}
- name: Create Pull Request
id: create_pr
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "refactor: apply OpenRewrite recommendations"
title: "refactor: apply OpenRewrite recommendations"
add-paths: |
.
:!settings.xml
:!toolchains.xml
body: |
This Pull Request was automatically generated by OpenRewrite.
## Changes Applied
The following recipes were applied:
- `org.openrewrite.maven.BestPractices` (Maven best practices)
- `org.openrewrite.staticanalysis.JavaApiBestPractices` (Java API best practices)
${{ inputs.OPENREWRITE_RECIPES_TO_APPLY }}
Please review the changes and merge if acceptable.
branch: ${{ env.PR_BRANCH_NAME }}
delete-branch: true
labels: |
openrewrite
automated-pr
draft: false
- name: PR Status
run: |
echo "Pull request creation status: ${{ steps.create_pr.outputs.pull-request-operation }}"
echo "Pull request number: ${{ steps.create_pr.outputs.pull-request-number }}"
Note:
Several unrelated and infrastructure-specific steps have been removed from the GitHub workflow described above.
Below you can see the steps of the GitHub workflow:
The main steps are:
- Run OpenRewrite via Maven
- Create Pull Request
This approach has proven to be robust; we've already added new recipes and removed old ones, confirming it works well in different scenarios. You can scope a shared GitHub Action to a team or an entire organization and still provide repository-specific overrides using environment variables. This allows the shared workflow to be as complex as necessary, as long as it remains maintainable.
Our results
- We ran 5 distinct recipes on a schedule across our team's repositories.
- The workflow was rolled out to 14 services.
- In a short time, we have already merged over 40 automated PRs generated by this system.
Final thoughts
In this article, we covered the pain points OpenRewrite solves, what worked (and didn't work) for us, and how we use the open-source version with GitHub Actions to run recipes automatically across all our repositories.
So far, our experience with OpenRewrite has been very positive. It has filled a crucial gap in our toolkit, especially for keeping our Maven pom.xml
files clean and consistent.
Check out the OpenRewrite documentation to find a recipe that fits your needs, and feel free to use our GitHub workflow as inspiration for your own automation.
Top comments (0)