From 626788cdacd6177ce21b51e46291922f3fd6451f Mon Sep 17 00:00:00 2001 From: Gunnar Velle Date: Tue, 16 Dec 2025 13:17:00 +0100 Subject: [PATCH 1/7] Stop using test dependency in application code. --- pom.xml | 1 + src/main/java/no/ndla/taxonomy/domain/ResourceType.java | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 041b001d..fe92e78b 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,7 @@ org.testcontainers testcontainers-postgresql ${testcontainers.version} + test diff --git a/src/main/java/no/ndla/taxonomy/domain/ResourceType.java b/src/main/java/no/ndla/taxonomy/domain/ResourceType.java index 6ce94d2e..dd0588c2 100644 --- a/src/main/java/no/ndla/taxonomy/domain/ResourceType.java +++ b/src/main/java/no/ndla/taxonomy/domain/ResourceType.java @@ -13,7 +13,6 @@ import java.util.*; import java.util.stream.Collectors; import org.hibernate.annotations.Type; -import org.jetbrains.annotations.NotNull; @Entity public class ResourceType extends DomainObject implements Comparable { @@ -88,7 +87,7 @@ void preRemove() { } @Override - public int compareTo(@NotNull ResourceType o) { + public int compareTo(ResourceType o) { if (this.order == -1 || o.order == -1) { return this.getPublicId().compareTo(o.getPublicId()); } From 251a184c28c5743f3a445e7c0c6430ac171c5207 Mon Sep 17 00:00:00 2001 From: Gunnar Velle Date: Tue, 16 Dec 2025 13:18:47 +0100 Subject: [PATCH 2/7] Mill build --- .gitignore | 4 + build.mill | 82 +++++++++++++++++ mill | 181 ++++++++++++++++++++++++++++++++++++++ mill-build/src/Deps.scala | 61 +++++++++++++ 4 files changed, 328 insertions(+) create mode 100644 build.mill create mode 100755 mill create mode 100644 mill-build/src/Deps.scala diff --git a/.gitignore b/.gitignore index c1e614e1..419f039b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,12 @@ target *.backup .env +.env.* .yarn/ +# Mill +out/ + # Elastic Beanstalk Files .elasticbeanstalk/* !.elasticbeanstalk/*.cfg.yml diff --git a/build.mill b/build.mill new file mode 100644 index 00000000..88053254 --- /dev/null +++ b/build.mill @@ -0,0 +1,82 @@ +//| mill-version: 1.1.0-RC3 +//| mill-jvm-version: temurin:21 +package build + +import mill.javalib.* +import mill.javalib.publish.* +import mill.javalib.spring.boot.SpringBootModule +import millbuild.* + +object `package` extends SpringBootModule, MavenModule, PublishModule { + def jvmId = "temurin:21" + def javacOptions = super.javacOptions() ++ Seq("-parameters") + override def springBootPlatformVersion = "3.5.8" + + def mvnDeps = super.mvnDeps() ++ Seq( + Deps.springBootStarter, + Deps.springBootStarterWeb, + Deps.springBootStarterValidation, + Deps.springBootConfigurationProcessor, + Deps.springdocOpenapiStarterWebmvcApi, + Deps.postgresql, + Deps.commonsCompress, + Deps.springBootStarterDataJpa, + Deps.springBootDevtools, + Deps.springSecurityWeb, + Deps.springBootStarterSecurity, + Deps.commonsLang3, + Deps.commonsCodec, + Deps.logbackCore, + Deps.logbackJsonClassic, + Deps.logbackJackson, + Deps.javaJwt, + Deps.jwksRsa, + Deps.liquibaseCore, + Deps.preliquibaseSpringBootStarter, + Deps.javamelodySpringBootStarter, + Deps.springBootStarterActuator, + Deps.micrometerRegistryPrometheus, + Deps.hypersistenceUtilsHibernate63, + Deps.jacksonModuleJakartaXmlbindAnnotations, + Deps.jacksonDatatypeJdk8, + Deps.jsoup + ) + + object test extends SpringBootTestsModule, MavenTests, TestModule.Junit5 { + def javacOptions = super.javacOptions() ++ Seq("-parameters") + + def mvnDeps = super.mvnDeps() ++ Seq( + Deps.springBootStarterTest, + Deps.testcontainersJunitJupiter, + Deps.testcontainersPostgresql + ) + } + + + def artifactName = "taxonomy-api" + + def pomParentProject = Some( + Artifact("org.springframework.boot", "spring-boot-starter-parent", "3.5.8") + ) + + def pomSettings = PomSettings( + "NDLA taxonomy", + "no.ndla.taxonomy", + "", + Seq(), + VersionControl(None, None, None, None), + Seq() + ) + + def publishVersion = "0.0.1-SNAPSHOT" + + def publishProperties = super.publishProperties() ++ Map( + ("project.build.sourceEncoding", "UTF-8"), + ("testcontainers.version", "2.0.2"), + ("spotless.version", "3.0.0"), + ("javamelody.version", "2.5.0"), + ("project.reporting.outputEncoding", "UTF-8"), + ("java.version", "21") + ) + +} diff --git a/mill b/mill new file mode 100755 index 00000000..509aca3f --- /dev/null +++ b/mill @@ -0,0 +1,181 @@ +#!/usr/bin/env sh + +set -e + +if [ -z "${DEFAULT_MILL_VERSION}" ] ; then DEFAULT_MILL_VERSION="1.1.0-RC3"; fi + +if [ -z "${GITHUB_RELEASE_CDN}" ] ; then GITHUB_RELEASE_CDN=""; fi + +if [ -z "$MILL_MAIN_CLI" ] ; then MILL_MAIN_CLI="${0}"; fi + +MILL_REPO_URL="https://github.com/com-lihaoyi/mill" + +MILL_BUILD_SCRIPT="" + +if [ -f "build.mill" ] ; then + MILL_BUILD_SCRIPT="build.mill" +elif [ -f "build.mill.scala" ] ; then + MILL_BUILD_SCRIPT="build.mill.scala" +elif [ -f "build.sc" ] ; then + MILL_BUILD_SCRIPT="build.sc" +fi + +# `s/.*://`: +# This is a greedy match that removes everything from the beginning of the line up to (and including) the last +# colon (:). This effectively isolates the value part of the declaration. +# +# `s/#.*//`: +# This removes any comments at the end of the line. +# +# `s/['\"]//g`: +# This removes all single and double quotes from the string, wherever they appear (g is for "global"). +# +# `s/^[[:space:]]*//; s/[[:space:]]*$//`: +# These two expressions trim any leading or trailing whitespace ([[:space:]] matches spaces and tabs). +TRIM_VALUE_SED="s/.*://; s/#.*//; s/['\"]//g; s/^[[:space:]]*//; s/[[:space:]]*$//" + +if [ -z "${MILL_VERSION}" ] ; then + if [ -f ".mill-version" ] ; then + MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)" + elif [ -f ".config/mill-version" ] ; then + MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)" + elif [ -f "build.mill.yaml" ] ; then + MILL_VERSION="$(grep -E "mill-version:" "build.mill.yaml" | sed -E "$TRIM_VALUE_SED")" + elif [ -n "${MILL_BUILD_SCRIPT}" ] ; then + MILL_VERSION="$(grep -E "//\|.*mill-version" "${MILL_BUILD_SCRIPT}" | sed -E "$TRIM_VALUE_SED")" + fi +fi + +if [ -z "${MILL_VERSION}" ] ; then MILL_VERSION="${DEFAULT_MILL_VERSION}"; fi + +MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill" + +if [ -z "${MILL_FINAL_DOWNLOAD_FOLDER}" ] ; then MILL_FINAL_DOWNLOAD_FOLDER="${MILL_USER_CACHE_DIR}/download"; fi + +MILL_NATIVE_SUFFIX="-native" +MILL_JVM_SUFFIX="-jvm" +FULL_MILL_VERSION=$MILL_VERSION +ARTIFACT_SUFFIX="" +set_artifact_suffix(){ + if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" = "Linux" ]; then + if [ "$(uname -m)" = "aarch64" ]; then ARTIFACT_SUFFIX="-native-linux-aarch64" + else ARTIFACT_SUFFIX="-native-linux-amd64"; fi + elif [ "$(uname)" = "Darwin" ]; then + if [ "$(uname -m)" = "arm64" ]; then ARTIFACT_SUFFIX="-native-mac-aarch64" + else ARTIFACT_SUFFIX="-native-mac-amd64"; fi + else + echo "This native mill launcher supports only Linux and macOS." 1>&2 + exit 1 + fi +} + +case "$MILL_VERSION" in + *"$MILL_NATIVE_SUFFIX") + MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} + set_artifact_suffix + ;; + + *"$MILL_JVM_SUFFIX") + MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"} + ;; + + *) + case "$MILL_VERSION" in + 0.1.*) ;; + 0.2.*) ;; + 0.3.*) ;; + 0.4.*) ;; + 0.5.*) ;; + 0.6.*) ;; + 0.7.*) ;; + 0.8.*) ;; + 0.9.*) ;; + 0.10.*) ;; + 0.11.*) ;; + 0.12.*) ;; + *) + set_artifact_suffix + esac + ;; +esac + +MILL="${MILL_FINAL_DOWNLOAD_FOLDER}/$MILL_VERSION$ARTIFACT_SUFFIX" + +# If not already downloaded, download it +if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then + case $MILL_VERSION in + 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.* ) + MILL_DOWNLOAD_SUFFIX="" + MILL_DOWNLOAD_FROM_MAVEN=0 + ;; + 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M* ) + MILL_DOWNLOAD_SUFFIX="-assembly" + MILL_DOWNLOAD_FROM_MAVEN=0 + ;; + *) + MILL_DOWNLOAD_SUFFIX="-assembly" + MILL_DOWNLOAD_FROM_MAVEN=1 + ;; + esac + case $MILL_VERSION in + 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11 ) + MILL_DOWNLOAD_EXT="jar" + ;; + 0.12.* ) + MILL_DOWNLOAD_EXT="exe" + ;; + 0.* ) + MILL_DOWNLOAD_EXT="jar" + ;; + *) + MILL_DOWNLOAD_EXT="exe" + ;; + esac + + MILL_TEMP_DOWNLOAD_FILE="${MILL_OUTPUT_DIR:-out}/mill-temp-download" + mkdir -p "$(dirname "${MILL_TEMP_DOWNLOAD_FILE}")" + + if [ "$MILL_DOWNLOAD_FROM_MAVEN" = "1" ] ; then + MILL_DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${MILL_DOWNLOAD_EXT}" + else + MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') + MILL_DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${MILL_DOWNLOAD_SUFFIX}" + unset MILL_VERSION_TAG + fi + + + if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then + echo $MILL_DOWNLOAD_URL + echo $MILL + exit 0 + fi + + echo "Downloading mill ${MILL_VERSION} from ${MILL_DOWNLOAD_URL} ..." 1>&2 + curl -f -L -o "${MILL_TEMP_DOWNLOAD_FILE}" "${MILL_DOWNLOAD_URL}" + + chmod +x "${MILL_TEMP_DOWNLOAD_FILE}" + + mkdir -p "${MILL_FINAL_DOWNLOAD_FOLDER}" + mv "${MILL_TEMP_DOWNLOAD_FILE}" "${MILL}" + + unset MILL_TEMP_DOWNLOAD_FILE + unset MILL_DOWNLOAD_SUFFIX +fi + +MILL_FIRST_ARG="" +if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then + # Need to preserve the first position of those listed options + MILL_FIRST_ARG=$1 + shift +fi + +unset MILL_FINAL_DOWNLOAD_FOLDER +unset MILL_OLD_DOWNLOAD_PATH +unset OLD_MILL +unset MILL_VERSION +unset MILL_REPO_URL + +# -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 +# We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes +# shellcheck disable=SC2086 +exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" diff --git a/mill-build/src/Deps.scala b/mill-build/src/Deps.scala new file mode 100644 index 00000000..2c13c509 --- /dev/null +++ b/mill-build/src/Deps.scala @@ -0,0 +1,61 @@ +package millbuild + +import mill.javalib.* + +object Deps { + + val commonsCodec = mvn"commons-codec:commons-codec:1.18.0" + val commonsCompress = mvn"org.apache.commons:commons-compress:1.28.0" + val commonsLang3 = mvn"org.apache.commons:commons-lang3:3.17.0" + val hypersistenceUtilsHibernate63 = + mvn"io.hypersistence:hypersistence-utils-hibernate-63:3.10.1" + val jacksonDatatypeJdk8 = + mvn"com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.19.4" + val jacksonModuleJakartaXmlbindAnnotations = + mvn"com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.19.4" + val javaJwt = mvn"com.auth0:java-jwt:4.5.0" + val javamelodySpringBootStarter = + mvn"net.bull.javamelody:javamelody-spring-boot-starter:2.5.0" + val junitJupiter = mvn"org.junit.jupiter:junit-jupiter:5.12.0" + val junitJupiterApi = mvn"org.junit.jupiter:junit-jupiter-api:5.12.0" + val junitJupiterEngine = mvn"org.junit.jupiter:junit-jupiter-engine:5.12.0" + val junitJupiterParams = mvn"org.junit.jupiter:junit-jupiter-params:5.12.0" + val jsoup = mvn"org.jsoup:jsoup:1.21.1" + val jwksRsa = mvn"com.auth0:jwks-rsa:0.23.0" + val liquibaseCore = mvn"org.liquibase:liquibase-core:4.31.1" + val logbackCore = mvn"ch.qos.logback:logback-core:1.5.21" + val logbackJackson = mvn"ch.qos.logback.contrib:logback-jackson:0.1.5" + val logbackJsonClassic = + mvn"ch.qos.logback.contrib:logback-json-classic:0.1.5" + val micrometerRegistryPrometheus = + mvn"io.micrometer:micrometer-registry-prometheus:1.15.6" + val postgresql = mvn"org.postgresql:postgresql:42.7.8" + val preliquibaseSpringBootStarter = + mvn"net.lbruun.springboot:preliquibase-spring-boot-starter:1.6.1" + val springBootConfigurationProcessor = + mvn"org.springframework.boot:spring-boot-configuration-processor:3.5.8" + val springBootDevtools = + mvn"org.springframework.boot:spring-boot-devtools:3.5.8" + val springBootStarter = + mvn"org.springframework.boot:spring-boot-starter:3.5.8" + val springBootStarterActuator = + mvn"org.springframework.boot:spring-boot-starter-actuator:3.5.8" + val springBootStarterDataJpa = + mvn"org.springframework.boot:spring-boot-starter-data-jpa:3.5.8" + val springBootStarterSecurity = + mvn"org.springframework.boot:spring-boot-starter-security:3.5.8" + val springBootStarterValidation = + mvn"org.springframework.boot:spring-boot-starter-validation:3.5.8" + val springBootStarterTest = + mvn"org.springframework.boot:spring-boot-starter-test:3.5.8" + val springBootStarterWeb = + mvn"org.springframework.boot:spring-boot-starter-web:3.5.8" + val springSecurityWeb = + mvn"org.springframework.security:spring-security-web:6.5.7" + val springdocOpenapiStarterWebmvcApi = + mvn"org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.14" + val testcontainersJunitJupiter = + mvn"org.testcontainers:testcontainers-junit-jupiter:2.0.2" + val testcontainersPostgresql = + mvn"org.testcontainers:testcontainers-postgresql:2.0.2" +} From ecced81032a059d3be480e07a05a6f803ad3023b Mon Sep 17 00:00:00 2001 From: Gunnar Velle Date: Tue, 16 Dec 2025 14:23:23 +0100 Subject: [PATCH 3/7] Run mill in ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a9648f2..ab2d058f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,4 +25,4 @@ jobs: key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - name: Build with maven - run: mvn --batch-mode --update-snapshots -P integration verify + run: ./mill test From 3af4382298254a64964f410dbb53e996d6aeda3e Mon Sep 17 00:00:00 2001 From: Gunnar Velle Date: Wed, 17 Dec 2025 10:19:05 +0100 Subject: [PATCH 4/7] Remove test scope for postgres since pre-integration needs it. --- pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/pom.xml b/pom.xml index fe92e78b..041b001d 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,6 @@ org.testcontainers testcontainers-postgresql ${testcontainers.version} - test From 877615f365f797534af2758277cafd09bd29b145 Mon Sep 17 00:00:00 2001 From: Gunnar Velle Date: Tue, 23 Dec 2025 10:41:44 +0100 Subject: [PATCH 5/7] Bump spring-version for mill to be able to run with typescript profile- --- .github/workflows/ci.yml | 2 +- build.mill | 9 ++++----- mill-build/src/Deps.scala | 18 +++++++++--------- scripts/OpenApiDocPlugin.scala | 0 scripts/package.mill | 0 src/main/resources/application-typescript.yml | 2 +- 6 files changed, 15 insertions(+), 16 deletions(-) create mode 100644 scripts/OpenApiDocPlugin.scala create mode 100644 scripts/package.mill diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab2d058f..7d81ac50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,5 +24,5 @@ jobs: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - - name: Build with maven + - name: Build with mill run: ./mill test diff --git a/build.mill b/build.mill index 88053254..33651fe9 100644 --- a/build.mill +++ b/build.mill @@ -10,9 +10,10 @@ import millbuild.* object `package` extends SpringBootModule, MavenModule, PublishModule { def jvmId = "temurin:21" def javacOptions = super.javacOptions() ++ Seq("-parameters") - override def springBootPlatformVersion = "3.5.8" + override def springBootPlatformVersion = "3.5.9" def mvnDeps = super.mvnDeps() ++ Seq( + Deps.testcontainersPostgresql, Deps.springBootStarter, Deps.springBootStarterWeb, Deps.springBootStarterValidation, @@ -47,16 +48,14 @@ object `package` extends SpringBootModule, MavenModule, PublishModule { def mvnDeps = super.mvnDeps() ++ Seq( Deps.springBootStarterTest, - Deps.testcontainersJunitJupiter, - Deps.testcontainersPostgresql + Deps.testcontainersJunitJupiter ) } - def artifactName = "taxonomy-api" def pomParentProject = Some( - Artifact("org.springframework.boot", "spring-boot-starter-parent", "3.5.8") + Artifact("org.springframework.boot", "spring-boot-starter-parent", "3.5.9") ) def pomSettings = PomSettings( diff --git a/mill-build/src/Deps.scala b/mill-build/src/Deps.scala index 2c13c509..49b17066 100644 --- a/mill-build/src/Deps.scala +++ b/mill-build/src/Deps.scala @@ -33,23 +33,23 @@ object Deps { val preliquibaseSpringBootStarter = mvn"net.lbruun.springboot:preliquibase-spring-boot-starter:1.6.1" val springBootConfigurationProcessor = - mvn"org.springframework.boot:spring-boot-configuration-processor:3.5.8" + mvn"org.springframework.boot:spring-boot-configuration-processor" val springBootDevtools = - mvn"org.springframework.boot:spring-boot-devtools:3.5.8" + mvn"org.springframework.boot:spring-boot-devtools" val springBootStarter = - mvn"org.springframework.boot:spring-boot-starter:3.5.8" + mvn"org.springframework.boot:spring-boot-starter" val springBootStarterActuator = - mvn"org.springframework.boot:spring-boot-starter-actuator:3.5.8" + mvn"org.springframework.boot:spring-boot-starter-actuator" val springBootStarterDataJpa = - mvn"org.springframework.boot:spring-boot-starter-data-jpa:3.5.8" + mvn"org.springframework.boot:spring-boot-starter-data-jpa" val springBootStarterSecurity = - mvn"org.springframework.boot:spring-boot-starter-security:3.5.8" + mvn"org.springframework.boot:spring-boot-starter-security" val springBootStarterValidation = - mvn"org.springframework.boot:spring-boot-starter-validation:3.5.8" + mvn"org.springframework.boot:spring-boot-starter-validation" val springBootStarterTest = - mvn"org.springframework.boot:spring-boot-starter-test:3.5.8" + mvn"org.springframework.boot:spring-boot-starter-test" val springBootStarterWeb = - mvn"org.springframework.boot:spring-boot-starter-web:3.5.8" + mvn"org.springframework.boot:spring-boot-starter-web" val springSecurityWeb = mvn"org.springframework.security:spring-security-web:6.5.7" val springdocOpenapiStarterWebmvcApi = diff --git a/scripts/OpenApiDocPlugin.scala b/scripts/OpenApiDocPlugin.scala new file mode 100644 index 00000000..e69de29b diff --git a/scripts/package.mill b/scripts/package.mill new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/application-typescript.yml b/src/main/resources/application-typescript.yml index a27021d5..e7de24cd 100644 --- a/src/main/resources/application-typescript.yml +++ b/src/main/resources/application-typescript.yml @@ -2,7 +2,7 @@ server.port: 5000 spring: datasource: - url: jdbc:tc:postgresql:17.5:///taxonomy_api + url: jdbc:tc:postgresql:17.5:///public username: test password: test hikari: From 958bd5d55c04a3accd6f7351024186c25c878056 Mon Sep 17 00:00:00 2001 From: Gunnar Velle Date: Tue, 23 Dec 2025 13:40:17 +0100 Subject: [PATCH 6/7] Generate typescript using mill plugin --- .github/workflows/typescript-generate.yml | 4 +- build.mill | 6 +- mill-build/src/TypescriptPlugin.scala | 124 ++++++++++++++++++++++ scripts/OpenApiDocPlugin.scala | 0 scripts/package.mill | 0 5 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 mill-build/src/TypescriptPlugin.scala delete mode 100644 scripts/OpenApiDocPlugin.scala delete mode 100644 scripts/package.mill diff --git a/.github/workflows/typescript-generate.yml b/.github/workflows/typescript-generate.yml index 538a4a08..4e0b094e 100644 --- a/.github/workflows/typescript-generate.yml +++ b/.github/workflows/typescript-generate.yml @@ -55,7 +55,7 @@ jobs: --password-stdin - name: Generate typescript types run: | - mvn -P integration verify -Dtest=VersionServiceIntegrationTest + ./mill generateTypescript cd typescript/ yarn yarn generate-typescript @@ -65,7 +65,7 @@ jobs: git config user.email "$KNOWIT_EMAIL" git config user.name "github-actions" - git add typescript/openapi.json + git add typescript/openapi.json git add typescript/*.ts git commit -m "AUTOMATION: Generated new typescript files" git push diff --git a/build.mill b/build.mill index 33651fe9..ba8dc433 100644 --- a/build.mill +++ b/build.mill @@ -2,12 +2,14 @@ //| mill-jvm-version: temurin:21 package build +import mill._ import mill.javalib.* import mill.javalib.publish.* import mill.javalib.spring.boot.SpringBootModule import millbuild.* -object `package` extends SpringBootModule, MavenModule, PublishModule { + +object `package` extends SpringBootModule, MavenModule, PublishModule, TypescriptPlugin { def jvmId = "temurin:21" def javacOptions = super.javacOptions() ++ Seq("-parameters") override def springBootPlatformVersion = "3.5.9" @@ -78,4 +80,6 @@ object `package` extends SpringBootModule, MavenModule, PublishModule { ("java.version", "21") ) + def mainClass = Some("no.ndla.taxonomy.TaxonomyApplication") + } diff --git a/mill-build/src/TypescriptPlugin.scala b/mill-build/src/TypescriptPlugin.scala new file mode 100644 index 00000000..f46623df --- /dev/null +++ b/mill-build/src/TypescriptPlugin.scala @@ -0,0 +1,124 @@ +package millbuild + +import mill._ +import mill.javalib.JavaModule + +object TypescriptPluginHelper { + + def fetchOpenApi( + apiUrl: String, + timeoutMs: Long, + backoffMs: Long + ): Option[String] = { + val url = new java.net.URI(apiUrl).toURL() + val startTime = System.currentTimeMillis() + var result: Option[String] = None + + while (result.isEmpty && (System.currentTimeMillis() - startTime) < timeoutMs) { + try { + val conn = url.openConnection().asInstanceOf[java.net.HttpURLConnection] + conn.setRequestMethod("GET") + conn.setConnectTimeout(3000) + conn.setReadTimeout(10000) + val code = conn.getResponseCode + + if (code == 200) { + val is = conn.getInputStream + val bytes = is.readAllBytes() + is.close() + result = Some(new String(bytes, java.nio.charset.StandardCharsets.UTF_8)) + println(s"Successfully fetched API docs (${bytes.length} bytes)") + } else { + println(s"Got HTTP $code, retrying...") + } + } catch { + case _: Throwable => + if (result.isEmpty) Thread.sleep(backoffMs) + } + } + + result + } + + def startApp( + javaCmd: String, + classpath: String, + mainClass: String, + profile: String + ): os.SubProcess = { + os.proc( + javaCmd, + "-cp", + classpath, + mainClass, + s"--spring.profiles.active=$profile" + ).spawn(stdout = os.Inherit, stderr = os.Inherit) + } + + def stopApp(proc: os.SubProcess): Unit = { + try { + proc.destroy() + Thread.sleep(1000) + } catch { + case _: Throwable => () + } + } +} + +trait TypescriptPlugin extends JavaModule { + + def typescriptProfile: String = "typescript" + def typescriptApiUrl: String = "http://localhost:5000/api-docs" + def typescriptTimeoutMs: Long = 120000L + def typescriptBackoffMs: Long = 500L + + def generateTypescript() = Task.Command { + val cp = runClasspath().map(_.path) + val classpath = cp.map(_.toString).mkString(java.io.File.pathSeparator) + + val javaCmd = sys.props.get("java.home") match { + case Some(h) => + val javaPath = os.Path(h) / "bin" / "java" + if (os.exists(javaPath)) javaPath.toString else "java" + case None => "java" + } + + val mainCls = mainClass().getOrElse { + throw new RuntimeException("mainClass must be defined") + } + + println(s"Starting Spring Boot app with profile: $typescriptProfile") + + val proc = TypescriptPluginHelper.startApp(javaCmd, classpath, mainCls, typescriptProfile) + + println(s"Polling $typescriptApiUrl until available...") + + val apiContent = TypescriptPluginHelper.fetchOpenApi( + typescriptApiUrl, + typescriptTimeoutMs, + typescriptBackoffMs + ) + + try { + apiContent match { + case Some(body) => + var projectRoot = os.pwd + while (!os.exists(projectRoot / "build.mill") && projectRoot != os.root) { + projectRoot = projectRoot / os.up + } + val outFile = projectRoot / "typescript" / "openapi.json" + os.makeDir.all(outFile / os.up) + os.write.over(outFile, body) + println(s"✓ Wrote OpenAPI spec to $outFile") + + case None => + throw new RuntimeException( + s"Failed to fetch $typescriptApiUrl within ${typescriptTimeoutMs}ms timeout" + ) + } + } finally { + println("Shutting down Spring Boot app...") + TypescriptPluginHelper.stopApp(proc) + } + } +} diff --git a/scripts/OpenApiDocPlugin.scala b/scripts/OpenApiDocPlugin.scala deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/package.mill b/scripts/package.mill deleted file mode 100644 index e69de29b..00000000 From 72f54775770129697487ec6d97ea1f86e97e622f Mon Sep 17 00:00:00 2001 From: Gunnar Velle Date: Tue, 6 Jan 2026 08:45:52 +0100 Subject: [PATCH 7/7] SpotlessModule similar to maven setup --- .spotless-formats.json | 51 +++++++++++++++++++ build.mill | 15 +----- .../taxonomy/service/OldUrlCanonifier.java | 8 +-- .../service/UrlResolverServiceImpl.java | 4 +- .../no/ndla/taxonomy/util/PrettyUrlUtil.java | 2 +- src/test/java/no/ndla/taxonomy/TestUtils.java | 2 +- 6 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 .spotless-formats.json diff --git a/.spotless-formats.json b/.spotless-formats.json new file mode 100644 index 00000000..7e0a317d --- /dev/null +++ b/.spotless-formats.json @@ -0,0 +1,51 @@ +[ + { + "includes": ["glob:build.mill"], + "steps": [ + { + "$type": "ScalaFmt" + } + ] + }, + { + "includes": ["glob:**.java"], + "steps": [ + { + "$type": "PalantirJavaFormat", + "version": "2.81.0", + "style": "PALANTIR", + "formatJavadoc": false + }, + { + "$type": "Indent", + "type": "SPACE", + "numSpacesPerTab": "4" + }, + { + "$type": "LicenseHeader", + "delimiter": "package ", + "header": { + "file": "license-header" + } + }, + { + "$type": "TrimTrailingWhitespace" + }, + { + "$type": "EndWithNewline" + }, + { + "$type": "CleanthatJava", + "version": "2.23", + "sourceJdkVersion": "21", + "includeDraft": false, + "mutators": [ + "SafeAndConsensual", + "ArithmethicAssignment", + "ForEachToIterableForEach", + "LiteralsFirstInComparisons" + ] + } + ] + } +] diff --git a/build.mill b/build.mill index ba8dc433..e7c85367 100644 --- a/build.mill +++ b/build.mill @@ -5,11 +5,11 @@ package build import mill._ import mill.javalib.* import mill.javalib.publish.* +import mill.javalib.spotless.* import mill.javalib.spring.boot.SpringBootModule import millbuild.* - -object `package` extends SpringBootModule, MavenModule, PublishModule, TypescriptPlugin { +object `package` extends MavenModule, SpringBootModule, SpotlessModule { def jvmId = "temurin:21" def javacOptions = super.javacOptions() ++ Seq("-parameters") override def springBootPlatformVersion = "3.5.9" @@ -69,17 +69,6 @@ object `package` extends SpringBootModule, MavenModule, PublishModule, Typescrip Seq() ) - def publishVersion = "0.0.1-SNAPSHOT" - - def publishProperties = super.publishProperties() ++ Map( - ("project.build.sourceEncoding", "UTF-8"), - ("testcontainers.version", "2.0.2"), - ("spotless.version", "3.0.0"), - ("javamelody.version", "2.5.0"), - ("project.reporting.outputEncoding", "UTF-8"), - ("java.version", "21") - ) - def mainClass = Some("no.ndla.taxonomy.TaxonomyApplication") } diff --git a/src/main/java/no/ndla/taxonomy/service/OldUrlCanonifier.java b/src/main/java/no/ndla/taxonomy/service/OldUrlCanonifier.java index 7505faad..5d3f6e83 100644 --- a/src/main/java/no/ndla/taxonomy/service/OldUrlCanonifier.java +++ b/src/main/java/no/ndla/taxonomy/service/OldUrlCanonifier.java @@ -32,7 +32,7 @@ public String canonify(String oldUrl) { nodeId = token.substring(nodeStartsAt); } else if (token.contains("fag=")) { int start = token.indexOf("fag="); - int ampersandIndex = token.indexOf("&"); + int ampersandIndex = token.indexOf('&'); int end = ampersandIndex > start ? ampersandIndex : token.length(); fagId = "?" + token.substring(start, end); } @@ -44,8 +44,8 @@ private String discardKnownNodeSuffixes(String oldUrl) { for (String suffix : KNOWN_NODE_SUFFIXES) { if (oldUrl.contains(suffix)) { int start = oldUrl.indexOf(suffix); - int indexOfSlashAfter = oldUrl.indexOf("/", start + 1); - int indexOfQuestionMark = oldUrl.indexOf("?", start); + int indexOfSlashAfter = oldUrl.indexOf('/', start + 1); + int indexOfQuestionMark = oldUrl.indexOf('?', start); String partToRemove; if (indexOfSlashAfter != -1) { partToRemove = oldUrl.substring(start, indexOfSlashAfter + 1); @@ -68,7 +68,7 @@ private String replaceKnownNodePrefixes(String oldUrl) { } private int findNodeStartsAt(String token) { - return token.substring(0, token.lastIndexOf("/")).lastIndexOf("/"); + return token.substring(0, token.lastIndexOf('/')).lastIndexOf('/'); } private String[] tokenize(String oldUrl) { diff --git a/src/main/java/no/ndla/taxonomy/service/UrlResolverServiceImpl.java b/src/main/java/no/ndla/taxonomy/service/UrlResolverServiceImpl.java index e2f9a61c..0943626f 100644 --- a/src/main/java/no/ndla/taxonomy/service/UrlResolverServiceImpl.java +++ b/src/main/java/no/ndla/taxonomy/service/UrlResolverServiceImpl.java @@ -109,9 +109,9 @@ private List getCachedUrlOldRig(String oldUrl) { private Optional getNodeId(String url) { if (url != null) { if (url.contains("?") && url.contains("/")) { - return Optional.of(url.substring(url.lastIndexOf("/"), url.indexOf("?"))); + return Optional.of(url.substring(url.lastIndexOf('/'), url.indexOf('?'))); } else if (url.contains("/")) { - return Optional.of(url.substring(url.lastIndexOf("/"))); + return Optional.of(url.substring(url.lastIndexOf('/'))); } } return Optional.empty(); diff --git a/src/main/java/no/ndla/taxonomy/util/PrettyUrlUtil.java b/src/main/java/no/ndla/taxonomy/util/PrettyUrlUtil.java index 5ef4c801..a291f99d 100644 --- a/src/main/java/no/ndla/taxonomy/util/PrettyUrlUtil.java +++ b/src/main/java/no/ndla/taxonomy/util/PrettyUrlUtil.java @@ -91,6 +91,6 @@ public static String getHashFromPath(String title) { if (!title.contains("/r/") && !title.contains("/e/") && !title.contains("/f/")) { return ""; } - return title.substring(title.lastIndexOf("/") + 1); + return title.substring(title.lastIndexOf('/') + 1); } } diff --git a/src/test/java/no/ndla/taxonomy/TestUtils.java b/src/test/java/no/ndla/taxonomy/TestUtils.java index af567586..95ecb352 100644 --- a/src/test/java/no/ndla/taxonomy/TestUtils.java +++ b/src/test/java/no/ndla/taxonomy/TestUtils.java @@ -141,7 +141,7 @@ public MockHttpServletResponse updateResource(String path, Object command, Resul public static URI getId(MockHttpServletResponse response) { String location = response.getHeader("Location"); - return URI.create(location.substring(location.lastIndexOf("/") + 1)); + return URI.create(location.substring(location.lastIndexOf('/') + 1)); } public V getObject(Class theClass, MockHttpServletResponse response) throws Exception {