Add openrewrite module: Boot 2.7 to 3.5 to 4.0 upgrade with OpenRewrite recipes, leftovers and CI gate

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Gth99spB1CzQmBFofXmU4Z
This commit is contained in:
Claude
2026-09-28 08:40:08 +00:00
parent e4f927d7af
commit b4b43b94bf
24 changed files with 559 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
# openrewrite
Companion module for the ankurm.com post *Automating javax → jakarta and Boot 3 → 4 Upgrades with OpenRewrite*.
`legacy-app/` is a small Spring Boot 2.7.18 application (JPA, validation, security, a servlet filter, `RestTemplate`, a JUnit 4 test, a `spring.factories` auto-configuration). `scripts/run-all.sh` copies it to a scratch directory outside this checkout, runs two OpenRewrite recipes in sequence, builds and tests after each, and records everything under `output/`.
| Tool | Version |
|---|---|
| rewrite-maven-plugin | 6.46.1 |
| rewrite-spring | 6.37.1 |
| rewrite-migrate-java | 3.42.1 |
| JDK / Maven | 17 / 3.9 |
```bash
export JAVA_HOME=/path/to/jdk17
./scripts/run-all.sh
```
| Output | Shows |
|---|---|
| `00-gotcha-gitignore.txt` | OpenRewrite silently skips sources that git ignores |
| `01-baseline.txt` | Legacy app builds and passes on Boot 2.7.18 |
| `02-step1-diff.txt` / `03-step1-build.txt` | `UpgradeSpringBoot_3_5`: diff and build |
| `04-step2-diff.txt` / `05-step2-build.txt` | `UpgradeSpringBoot_4_0`: diff and build |
| `06-what-it-left-behind.txt` | What the recipes did not change |
| `07-step3-boot-4.1.1.txt` | Hand bump from 4.0.x to 4.1.1, build and test |
| `08-ci-gate.txt` | `rewrite:dryRun` exit codes, including a property-name trap |
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
<artifactId>legacy-app</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
</dependencies>
<build>
<plugins>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,7 @@
package com.ankurm.legacy;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AuditAutoConfiguration {
}
@@ -0,0 +1,26 @@
package com.ankurm.legacy;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Entity
public class Customer {
@Id
@GeneratedValue
private Long id;
@NotBlank
private String name;
@Email
private String email;
protected Customer() {}
public Customer(String name, String email) { this.name = name; this.email = email; }
public Long getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
public void setName(String name) { this.name = name; }
public void setEmail(String email) { this.email = email; }
}
@@ -0,0 +1,28 @@
package com.ankurm.legacy;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/customers")
public class CustomerController {
private final CustomerRepository repository;
public CustomerController(CustomerRepository repository) { this.repository = repository; }
@PostConstruct
void seed() {
repository.save(new Customer("Ada", "[email protected]"));
}
@GetMapping
List<Customer> all() { return repository.findAll(); }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Customer create(@Valid @RequestBody Customer c) { return repository.save(c); }
}
@@ -0,0 +1,14 @@
package com.ankurm.legacy;
import javax.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.QueryHints;
import javax.persistence.QueryHint;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints(@QueryHint(name = "javax.persistence.lock.timeout", value = "3000"))
Customer findFirstByName(String name);
}
@@ -0,0 +1,11 @@
package com.ankurm.legacy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LegacyApplication {
public static void main(String[] args) {
SpringApplication.run(LegacyApplication.class, args);
}
}
@@ -0,0 +1,13 @@
package com.ankurm.legacy;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class RemoteClient {
private final RestTemplate rest = new RestTemplate();
public String fetch(String url) {
return rest.getForObject(url, String.class);
}
}
@@ -0,0 +1,19 @@
package com.ankurm.legacy;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class RequestLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
response.setHeader("X-Seen-By", "RequestLogFilter");
chain.doFilter(request, response);
}
}
@@ -0,0 +1,17 @@
package com.ankurm.legacy;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/actuator/health").permitAll()
.anyRequest().permitAll();
}
}
@@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ankurm.legacy.AuditAutoConfiguration
@@ -0,0 +1,6 @@
spring.redis.host=localhost
spring.redis.port=6379
management.metrics.export.prometheus.enabled=true
spring.jpa.hibernate.ddl-auto=create-drop
server.max-http-header-size=16KB
spring.jpa.properties.javax.persistence.validation.mode=none
@@ -0,0 +1,26 @@
package com.ankurm.legacy;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CustomerControllerTest {
@Autowired MockMvc mvc;
@Test
public void listsSeededCustomer() throws Exception {
mvc.perform(get("/customers")).andExpect(status().isOk()).andExpect(jsonPath("$[0].name").value("Ada"));
}
}
@@ -0,0 +1,3 @@
# 00-gotcha-gitignore: same recipe, same project, sources under a .gitignore'd directory
[INFO] Project [legacy-app] Parsing source files
[INFO] Applying recipes would make no changes. No patch file generated.
+3
View File
@@ -0,0 +1,3 @@
# 01-baseline: legacy app, Spring Boot 2.7.18, JDK 17
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
+173
View File
@@ -0,0 +1,173 @@
# 02-step1-diff: recipe org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5 on the legacy app (52 s)
diff -ru -x target legacy-app/pom.xml step1/pom.xml
--- legacy-app/pom.xml
+++ step1/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
- <version>2.7.18</version>
+ <version>3.5.16</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
@@ -14,6 +14,10 @@
<java.version>17</java.version>
</properties>
<dependencies>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ </dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
@@ -21,7 +25,6 @@
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
- <dependency><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
</dependencies>
<build>
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java step1/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java
--- legacy-app/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java
+++ step1/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java
@@ -1,7 +1,7 @@
package com.ankurm.legacy;
-import org.springframework.context.annotation.Configuration;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
-@Configuration
+@AutoConfiguration
public class AuditAutoConfiguration {
}
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/Customer.java step1/src/main/java/com/ankurm/legacy/Customer.java
--- legacy-app/src/main/java/com/ankurm/legacy/Customer.java
+++ step1/src/main/java/com/ankurm/legacy/Customer.java
@@ -1,10 +1,10 @@
package com.ankurm.legacy;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.Id;
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotBlank;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
@Entity
public class Customer {
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/CustomerController.java step1/src/main/java/com/ankurm/legacy/CustomerController.java
--- legacy-app/src/main/java/com/ankurm/legacy/CustomerController.java
+++ step1/src/main/java/com/ankurm/legacy/CustomerController.java
@@ -1,8 +1,8 @@
package com.ankurm.legacy;
import java.util.List;
-import javax.annotation.PostConstruct;
-import javax.validation.Valid;
+import jakarta.annotation.PostConstruct;
+import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/CustomerRepository.java step1/src/main/java/com/ankurm/legacy/CustomerRepository.java
--- legacy-app/src/main/java/com/ankurm/legacy/CustomerRepository.java
+++ step1/src/main/java/com/ankurm/legacy/CustomerRepository.java
@@ -1,10 +1,10 @@
package com.ankurm.legacy;
-import javax.persistence.LockModeType;
+import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.QueryHints;
-import javax.persistence.QueryHint;
+import jakarta.persistence.QueryHint;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/RequestLogFilter.java step1/src/main/java/com/ankurm/legacy/RequestLogFilter.java
--- legacy-app/src/main/java/com/ankurm/legacy/RequestLogFilter.java
+++ step1/src/main/java/com/ankurm/legacy/RequestLogFilter.java
@@ -1,10 +1,10 @@
package com.ankurm.legacy;
import java.io.IOException;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/SecurityConfig.java step1/src/main/java/com/ankurm/legacy/SecurityConfig.java
--- legacy-app/src/main/java/com/ankurm/legacy/SecurityConfig.java
+++ step1/src/main/java/com/ankurm/legacy/SecurityConfig.java
@@ -1,17 +1,19 @@
package com.ankurm.legacy;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
-public class SecurityConfig extends WebSecurityConfigurerAdapter {
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.csrf().disable()
- .authorizeRequests().antMatchers("/actuator/health").permitAll()
- .anyRequest().permitAll();
+public class SecurityConfig {
+ @Bean
+ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http.csrf(csrf -> csrf.disable())
+ .authorizeHttpRequests(requests -> requests.requestMatchers("/actuator/health").permitAll()
+ .anyRequest().permitAll());
+ return http.build();
}
}
Only in step1/src/main/resources/META-INF: spring
diff -ru -x target legacy-app/src/main/resources/application.properties step1/src/main/resources/application.properties
--- legacy-app/src/main/resources/application.properties
+++ step1/src/main/resources/application.properties
@@ -3,4 +3,4 @@
management.metrics.export.prometheus.enabled=true
spring.jpa.hibernate.ddl-auto=create-drop
server.max-http-header-size=16KB
-spring.jpa.properties.javax.persistence.validation.mode=none
+spring.jpa.properties.jakarta.persistence.validation.mode=none
diff -ru -x target legacy-app/src/test/java/com/ankurm/legacy/CustomerControllerTest.java step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
--- legacy-app/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
+++ step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
@@ -4,15 +4,12 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
-@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CustomerControllerTest {
Only in step1/src/main/resources/META-INF: spring
+3
View File
@@ -0,0 +1,3 @@
# 03-step1-build: after step 1, JDK 17
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
+65
View File
@@ -0,0 +1,65 @@
# 04-step2-diff: recipe org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0 applied on top of step 1 (32 s)
diff -ru -x target step1/pom.xml step2/pom.xml
--- step1/pom.xml
+++ step2/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
- <version>3.5.16</version>
+ <version>4.0.8</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
@@ -18,14 +18,23 @@
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
- <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
+ <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-restclient</artifactId>
+ </dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
- <dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-webmvc-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security-test</artifactId><scope>test</scope></dependency>
</dependencies>
<build>
<plugins>
diff -ru -x target step1/src/main/resources/application.properties step2/src/main/resources/application.properties
--- step1/src/main/resources/application.properties
+++ step2/src/main/resources/application.properties
@@ -1,6 +1,6 @@
-spring.redis.host=localhost
-spring.redis.port=6379
-management.metrics.export.prometheus.enabled=true
+spring.data.redis.host=localhost
+spring.data.redis.port=6379
+management.prometheus.metrics.export.enabled=true
spring.jpa.hibernate.ddl-auto=create-drop
-server.max-http-header-size=16KB
+server.max-http-request-header-size=16KB
spring.jpa.properties.jakarta.persistence.validation.mode=none
diff -ru -x target step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java step2/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
--- step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
+++ step2/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
@@ -6,7 +6,7 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
+3
View File
@@ -0,0 +1,3 @@
# 05-step2-build: after step 2, JDK 17
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
@@ -0,0 +1,13 @@
# 06-what-it-left-behind (in /tmp/or-work/step2, after both recipes)
--- leftover 'javax.' string literals or keys in source and resources:
src/main/java/com/ankurm/legacy/CustomerRepository.java:12: @QueryHints(@QueryHint(name = "javax.persistence.lock.timeout", value = "3000"))
--- META-INF files:
./spring.factories
./spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
--- explicit jakarta.servlet-api dependency added to the pom:
1
--- RestTemplate usages (recipe does not touch them):
src/main/java/com/ankurm/legacy/RemoteClient.java:4:import org.springframework.web.client.RestTemplate;
src/main/java/com/ankurm/legacy/RemoteClient.java:8: private final RestTemplate rest = new RestTemplate();
--- Spring Boot version the recipe stopped at:
<version>4.0.8</version>
@@ -0,0 +1,4 @@
# 07-step3-boot-4.1.1: parent bumped by hand from the recipe's 4.0.x to 4.1.1, JDK 17
<version>4.1.1</version>
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
+4
View File
@@ -0,0 +1,4 @@
# 08-ci-gate (mvn rewrite:dryRun, exit code decides the CI job)
legacy app, -Drewrite.failOnDryRunResults=true (the name you would guess): exit code 0
legacy app, -DfailOnDryRunResults=true (the real user property): exit code 1
migrated app, -DfailOnDryRunResults=true: exit code 0
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# usage: rewrite.sh <from-dir> <to-dir> <recipe-fqn> (copies from-dir to to-dir and runs the recipe there)
set -uo pipefail
RW=6.46.1; SPRING=6.37.1; MIGRATE=3.42.1
rm -rf "$2"; mkdir -p "$(dirname "$2")"; cp -r "$1" "$2"; rm -rf "$2/target"
cd "$2"
mvn -B -q org.openrewrite.maven:rewrite-maven-plugin:$RW:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:$SPRING,org.openrewrite.recipe:rewrite-migrate-java:$MIGRATE \
-Drewrite.activeRecipes="$3" -Drewrite.exportDatatables=false 2>&1
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Regenerates output/. Needs JDK 17 on JAVA_HOME (the legacy app targets 17), Maven 3.9, network access to Maven Central.
# Work happens in $WORK (default /tmp/or-work) OUTSIDE this git checkout, because OpenRewrite honours .gitignore
# and ignores files git ignores (see output/00-gotcha-gitignore.txt).
set -uo pipefail
cd "$(dirname "$0")/.."
WORK=${WORK:-/tmp/or-work}; OUT=$PWD/output; mkdir -p "$OUT"
R1=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5
R2=org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0
tests() { (cd "$1" && mvn -B test 2>&1 | grep -E "Tests run:.*Fail|BUILD|ERROR" | grep -v "Time elapsed" | head -${2:-8}); }
difff() { diff -ru -x target "$1" "$2" | sed -E 's/\t20[0-9-]+ [0-9:.]+ \+0000$//; s#'"$WORK"'/##g'; }
{ echo "# 01-baseline: legacy app, Spring Boot 2.7.18, JDK 17"; tests legacy-app; } > "$OUT/01-baseline.txt"
s=$(date +%s); ./scripts/rewrite.sh legacy-app "$WORK/step1" $R1 >/dev/null 2>&1; e=$(date +%s)
{ echo "# 02-step1-diff: recipe $R1 on the legacy app ($((e-s)) s)"; difff legacy-app "$WORK/step1"; diff -rq legacy-app "$WORK/step1" -x target | grep '^Only in' | sed "s#$WORK/##"; } > "$OUT/02-step1-diff.txt"
{ echo "# 03-step1-build: after step 1, JDK 17"; tests "$WORK/step1"; } > "$OUT/03-step1-build.txt"
s=$(date +%s); ./scripts/rewrite.sh "$WORK/step1" "$WORK/step2" $R2 >/dev/null 2>&1; e=$(date +%s)
{ echo "# 04-step2-diff: recipe $R2 applied on top of step 1 ($((e-s)) s)"; difff "$WORK/step1" "$WORK/step2"; } > "$OUT/04-step2-diff.txt"
{ echo "# 05-step2-build: after step 2, JDK 17"; tests "$WORK/step2"; } > "$OUT/05-step2-build.txt"
{
echo "# 06-what-it-left-behind (in $WORK/step2, after both recipes)"
echo "--- leftover 'javax.' string literals or keys in source and resources:"
grep -rn 'javax\.' "$WORK/step2/src" | sed "s#$WORK/step2/##" || true
echo "--- META-INF files:"; (cd "$WORK/step2/src/main/resources/META-INF" && find . -type f | sort)
echo "--- explicit jakarta.servlet-api dependency added to the pom:"; grep -c '<artifactId>jakarta.servlet-api</artifactId>' "$WORK/step2/pom.xml"
echo "--- RestTemplate usages (recipe does not touch them):"; grep -rn 'RestTemplate' "$WORK/step2/src" | sed "s#$WORK/step2/##"
echo "--- Spring Boot version the recipe stopped at:"; grep -A1 '<artifactId>spring-boot-starter-parent' "$WORK/step2/pom.xml" | grep version
} > "$OUT/06-what-it-left-behind.txt"
rm -rf "$WORK/step3"; cp -r "$WORK/step2" "$WORK/step3"; rm -rf "$WORK/step3/target"
sed -i 's#<version>4.0.[0-9]*</version>#<version>4.1.1</version>#' "$WORK/step3/pom.xml"
{ echo "# 07-step3-boot-4.1.1: parent bumped by hand from the recipe's 4.0.x to 4.1.1, JDK 17"; grep -A1 '<artifactId>spring-boot-starter-parent' "$WORK/step3/pom.xml" | grep version; tests "$WORK/step3"; } > "$OUT/07-step3-boot-4.1.1.txt"
# CI gate: dryRun with failOnDryRunResults. Fails on the legacy app, passes once migrated.
gate() { (cd "$1" && rm -rf target && mvn -B -q org.openrewrite.maven:rewrite-maven-plugin:6.46.1:dryRun \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:6.37.1,org.openrewrite.recipe:rewrite-migrate-java:3.42.1 \
-Drewrite.activeRecipes="$2" -D"$3"=true >/dev/null 2>&1; echo "exit code $?"); }
rm -rf "$WORK/g1" "$WORK/g2"; cp -r legacy-app "$WORK/g1"; cp -r "$WORK/step2" "$WORK/g2"
{ echo "# 08-ci-gate (mvn rewrite:dryRun, exit code decides the CI job)"
echo "legacy app, -Drewrite.failOnDryRunResults=true (the name you would guess): $(gate "$WORK/g1" $R1 rewrite.failOnDryRunResults)"
echo "legacy app, -DfailOnDryRunResults=true (the real user property): $(gate "$WORK/g1" $R1 failOnDryRunResults)"
echo "migrated app, -DfailOnDryRunResults=true: $(gate "$WORK/g2" $R1 failOnDryRunResults)"; } > "$OUT/08-ci-gate.txt"
{ echo "# 00-gotcha-gitignore: same recipe, same project, sources under a .gitignore'd directory"
rm -rf "$WORK/ign"; mkdir -p "$WORK/ign"; (cd "$WORK/ign" && git init -q && echo 'legacy/' > .gitignore && cp -r "$OLDPWD/legacy-app" legacy && cd legacy \
&& mvn -B org.openrewrite.maven:rewrite-maven-plugin:6.46.1:dryRun -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:6.37.1,org.openrewrite.recipe:rewrite-migrate-java:3.42.1 -Drewrite.activeRecipes=$R1 2>&1 | grep -E "Applying recipes|Parsing|patch file" )
} > "$OUT/00-gotcha-gitignore.txt"
ls -1 "$OUT"