Repository: jhipster/jhipster-lite
Pull request #11600 (2024-12-17 22:54:43)

feat(liquibase): new module for configuring a liquibase linter executed as a maven plugin
Easy setup of a [Linter for liquibase](https://liquibase-linter.github.io/liquibase-linter/)


Patch:
diff --git a/.lintstagedrc.js b/.lintstagedrc.js
index d43be1517..f33b94d46 100644
--- a/.lintstagedrc.js
+++ b/.lintstagedrc.js
@@ -2,5 +2,5 @@ export default {
   '*.{js,cjs,ts,tsx,vue}': ['eslint --fix', 'prettier --write'],
   '*.{css,scss}': ['stylelint --fix --allow-empty-input', 'prettier --write'],
   '*.pug': ['eslint --fix', 'prettier --write'],
-  '*.{md,json,yml,html,java,xml,feature,sh}': ['prettier --write'],
+  '*.{md,json*,yml,html,java,xml,feature,sh}': ['prettier --write'],
 };
diff --git a/.prettierrc b/.prettierrc
index 186ff55dd..9fe754c87 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -22,3 +22,9 @@ bracketSameLine: false
 
 # xml rules:
 xmlWhitespaceSensitivity: ignore
+
+# https://github.com/prettier/prettier/issues/15956
+overrides:
+  - files: ['*.jsonc']
+    options:
+      trailingComma: none
diff --git a/src/main/java/tech/jhipster/lite/generator/prettier/domain/PrettierModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/prettier/domain/PrettierModuleFactory.java
index 55b330b2b..d66267417 100644
--- a/src/main/java/tech/jhipster/lite/generator/prettier/domain/PrettierModuleFactory.java
+++ b/src/main/java/tech/jhipster/lite/generator/prettier/domain/PrettierModuleFactory.java
@@ -53,7 +53,7 @@ public class PrettierModuleFactory {
       .postActions()
         .add(context -> npmLazyInstaller.runInstallIn(context.projectFolder()))
         .and()
-      .preCommitActions(stagedFilesFilter("*.{md,json,yml,html,css,scss,java,xml,feature}"), preCommitCommands("['prettier --write']"))
+      .preCommitActions(stagedFilesFilter("*.{md,json*,yml,html,css,scss,java,xml,feature}"), preCommitCommands("['prettier --write']"))
       .build();
     //@formatter:on
   }
diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/application/LiquibaseApplicationService.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/application/LiquibaseApplicationService.java
index 94d52cad2..d69e790f3 100644
--- a/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/application/LiquibaseApplicationService.java
+++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/application/LiquibaseApplicationService.java
@@ -21,4 +21,8 @@ public class LiquibaseApplicationService {
   public JHipsterModule buildAsyncModule(JHipsterModuleProperties properties) {
     return factory.buildAsyncModule(properties);
   }
+
+  public JHipsterModule buildLinterModule(JHipsterModuleProperties properties) {
+    return factory.buildLinterModule(properties);
+  }
 }
diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/domain/LiquibaseModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/domain/LiquibaseModuleFactory.java
index 1db771772..36ebd0f5d 100644
--- a/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/domain/LiquibaseModuleFactory.java
+++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/domain/LiquibaseModuleFactory.java
@@ -3,12 +3,15 @@ package tech.jhipster.lite.generator.server.springboot.dbmigration.liquibase.dom
 import static tech.jhipster.lite.module.domain.JHipsterModule.artifactId;
 import static tech.jhipster.lite.module.domain.JHipsterModule.from;
 import static tech.jhipster.lite.module.domain.JHipsterModule.groupId;
+import static tech.jhipster.lite.module.domain.JHipsterModule.mavenPlugin;
 import static tech.jhipster.lite.module.domain.JHipsterModule.moduleBuilder;
+import static tech.jhipster.lite.module.domain.JHipsterModule.pluginExecution;
 import static tech.jhipster.lite.module.domain.JHipsterModule.propertyKey;
 import static tech.jhipster.lite.module.domain.JHipsterModule.propertyValue;
 import static tech.jhipster.lite.module.domain.JHipsterModule.to;
 import static tech.jhipster.lite.module.domain.JHipsterModule.toSrcMainJava;
 import static tech.jhipster.lite.module.domain.JHipsterModule.toSrcTestJava;
+import static tech.jhipster.lite.module.domain.JHipsterModule.toSrcTestResources;
 import static tech.jhipster.lite.module.domain.JHipsterModule.versionSlug;
 import static tech.jhipster.lite.module.domain.properties.SpringConfigurationFormat.PROPERTIES;
 import static tech.jhipster.lite.module.domain.properties.SpringConfigurationFormat.YAML;
@@ -16,6 +19,8 @@ import static tech.jhipster.lite.module.domain.properties.SpringConfigurationFor
 import tech.jhipster.lite.module.domain.JHipsterModule;
 import tech.jhipster.lite.module.domain.LogLevel;
 import tech.jhipster.lite.module.domain.file.JHipsterSource;
+import tech.jhipster.lite.module.domain.mavenplugin.MavenPlugin;
+import tech.jhipster.lite.module.domain.mavenplugin.MavenPlugin.MavenPluginOptionalBuilder;
 import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
 import tech.jhipster.lite.shared.error.domain.Assert;
 
@@ -76,4 +81,35 @@ public class LiquibaseModuleFactory {
       .build();
     //@formatter:on
   }
+
+  public JHipsterModule buildLinterModule(JHipsterModuleProperties properties) {
+    //@formatter:off
+    return moduleBuilder(properties)
+      .mavenPlugins()
+        .plugin(liquibaseLinterMavenPlugin().build())
+        .pluginManagement(liquibaseLinterMavenPluginManagement())
+        .and()
+      .files()
+        .add(SOURCE.append("liquibase-linter.jsonc"), toSrcTestResources().append("liquibase-linter.jsonc"))
+        .and()
+      .build();
+    //@formatter:on
+  }
+
+  private MavenPlugin liquibaseLinterMavenPluginManagement() {
+    return liquibaseLinterMavenPlugin()
+      .versionSlug("liquibase-linter-maven-plugin")
+      .configuration(
+        """
+        <changeLogFile>src/main/resources/config/liquibase/master.xml</changeLogFile>
+        <configurationFile>src/test/resources/liquibase-linter.jsonc</configurationFile>
+        """
+      )
+      .addExecution(pluginExecution().goals("lint"))
+      .build();
+  }
+
+  private static MavenPluginOptionalBuilder liquibaseLinterMavenPlugin() {
+    return mavenPlugin().groupId("io.github.liquibase-linter").artifactId("liquibase-linter-maven-plugin");
+  }
 }
diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/infrastructure/primary/LiquibaseModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/infrastructure/primary/LiquibaseModuleConfiguration.java
index 503ec62db..00d10df42 100644
--- a/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/infrastructure/primary/LiquibaseModuleConfiguration.java
+++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/infrastructure/primary/LiquibaseModuleConfiguration.java
@@ -4,7 +4,9 @@ import static tech.jhipster.lite.shared.slug.domain.JHLiteFeatureSlug.DATABASE_M
 import static tech.jhipster.lite.shared.slug.domain.JHLiteFeatureSlug.JPA_PERSISTENCE;
 import static tech.jhipster.lite.shared.slug.domain.JHLiteModuleSlug.LIQUIBASE;
 import static tech.jhipster.lite.shared.slug.domain.JHLiteModuleSlug.LIQUIBASE_ASYNC;
+import static tech.jhipster.lite.shared.slug.domain.JHLiteModuleSlug.LIQUIBASE_LINTER;
 import static tech.jhipster.lite.shared.slug.domain.JHLiteModuleSlug.LOGS_SPY;
+import static tech.jhipster.lite.shared.slug.domain.JHLiteModuleSlug.MAVEN_JAVA;
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -16,19 +18,25 @@ import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource;
 @Configuration
 class LiquibaseModuleConfiguration {
 
+  private static final String SPRING_BOOT_DATABASE_MIGRATION = "Spring Boot - Database Migration";
+
   @Bean
   JHipsterModuleResource liquibaseModule(LiquibaseApplicationService liquibase) {
     return JHipsterModuleResource.builder()
       .slug(LIQUIBASE)
       .propertiesDefinition(JHipsterModulePropertiesDefinition.builder().addIndentation().addSpringConfigurationFormat().build())
-      .apiDoc("Spring Boot - Database Migration", "Add Liquibase")
+      .apiDoc(SPRING_BOOT_DATABASE_MIGRATION, "Add Liquibase")
       .organization(
         JHipsterModuleOrganization.builder().feature(DATABASE_MIGRATION).addDependency(JPA_PERSISTENCE).addDependency(LOGS_SPY).build()
       )
-      .tags("server", "spring", "spring-boot", "database", "migration", "liquibase")
+      .tags(liquibaseTags())
       .factory(liquibase::buildModule);
   }
 
+  private String[] liquibaseTags() {
+    return new String[] { "liquibase", "database", "migration", "spring", "spring-boot" };
+  }
+
   @Bean
   JHipsterModuleResource liquibaseAsyncModule(LiquibaseApplicationService liquibase) {
     return JHipsterModuleResource.builder()
@@ -36,9 +44,20 @@ class LiquibaseModuleConfiguration {
       .propertiesDefinition(
         JHipsterModulePropertiesDefinition.builder().addIndentation().addBasePackage().addSpringConfigurationFormat().build()
       )
-      .apiDoc("Spring Boot - Database Migration", "Support updating the database asynchronously with Liquibase")
+      .apiDoc(SPRING_BOOT_DATABASE_MIGRATION, "Support updating the database asynchronously with Liquibase")
       .organization(JHipsterModuleOrganization.builder().addDependency(LIQUIBASE).build())
-      .tags("server", "spring", "spring-boot", "database", "migration", "liquibase")
+      .tags(liquibaseTags())
       .factory(liquibase::buildAsyncModule);
   }
+
+  @Bean
+  JHipsterModuleResource liquibaseLinterModule(LiquibaseApplicationService liquibase) {
+    return JHipsterModuleResource.builder()
+      .slug(LIQUIBASE_LINTER)
+      .propertiesDefinition(JHipsterModulePropertiesDefinition.EMPTY)
+      .apiDoc(SPRING_BOOT_DATABASE_MIGRATION, "Configure a linter for the Liquibase migration scripts")
+      .organization(JHipsterModuleOrganization.builder().addDependency(LIQUIBASE).addDependency(MAVEN_JAVA).build())
+      .tags("server", "database", "migration", "liquibase", "linter")
+      .factory(liquibase::buildLinterModule);
+  }
 }
diff --git a/src/main/java/tech/jhipster/lite/module/domain/JHipsterModule.java b/src/main/java/tech/jhipster/lite/module/domain/JHipsterModule.java
index 0d4f881c6..19b8d822c 100644
--- a/src/main/java/tech/jhipster/lite/module/domain/JHipsterModule.java
+++ b/src/main/java/tech/jhipster/lite/module/domain/JHipsterModule.java
@@ -237,6 +237,10 @@ public final class JHipsterModule {
     return JHipsterDestination.SRC_MAIN_RESOURCES;
   }
 
+  public static JHipsterDestination toSrcTestResources() {
+    return JHipsterDestination.SRC_TEST_RESOURCES;
+  }
+
   public static JHipsterFileMatcher filesWithExtension(String extension) {
     return path -> StringUtils.endsWithIgnoreCase(path.get(), "." + extension);
   }
diff --git a/src/main/java/tech/jhipster/lite/module/domain/file/JHipsterDestination.java b/src/main/java/tech/jhipster/lite/module/domain/file/JHipsterDestination.java
index da6aa216f..93c4153d2 100644
--- a/src/main/java/tech/jhipster/lite/module/domain/file/JHipsterDestination.java
+++ b/src/main/java/tech/jhipster/lite/module/domain/file/JHipsterDestination.java
@@ -13,6 +13,7 @@ public class JHipsterDestination {
   public static final JHipsterDestination SRC_TEST_JAVA = new JHipsterDestination("src/test/java");
   public static final JHipsterDestination SRC_MAIN_DOCKER = new JHipsterDestination("src/main/docker");
   public static final JHipsterDestination SRC_MAIN_RESOURCES = new JHipsterDestination("src/main/resources");
+  public static final JHipsterDestination SRC_TEST_RESOURCES = new JHipsterDestination("src/test/resources");
 
   private static final String MUSTACHE_EXTENSION = ".mustache";
 
diff --git a/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java b/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java
index d2d75d256..c2a49bfa4 100644
--- a/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java
+++ b/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java
@@ -78,6 +78,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
   LICENSE_MIT("license-mit"),
   LIQUIBASE("liquibase"),
   LIQUIBASE_ASYNC("liquibase-async"),
+  LIQUIBASE_LINTER("liquibase-linter"),
   LOGSTASH("logstash"),
   LOGS_SPY("logs-spy"),
   MAVEN_JAVA("maven-java"),
diff --git a/src/main/resources/generator/dependencies/pom.xml b/src/main/resources/generator/dependencies/pom.xml
index 3a4b73d6b..c50a967ac 100644
--- a/src/main/resources/generator/dependencies/pom.xml
+++ b/src/main/resources/generator/dependencies/pom.xml
@@ -40,6 +40,8 @@
     <jqassistant-context-mapper-plugin.version>2.1.0-M2</jqassistant-context-mapper-plugin.version>
     <jqassistant-jmolecules-plugin.version>2.1.0-M4</jqassistant-jmolecules-plugin.version>
     <jqassistant-spring-plugin.version>2.2.1</jqassistant-spring-plugin.version>
+    <langchain4j.version>0.36.2</langchain4j.version>
+    <jooq.version>3.19.16</jooq.version>
     <!-- Test dependencies -->
     <junit-jupiter.version>5.11.4</junit-jupiter.version>
     <assertj.version>3.26.3</assertj.version>
@@ -69,8 +71,7 @@
     <asciidoctor-maven-plugin.version>3.1.1</asciidoctor-maven-plugin.version>
     <openapi-maven-plugin.version>0.0.21</openapi-maven-plugin.version>
     <openapi-backwards-compat-maven-plugin.version>1.0.3</openapi-backwards-compat-maven-plugin.version>
-    <langchain4j.version>0.36.2</langchain4j.version>
-    <jooq.version>3.19.16</jooq.version>
+    <liquibase-linter-maven-plugin.version>0.8.0</liquibase-linter-maven-plugin.version>
   </properties>
 
   <dependencyManagement>
@@ -416,6 +417,11 @@
         <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
         <version>${langchain4j.version}</version>
       </dependency>
+      <dependency>
+        <groupId>io.github.liquibase-linter</groupId>
+        <artifactId>liquibase-linter-maven-plugin</artifactId>
+        <version>${liquibase-linter-maven-plugin.version}</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 </project>
diff --git a/src/main/resources/generator/prettier/.prettierrc.mustache b/src/main/resources/generator/prettier/.prettierrc.mustache
index 33f34cf35..cd193c274 100644
--- a/src/main/resources/generator/prettier/.prettierrc.mustache
+++ b/src/main/resources/generator/prettier/.prettierrc.mustache
@@ -21,3 +21,9 @@ bracketSameLine: false
 
 # xml rules:
 xmlWhitespaceSensitivity: ignore
+
+# https://github.com/prettier/prettier/issues/15956
+overrides:
+  - files: ['*.jsonc']
+    options:
+      trailingComma: none
diff --git a/src/main/resources/generator/server/springboot/dbmigration/liquibase/liquibase-linter.jsonc b/src/main/resources/generator/server/springboot/dbmigration/liquibase/liquibase-linter.jsonc
new file mode 100644
index 000000000..749dfdccc
--- /dev/null
+++ b/src/main/resources/generator/server/springboot/dbmigration/liquibase/liquibase-linter.jsonc
@@ -0,0 +1,5 @@
+{
+  "rules": {
+    "isolate-ddl-changes": true
+  }
+}


Test patch:
diff --git a/src/test/features/server/springboot/dbmigration/liquibase.feature b/src/test/features/server/springboot/dbmigration/liquibase.feature
index 1b306506d..fa4734fbc 100644
--- a/src/test/features/server/springboot/dbmigration/liquibase.feature
+++ b/src/test/features/server/springboot/dbmigration/liquibase.feature
@@ -11,3 +11,10 @@ Feature: Liquibase module
       | packageName | tech.jhipster.chips |
     Then I should have files in "src/main/java/tech/jhipster/chips/wire/liquibase/infrastructure/secondary"
       | AsyncSpringLiquibase.java |
+
+  Scenario: Should apply liquibase-linter module
+    When I apply "liquibase-linter" module to default project with maven file
+      | packageName | tech.jhipster.chips |
+    Then I should have "<artifactId>liquibase-linter-maven-plugin</artifactId>" in "pom.xml"
+    Then I should have files in "src/test/resources/"
+      | liquibase-linter.jsonc |
diff --git a/src/test/java/tech/jhipster/lite/generator/prettier/domain/PrettierModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/prettier/domain/PrettierModuleFactoryTest.java
index ad59ca366..8933a51fb 100644
--- a/src/test/java/tech/jhipster/lite/generator/prettier/domain/PrettierModuleFactoryTest.java
+++ b/src/test/java/tech/jhipster/lite/generator/prettier/domain/PrettierModuleFactoryTest.java
@@ -43,7 +43,7 @@ class PrettierModuleFactoryTest {
       .containing(
         """
         module.exports = {
-          '*.{md,json,yml,html,css,scss,java,xml,feature}': ['prettier --write'],
+          '*.{md,json*,yml,html,css,scss,java,xml,feature}': ['prettier --write'],
           '*.pug': ['eslint --fix', 'prettier --write'],
         };
         """
@@ -83,7 +83,7 @@ class PrettierModuleFactoryTest {
       .containing(
         """
         module.exports = {
-          '*.{md,json,yml,html,css,scss,java,xml,feature}': ['prettier --write'],
+          '*.{md,json*,yml,html,css,scss,java,xml,feature}': ['prettier --write'],
         };
         """
       );
diff --git a/src/test/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/domain/LiquibaseModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/domain/LiquibaseModuleFactoryTest.java
index 5af09a7f2..a434ab291 100644
--- a/src/test/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/domain/LiquibaseModuleFactoryTest.java
+++ b/src/test/java/tech/jhipster/lite/generator/server/springboot/dbmigration/liquibase/domain/LiquibaseModuleFactoryTest.java
@@ -117,4 +117,50 @@ class LiquibaseModuleFactoryTest {
         .notContaining("YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();");
     }
   }
+
+  @Nested
+  class LiquibaseLinterModule {
+
+    @Test
+    void shouldBuildModule() {
+      JHipsterModuleProperties properties = JHipsterModulesFixture.propertiesBuilder(tmpDirForTest())
+        .basePackage("tech.jhipster.jhlitest")
+        .build();
+
+      JHipsterModule module = factory.buildLinterModule(properties);
+
+      assertThatModuleWithFiles(module, pomFile())
+        .hasFile("pom.xml")
+        .containing(
+          """
+                <plugin>
+                  <groupId>io.github.liquibase-linter</groupId>
+                  <artifactId>liquibase-linter-maven-plugin</artifactId>
+                </plugin>
+          """
+        )
+        .containing(
+          """
+                  <plugin>
+                    <groupId>io.github.liquibase-linter</groupId>
+                    <artifactId>liquibase-linter-maven-plugin</artifactId>
+                    <version>${liquibase-linter-maven-plugin.version}</version>
+                    <executions>
+                      <execution>
+                        <goals>
+                          <goal>lint</goal>
+                        </goals>
+                      </execution>
+                    </executions>
+                    <configuration>
+                      <changeLogFile>src/main/resources/config/liquibase/master.xml</changeLogFile>
+                      <configurationFile>src/test/resources/liquibase-linter.jsonc</configurationFile>
+                    </configuration>
+                  </plugin>
+          """
+        )
+        .and()
+        .hasFile("src/test/resources/liquibase-linter.jsonc");
+    }
+  }
 }
diff --git a/tests-ci/generate.sh b/tests-ci/generate.sh
index 2e70a4050..75fc68579 100755
--- a/tests-ci/generate.sh
+++ b/tests-ci/generate.sh
@@ -168,6 +168,7 @@ elif [[ $application == 'fullapp' ]]; then
   apply_modules \
     "jpa-postgresql" \
     "liquibase" \
+    "liquibase-linter" \
     "liquibase-async"
 
   apply_modules \