From 54eb45b16436311fe8f88a6cdea3d413c6018efd Mon Sep 17 00:00:00 2001 From: Paul Ambrose Date: Fri, 3 Jul 2026 14:45:30 -0700 Subject: [PATCH 1/2] Upgrade dependencies and toolchain; refresh project docs Toolchain and dependencies: - Raise the JVM toolchain from Java 17 to Java 25, and bump the GitHub Actions CI runners (lint + test jobs) to JDK 25 to match. - Upgrade the Gradle wrapper from 9.5.0 to 9.6.1. - Bump Kotlin 2.4.0, Ktor 3.5.1, Kotest 6.2.1, readingbat-core 3.2.1, common-utils 2.9.3, kotlin-logging 8.0.4, kotlinter 5.5.0, detekt 2.0.0-alpha.5. Rename the `gradle` catalog key to `gradle-wrapper`. Build and Makefile: - Refactor build.gradle.kts into per-concern helper functions, build the fat jar via Ktor's fatJar task, run the Kotlin unused-return-value checker over production code, and reject pre-release dependencyUpdates candidates for stable dependencies. - Remove the Makefile heroku and logs targets. Docs: - Update CHANGELOG.md, RELEASE_NOTES.md, README.md, CLAUDE.md, and llms.txt to reflect the Java 25 toolchain, the GitHub Actions CI workflow, the versioncheck -> versions target rename, and the current dependency set. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 8 +- CHANGELOG.md | 19 +++- CLAUDE.md | 8 +- Makefile | 16 +-- README.md | 2 +- RELEASE_NOTES.md | 45 ++++++--- build.gradle.kts | 120 +++++++++++++++++------ gradle/libs.versions.toml | 14 +-- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 4 +- gradlew.bat | 4 +- llms.txt | 2 +- 12 files changed, 168 insertions(+), 76 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0fbbccb..45e7c12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,11 +17,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 @@ -45,11 +45,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/CHANGELOG.md b/CHANGELOG.md index 9559eb6..b8dd1fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,18 +7,29 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added -- Kotlinter Gradle plugin (`org.jmailen.kotlinter` 5.4.2) wired into the build for Kotlin lint and formatting. -- Detekt Gradle plugin (`dev.detekt` 2.0.0-alpha.3) with a `detekt { ... }` configuration block. +- Kotlinter Gradle plugin (`org.jmailen.kotlinter` 5.5.0) wired into the build for Kotlin lint and formatting. +- Detekt Gradle plugin (`dev.detekt` 2.0.0-alpha.5) with a `detekt { ... }` configuration block. - `.editorconfig` pinning charset, end-of-line, indent style, and ktlint rule overrides so kotlinter aligns with the project's existing Kotlin style. -- Makefile targets: `help` (self-documenting target list), `lint`, `format`, `detekt`, `detekt-baseline`. +- GitHub Actions CI (`.github/workflows/ci.yml`) running `make lint` and `make tests` as parallel jobs on every push and pull request to `master`, with Gradle caching, concurrency cancellation, and uploaded lint/test reports. +- Makefile targets: `help` (self-documenting target list), `lint`, `format`, `detekt`, `detekt-baseline`, `versions`, `upgrade-wrapper`. - Makefile guard `_require-gradle-version` that fails fast if the Gradle version cannot be read from `gradle/libs.versions.toml`. ### Changed -- Bumped `readingbat-core` to 3.1.8. +- Raised the JVM toolchain from Java 17 to Java 25 (`jvm` in `gradle/libs.versions.toml`). +- Upgraded the Gradle wrapper from 9.5.0 to 9.6.1. +- Bumped dependencies: Kotlin → 2.4.0, Ktor → 3.5.1, Kotest → 6.2.1, `readingbat-core` → 3.2.1, `common-utils` → 2.9.3, `kotlin-logging` → 8.0.4, kotlinter → 5.5.0, detekt → 2.0.0-alpha.5. +- Refactored `build.gradle.kts` into per-concern helper functions (`configureKotlin`, `configureDetekt`, `configureKotlinter`, `configureKtor`, `configureShadowJar`, `configureTest`, `configureVersions`). +- Enabled the Kotlin unused-return-value checker (`-Xreturn-value-checker=check`) on production code only (the test source set is excluded to avoid false positives from Kotest's fluent assertions). +- Build the fat jar via Ktor's `fatJar` task; the `dependencyUpdates` check now rejects pre-release candidates for dependencies currently on a stable version. +- Renamed the `gradle` version key to `gradle-wrapper` in `gradle/libs.versions.toml`. +- Renamed the Makefile `versioncheck` target to `versions`, made `help` the default target, and added `--no-configuration-cache --no-parallel` to the dependency-update check. - `Content.kt` now imports `ReturnType` directly instead of `ReturnType.*`; all challenge registrations qualify return types (e.g., `ReturnType.IntType`). - Makefile `build` and `cc` targets use the canonical `-x test` flag instead of `-xtest`. - Reordered `.PHONY` to match the in-file target definition order. +### Removed +- Makefile `heroku` and `logs` targets. + ## [Pre-Unreleased history] Prior changes were tracked only via Git history; see `git log` for details. diff --git a/CLAUDE.md b/CLAUDE.md index fb9f8dd..f9c364c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,13 +62,17 @@ make lint # Run kotlinter (lintKotlin) + detekt make format # Format Kotlin sources via formatKotlin make detekt # Run detekt static analysis only make detekt-baseline # Regenerate the detekt baseline file -make versioncheck # Check dependency updates +make versions # Check for dependency updates make upgrade-wrapper # Upgrade Gradle wrapper make uberjar # Build fat jar make uber # Build and run fat jar (java -jar build/libs/server.jar) ``` -JVM toolchain: Java 17. Testing: Kotest with JUnit5 platform. Lint/format: kotlinter + detekt, configured via `.editorconfig` (ktlint rule overrides) and the `detekt { ... }` block in `build.gradle.kts`. +JVM toolchain: Java 25 (set via `jvm` in `gradle/libs.versions.toml`, consumed by `jvmToolchain(...)` in `build.gradle.kts`). Testing: Kotest with JUnit5 platform. Lint/format: kotlinter + detekt, configured via `.editorconfig` (ktlint rule overrides) and the `detekt { ... }` block in `build.gradle.kts`. + +## Continuous Integration + +`.github/workflows/ci.yml` runs `make lint` and `make tests` as parallel jobs on every push and pull request to `master`, with Gradle caching, concurrency cancellation, and uploaded lint/test reports. ## Adding a New Challenge diff --git a/Makefile b/Makefile index 808d2d2..3bc01f7 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 _require-gradle-version + cc run versions upgrade-wrapper _require-gradle-version GRADLE_VERSION := $(shell sed -n 's/^gradle-wrapper = "\(.*\)"/\1/p' gradle/libs.versions.toml) @@ -42,19 +42,13 @@ cc: ## Continuous compilation (no tests) run: ## Start the server on port 8080 ./gradlew run -heroku: ## Push master to Heroku - git push heroku master - -logs: ## Tail Heroku logs - heroku logs --tail - -# 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. versions: ## Check for dependency updates (default target) ./gradlew dependencyUpdates --no-configuration-cache --no-parallel -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/README.md b/README.md index 9c7effa..24eadb6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Python programming challenges served by the [ReadingBat](https://github.com/read ## Prerequisites -- Java 17+ +- Java 25+ (the Gradle toolchain targets Java 25; Gradle can auto-provision it) ## Setup diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c2fbf8b..28d65ad 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,36 +1,57 @@ # Release Notes -## Unreleased — Kotlinter, detekt, and Makefile help +## Unreleased — tooling, CI, and dependency upgrades -This release focuses on developer ergonomics and code-style enforcement. No -runtime behavior, content, or public DSL has changed. +This release focuses on developer ergonomics, continuous integration, and +toolchain/dependency upgrades. No runtime behavior, content, or public DSL has +changed. ### Highlights -- **Kotlin linting and formatting.** The build now applies the - [kotlinter](https://github.com/jeremymailen/kotlinter-gradle) Gradle plugin. - Run `make lint` to check Kotlin sources, and `make format` to apply fixes. +- **Kotlin linting and formatting.** The build applies the + [kotlinter](https://github.com/jeremymailen/kotlinter-gradle) Gradle plugin + (5.5.0). Run `make lint` to check Kotlin sources, and `make format` to apply + fixes. - **Detekt static analysis.** Detekt is wired up via the `dev.detekt` plugin - and a `detekt { ... }` block in `build.gradle.kts`. Use `make detekt` to run - it standalone, or `make detekt-baseline` to (re)generate the baseline. + (2.0.0-alpha.5) and a `detekt { ... }` block in `build.gradle.kts`. Use + `make detekt` to run it standalone, or `make detekt-baseline` to (re)generate + the baseline. +- **GitHub Actions CI.** A new `.github/workflows/ci.yml` runs `make lint` and + `make tests` as parallel jobs on every push and pull request to `master`, + with Gradle caching, concurrency cancellation, and uploaded reports. - **Self-documenting Makefile.** Run `make help` for a colorized list of every target, generated from `## ` annotations in the Makefile itself. -- **`.editorconfig`.** New file pinning charset, EOL, indent width, and the - set of ktlint rules disabled to match the existing Kotlin style. -- **Dependency bump.** `readingbat-core` updated from 3.1.5 to 3.1.8. +- **`.editorconfig`.** Pins charset, EOL, indent width, and the set of ktlint + rules disabled to match the existing Kotlin style. +- **JVM toolchain → Java 25.** The Gradle toolchain now targets Java 25 + (previously Java 17); Gradle can auto-provision it. +- **Dependency upgrades.** Kotlin → 2.4.0, Ktor → 3.5.1, Kotest → 6.2.1, + `readingbat-core` → 3.2.1, `common-utils` → 2.9.3, `kotlin-logging` → 8.0.4. + The Gradle wrapper moved from 9.5.0 to 9.6.1. +- **Build script cleanup.** `build.gradle.kts` is reorganized into per-concern + helper functions, the fat jar is built via Ktor's `fatJar` task, the Kotlin + unused-return-value checker runs over production code, and `dependencyUpdates` + now filters out pre-release candidates for stable dependencies. ### Source-level changes - `Content.kt` no longer wildcard-imports `ReturnType.*`; all return types are qualified (e.g., `ReturnType.IntType`). When adding new challenges, use the qualified form. +- The `gradle` version key in `gradle/libs.versions.toml` was renamed to + `gradle-wrapper`. +- The Makefile `versioncheck` target was renamed to `versions`, `help` is now + the default target, and the dependency-update check runs with + `--no-configuration-cache --no-parallel`. - The Makefile uses the canonical `-x test` flag (with a space) in `build` and `cc` targets, replacing the deprecated `-xtest` short form. ### Upgrade notes +- Java 25 is now the toolchain target. Gradle will auto-provision a matching + JDK; ensure toolchain auto-download is enabled or a Java 25 JDK is installed. - After pulling, run `./gradlew --refresh-dependencies` (or `make build`) once - so the new plugins resolve. + so the new plugins and dependencies resolve. - Existing developer workflows are unchanged; the new `lint` / `format` targets are optional but recommended before pushing. 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/llms.txt b/llms.txt index cae5949..58e2bd9 100644 --- a/llms.txt +++ b/llms.txt @@ -38,7 +38,7 @@ Each .py file has: a `# @desc` comment, a function implementing the challenge, a - Continuous build: `make cc` - Lint (kotlinter + detekt): `make lint` - Format Kotlin sources: `make format` -- JVM toolchain: Java 17 +- JVM toolchain: Java 25 - Testing: Kotest with JUnit5 - Lint/format: kotlinter + detekt, configured via `.editorconfig` and `build.gradle.kts` From bb68fcc3081cc98a7758dbb37327f950b5e2beaa Mon Sep 17 00:00:00 2001 From: Paul Ambrose Date: Fri, 3 Jul 2026 15:09:43 -0700 Subject: [PATCH 2/2] Fix ContentTests for upgraded readingbat-kotest API The Kotest/readingbat-core bump turned `correctAnswers` from a property into a function, breaking `:compileTestKotlin`. Invoke it as `correctAnswers()`. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 3 +++ src/test/kotlin/ContentTests.kt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8dd1fe..e5d8b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Removed - Makefile `heroku` and `logs` targets. +### Fixed +- `ContentTests` now calls `correctAnswers()` as a function, matching the upgraded `readingbat-kotest` API (previously a property). + ## [Pre-Unreleased history] Prior changes were tracked only via Git history; see `git log` for details. diff --git a/src/test/kotlin/ContentTests.kt b/src/test/kotlin/ContentTests.kt index 2589bd8..aead57d 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] } } }