diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20aa51a..17c009e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,11 +16,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 25 - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 @@ -33,11 +33,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 25 - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 diff --git a/Makefile b/Makefile index 88810fd..fcba971 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: default help clean build lint format detekt detekt-baseline tests uberjar uber \ - cc run heroku logs versions upgrade-wrapper + cc run versions upgrade-wrapper GRADLE_VERSION := $(shell sed -n 's/^gradle-wrapper = "\(.*\)"/\1/p' gradle/libs.versions.toml) @@ -42,19 +42,13 @@ cc: ## Continuous compilation (watches for changes) run: ## Start the content server locally (port 8080) ./gradlew run -heroku: ## Deploy by pushing master to Heroku - git push heroku master - -logs: ## Tail Heroku logs - heroku logs --tail - versions: ## Check for dependency updates ./gradlew dependencyUpdates --no-configuration-cache --no-parallel -# Gradle's documented upgrade procedure: the first run rewrites -# gradle-wrapper.properties using the *old* wrapper jar; the second run -# regenerates the wrapper itself with the new version. -upgrade-wrapper: _require-gradle-version ## Upgrade Gradle wrapper to version in libs.versions.toml +upgrade-wrapper: _require-gradle-version ## Upgrade the Gradle wrapper to the catalog version + # Gradle's documented upgrade procedure: the first run rewrites + # gradle-wrapper.properties using the *old* wrapper jar; the second run + # regenerates the wrapper itself with the new version. ./gradlew wrapper --gradle-version=$(GRADLE_VERSION) --distribution-type=bin ./gradlew wrapper --gradle-version=$(GRADLE_VERSION) --distribution-type=bin diff --git a/build.gradle.kts b/build.gradle.kts index b7598e9..3d1ba48 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,11 @@ +import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask +import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.gradle.kotlin.dsl.named +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - application alias(libs.plugins.kotlin.jvm) alias(libs.plugins.versions) alias(libs.plugins.ktor.plugin) @@ -9,13 +13,12 @@ plugins { alias(libs.plugins.kotlinter) } -// This is for ./gradlew run +description = "ReadingBat Site" + application { mainClass = "ContentServerKt" } -description = "ReadingBat Site" - dependencies { implementation(libs.readingbat.core) implementation(libs.core.utils) @@ -24,43 +27,102 @@ dependencies { testImplementation(libs.bundles.testing) } -kotlin { - jvmToolchain(libs.versions.jvm.get().toInt()) +tasks.register("stage") { + group = "distribution" + description = "Clean-builds the project for deployment." + dependsOn("clean", "build") } -detekt { - source.setFrom("src/main/kotlin", "src/test/kotlin") - buildUponDefaultConfig = true - parallel = true +tasks.named("build") { + mustRunAfter("clean") } -kotlinter { - ignoreFormatFailures = false - ignoreLintFailures = false - reporters = arrayOf("checkstyle", "plain") +configureKotlin() +configureDetekt() +configureKotlinter() +configureKtor() +configureShadowJar() +configureTest() +configureVersions() + +fun Project.configureKotlin() { + kotlin { + jvmToolchain(libs.versions.jvm.get().toInt()) + } + + // Run the unused-return-value checker over production code only. Kotest's + // assertion DSL (e.g. shouldBe) returns its receiver, and tests intentionally + // discard that result, so applying the checker to the test source set would + // emit only false-positive warnings. + tasks.named("compileKotlin") { + compilerOptions { + freeCompilerArgs.add("-Xreturn-value-checker=check") + } + } } -tasks.register("stage") { - dependsOn("clean", "build") +fun Project.configureDetekt() { + detekt { + source.setFrom("src/main/kotlin", "src/test/kotlin") + buildUponDefaultConfig = true + parallel = true + } } -tasks.named("build") { - mustRunAfter("clean") +fun Project.configureKotlinter() { + kotlinter { + ignoreFormatFailures = false + ignoreLintFailures = false + reporters = arrayOf("checkstyle", "plain") + } } -tasks.shadowJar { - archiveFileName.set("server.jar") - isZip64 = true - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - listOf("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "LICENSE*").forEach(::exclude) +fun Project.configureKtor() { + ktor { + fatJar { + archiveFileName = "server.jar" + } + } } -tasks.test { - useJUnitPlatform() +fun Project.configureShadowJar() { + // Ktor's `buildFatJar` task delegates to shadow; this block configures that output. + tasks.shadowJar { + isZip64 = true + duplicatesStrategy = DuplicatesStrategy.WARN + exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") + } +} + +fun Project.configureTest() { + tasks.test { + useJUnitPlatform() + + testLogging { + events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED, TestLogEvent.STANDARD_ERROR) + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = false + } + } +} + +fun Project.configureVersions() { + // A pre-release qualifier is a `.` or `-` delimiter followed by a known unstable + // keyword. `m\d` matches milestones (`-M1`/`.M2`) without catching stable classifiers + // like `-macos`/`-MR1`, and the `[.-]` delimiter catches both dash-style (`-alpha`) + // and dot-style (Netty's `.Beta1`) qualifiers while leaving `-jre`/`.Final` stable. + val preReleaseQualifier = + Regex("""[.-](rc|beta|alpha|m\d|cr|snapshot|eap|dev|milestone|pre)""", RegexOption.IGNORE_CASE) + + fun isNonStable(version: String): Boolean = preReleaseQualifier.containsMatchIn(version) - testLogging { - events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED, TestLogEvent.STANDARD_ERROR) - exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL - showStandardStreams = false + tasks.withType().configureEach { + notCompatibleWithConfigurationCache("the dependency updates plugin is not compatible with the configuration cache") + // Reject a pre-release candidate only when the current version is stable. For + // dependencies we intentionally track on a pre-release line (e.g. a detekt + // alpha), newer pre-releases are still surfaced as available updates. + rejectVersionIf { + isNonStable(candidate.version) && !isNonStable(currentVersion) + } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f644d06..ff6073b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,14 @@ [versions] -detekt = "2.0.0-alpha.3" -gradle-wrapper = "9.5.1" -jvm = "17" -kotest = "6.1.11" +detekt = "2.0.0-alpha.5" +gradle-wrapper = "9.6.1" +jvm = "25" +kotest = "6.2.1" kotlin = "2.4.0" kotlinter = "5.5.0" -ktor = "3.5.0" +ktor = "3.5.1" logging = "8.0.4" -readingbat = "3.1.8" -utils = "2.9.0" +readingbat = "3.2.1" +utils = "2.9.3" versions = "0.54.0" [libraries] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df6a6ad..a9db115 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/gradlew b/gradlew index b9bb139..249efbb 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/gradlew.bat b/gradlew.bat index 24c62d5..a51ec4f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel diff --git a/src/test/kotlin/ContentTests.kt b/src/test/kotlin/ContentTests.kt index 9212fa0..ecb97c3 100644 --- a/src/test/kotlin/ContentTests.kt +++ b/src/test/kotlin/ContentTests.kt @@ -72,7 +72,7 @@ class ContentTests : StringSpec() { forEachGroup { forEachChallenge { forEachAnswer { - it shouldHaveAnswer correctAnswers[it.index] + it shouldHaveAnswer correctAnswers()[it.index] } } }