diff --git a/.claude/settings.json b/.claude/settings.json index f7bbfb98f..08c475740 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -53,16 +53,17 @@ ] }, "hooks": { - "PreToolUse": [ + "SessionStart": [ { - "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command", - "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/protect-version-file.sh" + "command": "$CLAUDE_PROJECT_DIR/init-submodules" } ] - }, + } + ], + "PreToolUse": [ { "matcher": "Bash", "hooks": [ diff --git a/.github/workflows/build-on-ubuntu.yml b/.github/workflows/build-on-ubuntu.yml index 27d9302b9..516f347e1 100644 --- a/.github/workflows/build-on-ubuntu.yml +++ b/.github/workflows/build-on-ubuntu.yml @@ -19,9 +19,13 @@ jobs: - uses: gradle/actions/setup-gradle@v6 - - name: Build project and run tests + # `build` does not run Dokka — its tasks are gated to the publishing + # graph — so `dokkaGenerate` is appended to surface documentation + # warnings on each push, before merge, instead of only in the post-merge + # `Publish` job. `failOnWarning` is enabled in the Dokka setup. + - name: Build project, run tests, and check documentation shell: bash - run: ./gradlew build --stacktrace + run: ./gradlew build dokkaGenerate --stacktrace # See: https://github.com/marketplace/actions/junit-report-action - name: Publish Test Report diff --git a/.github/workflows/build-on-windows.yml b/.github/workflows/build-on-windows.yml index 992272ce4..0a07cfab6 100644 --- a/.github/workflows/build-on-windows.yml +++ b/.github/workflows/build-on-windows.yml @@ -8,14 +8,6 @@ jobs: name: Build on Windows steps: - - name: Configure Git for Windows symlink compatibility - shell: pwsh - run: | - # Avoid creating/expecting native symlinks on Windows runners. - # This helps when repository paths (like .agents/guidelines) are represented - # via links that can trigger EPERM on stat/access. - git config --global core.symlinks false - - uses: actions/checkout@v6 with: submodules: recursive diff --git a/.gitmodules b/.gitmodules index 5b3352bf4..1978333b9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,5 +5,5 @@ path = .agents/shared url = https://github.com/SpineEventEngine/agents.git branch = master - update = checkout + update = merge ignore = all diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 1b15ab447..5c4b3ef9d 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -3,12 +3,8 @@ - - - + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index d96636dd2..8c5f6198d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,8 +16,21 @@ links to a shared requirements file (e.g. `jvm-project.md`), read that too. Shared skills, scripts, and guidelines come from the `.agents/shared` submodule (the [`agents`][agents-repo] repository) exposed via symlinks. -`./config/pull` initializes and floats it automatically; on a fresh clone that skips -`pull`, run `git submodule update --init --remote .agents/shared`. +`./config/pull` initializes and floats them automatically. But a fresh `git worktree` +(and some shallow clones / cloud checkouts) start with NO submodules checked out, so +those symlinks dangle and no skills are found. Bootstrap such a tree with +**`./init-submodules`** — a root script that materializes the missing +*config-managed* submodules at their pinned commits: `config` itself, plus every +submodule that declares a tracked `branch` in `.gitmodules` (`.agents/shared`, and +any shared submodule added later) — the same rule `./config/pull` uses to decide +what it floats. Submodules the consumer owns (a Hugo theme, a vendored library, +doc-example submodules, …) declare no tracked branch and are left untouched, so the +automatic `SessionStart` run never tries to clone — or fail on credentials for — a +submodule this project does not manage. It depends on no pre-existing `config` +submodule, so it works before `./config/pull` (which lives inside the `config` +submodule) can. Claude Code runs it automatically via a `SessionStart` hook; other +agents and humans run it by hand, then `./config/pull` to float the shared submodules +to their branch tips. ## Commit and history safety @@ -115,7 +128,7 @@ In consumer repositories, skip without comment any path matching: - `.claude/**`, `.idea/**`, `.junie/**` - `.github/copilot-instructions.md` - `buildSrc/**` (except `buildSrc/src/main/kotlin/module.gradle.kts`) -- `gradle/`, `gradlew`, `gradlew.bat` +- `gradle/`, `gradlew`, `gradlew.bat`, `init-submodules` - `.codecov.yml`, `.gitignore`, `gradle.properties`, `lychee.toml` - `.github/workflows/` — unless the workflow was introduced by this repo diff --git a/buildSrc/src/main/kotlin/BuildExtensions.kt b/buildSrc/src/main/kotlin/BuildExtensions.kt index f7a11e3e0..eefdb23e1 100644 --- a/buildSrc/src/main/kotlin/BuildExtensions.kt +++ b/buildSrc/src/main/kotlin/BuildExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,6 @@ import io.spine.dependency.build.PluginPublishPlugin import io.spine.dependency.lib.Protobuf import io.spine.dependency.local.Compiler import io.spine.dependency.local.CoreJvmCompiler -import io.spine.dependency.local.McJava import io.spine.dependency.local.ProtoTap import io.spine.dependency.test.Kotest import io.spine.dependency.test.Kover @@ -88,12 +87,6 @@ val ScriptHandlerScope.protobuf: Protobuf val ScriptHandlerScope.coreJvmCompiler: CoreJvmCompiler get() = CoreJvmCompiler -/** - * Shortcut to [McJava] dependency object for using under `buildScript`. - */ -val ScriptHandlerScope.mcJava: McJava - get() = McJava - /** * Shortcut to [CoreJvmCompiler] dependency object. * @@ -112,10 +105,10 @@ val ScriptHandlerScope.spineCompiler: Compiler /** * Shortcut to [Compiler] dependency object. * - * This plugin is published at Gradle Plugin Portal. - * But when used in a pair with [mcJava], it cannot be applied directly to a project. - * It is so, because [mcJava] uses [spineCompiler] as its dependency. - * And the buildscript's classpath ends up with both of them. + * This plugin is published at Gradle Plugin Portal. But when another plugin that + * depends on the Compiler is also on the buildscript's classpath, the Compiler + * cannot be applied directly to a project. In such a case, declare it here so that + * it is added to the classpath, then apply it by ID. */ val PluginDependenciesSpec.spineCompiler: Compiler get() = Compiler @@ -344,7 +337,7 @@ val buildToolConfigurations: Array = arrayOf( ) /** - * Make the `sourcesJar` task accept duplicated input which seems to occur + * Make the `sourcesJar` task accept duplicated input, which seems to occur * somewhere inside Protobuf Gradle Plugin. */ fun Project.allowDuplicationInSourcesJar() { diff --git a/buildSrc/src/main/kotlin/DokkaExts.kt b/buildSrc/src/main/kotlin/DokkaExts.kt index 800b3236c..814250a54 100644 --- a/buildSrc/src/main/kotlin/DokkaExts.kt +++ b/buildSrc/src/main/kotlin/DokkaExts.kt @@ -216,7 +216,7 @@ fun Project.htmlDocsJar(): TaskProvider = tasks.getOrCreate("htmlDocsJar") } /** - * Tells if this task belongs to the execution graph which contains + * Tells if this task belongs to the execution graph that contains * the `publish` and `dokkaGenerate` tasks. * * This predicate could be useful for disabling publishing tasks diff --git a/buildSrc/src/main/kotlin/LicenseSettings.kt b/buildSrc/src/main/kotlin/LicenseSettings.kt index 465129f55..3b49cb57e 100644 --- a/buildSrc/src/main/kotlin/LicenseSettings.kt +++ b/buildSrc/src/main/kotlin/LicenseSettings.kt @@ -1,31 +1,31 @@ /* -* Copyright 2025, TeamDev. All rights reserved. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* https://www.apache.org/licenses/LICENSE-2.0 -* -* Redistribution and use in source and/or binary forms, with or without -* modification, must retain the above copyright notice and the following -* disclaimer. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ /** - * The settings of the software license which apply to the code of this project. + * The settings of the software license that apply to the code of this project. * * The constants defined in this object are used by the * [PublicationHandler][io.spine.gradle.publish.PublicationHandler] to set up diff --git a/buildSrc/src/main/kotlin/Strings.kt b/buildSrc/src/main/kotlin/Strings.kt index 19e0c218b..227bd8a57 100644 --- a/buildSrc/src/main/kotlin/Strings.kt +++ b/buildSrc/src/main/kotlin/Strings.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ * This file provides extensions to `String` and `CharSequence` that wrap * analogues from standard Kotlin runtime. * - * It helps in switching between versions of Gradle which have different versions of + * It helps in switching between versions of Gradle that have different versions of * the Kotlin runtime. Please see the bodies of the extension functions for details on * switching the implementations depending on the Kotlin version at hand. * diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/Dependency.kt b/buildSrc/src/main/kotlin/io/spine/dependency/Dependency.kt index 999538fd6..372651128 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/Dependency.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/Dependency.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,7 +80,7 @@ abstract class Dependency { * * @param project The project in which the artifacts are forced. Used for logging. * @param cfg The configuration for which the artifacts are forced. Used for logging. - * @param rs The resolution strategy which forces the artifacts. + * @param rs The resolution strategy that forces the artifacts. */ fun forceArtifacts(project: Project, cfg: Configuration, rs: ResolutionStrategy) { artifacts.values.forEach { @@ -90,7 +90,7 @@ abstract class Dependency { } /** - * A dependency which declares a Maven Bill of Materials (BOM). + * A dependency that declares a Maven Bill of Materials (BOM). * * @see * Maven Bill of Materials @@ -111,7 +111,6 @@ abstract class DependencyWithBom : Dependency() { fun Configuration.diagSuffix(project: Project): String = "the configuration `$name` in the project: `${project.path}`." - private fun ResolutionStrategy.forceWithLogging( project: Project, configuration: Configuration, diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/boms/BomsPlugin.kt b/buildSrc/src/main/kotlin/io/spine/dependency/boms/BomsPlugin.kt index 2724dda54..c5444a00d 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/boms/BomsPlugin.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/boms/BomsPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.ConfigurationContainer /** - * The plugin which forces versions of platforms declared in the [Boms] object. + * The plugin that forces versions of platforms declared in the [Boms] object. * * Versions are enforced via the * [org.gradle.api.artifacts.dsl.DependencyHandler.enforcedPlatform] call diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/CommonsCli.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/CommonsCli.kt index 06fb2eb42..701453df4 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/CommonsCli.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/CommonsCli.kt @@ -27,7 +27,7 @@ package io.spine.dependency.lib /** - * Commons CLI is a transitive dependency which we don't use directly. + * Commons CLI is a transitive dependency that we don't use directly. * We `force` it in [forceVersions]. * * [Commons CLI](https://commons.apache.org/proper/commons-cli/) diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/IntelliJ.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/IntelliJ.kt index 7f9232b75..cf2d6b02d 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/IntelliJ.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/IntelliJ.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,8 @@ package io.spine.dependency.lib /** * The components of the IntelliJ Platform. * - * Make sure to add the `intellijReleases` and `jetBrainsCacheRedirector` - * repositories to your project. See `kotlin/Repositories.kt` for details. + * Make sure to add the `intellijReleases` and `intellijDependencies` + * repositories to your project. See `io/spine/gradle/repo/Repositories.kt` for details. */ @Suppress("unused") object IntelliJ { diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt index 416a6af08..cc74bfeae 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt @@ -35,7 +35,7 @@ import io.spine.dependency.DependencyWithBom object Kotlin : DependencyWithBom() { /** - * This is the version of Kotlin we use for writing code which does not + * This is the version of Kotlin we use for writing code that does not * depend on Gradle and the version of embedded Kotlin. */ @Suppress("MemberVisibilityCanBePrivate") // used directly from the outside. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt index f2f0507d7..6253c0ef1 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt @@ -33,8 +33,8 @@ package io.spine.dependency.local */ @Suppress("ConstPropertyName", "unused") object Base { - const val version = "2.0.0-SNAPSHOT.404" - const val versionForBuildScript = "2.0.0-SNAPSHOT.404" + const val version = "2.0.0-SNAPSHOT.420" + const val versionForBuildScript = "2.0.0-SNAPSHOT.420" const val group = Spine.group private const val prefix = "spine" const val libModule = "$prefix-base" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt index 7b1160d3b..d118d7c62 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt @@ -72,7 +72,7 @@ object Compiler : Dependency() { * The version of the Compiler dependencies. */ override val version: String - private const val fallbackVersion = "2.0.0-SNAPSHOT.051" + private const val fallbackVersion = "2.0.0-SNAPSHOT.053" /** * The distinct version of the Compiler used by other build tools. @@ -81,7 +81,7 @@ object Compiler : Dependency() { * transitive dependencies, this is the version used to build the project itself. */ val dogfoodingVersion: String - private const val fallbackDfVersion = "2.0.0-SNAPSHOT.051" + private const val fallbackDfVersion = "2.0.0-SNAPSHOT.053" /** * The artifact for the Compiler Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt index 1f95edc47..805fdb0c8 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt @@ -39,7 +39,7 @@ typealias CoreJava = CoreJvm @Suppress("ConstPropertyName", "unused") object CoreJvm { const val group = Spine.group - const val version = "2.0.0-SNAPSHOT.373" + const val version = "2.0.0-SNAPSHOT.376" const val coreArtifact = "spine-core" const val clientArtifact = "spine-client" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt index c3d8edbef..1f91cf249 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt @@ -46,12 +46,12 @@ object CoreJvmCompiler { /** * The version used in the build classpath. */ - const val dogfoodingVersion = "2.0.0-SNAPSHOT.068" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.077" /** * The version to be used for integration tests. */ - const val version = "2.0.0-SNAPSHOT.068" + const val version = "2.0.0-SNAPSHOT.077" /** * The ID of the Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/McJava.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/McJava.kt deleted file mode 100644 index 6ab0a9565..000000000 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/McJava.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.dependency.local - -/** - * Dependencies on Spine Model Compiler for Java. - * - * See [mc-java](https://github.com/SpineEventEngine/mc-java). - */ -@Suppress( - "MemberVisibilityCanBePrivate" /* `pluginLib()` is used by subprojects. */, - "ConstPropertyName", - "unused" -) -object McJava { - const val group = Spine.toolsGroup - - /** - * The version used to in the build classpath. - */ - const val dogfoodingVersion = "2.0.0-SNAPSHOT.320" - - /** - * The version to be used for integration tests. - */ - const val version = "2.0.0-SNAPSHOT.320" - - /** - * The ID of the Gradle plugin. - */ - const val pluginId = "io.spine.mc-java" - - /** - * The library with the [dogfoodingVersion]. - */ - val pluginLib = pluginLib(dogfoodingVersion) - - /** - * The library with the given [version]. - */ - fun pluginLib(version: String): String = "$group:spine-mc-java-plugins:$version:all" - - /** The artifact reference for forcing in configurations. */ - const val pluginsArtifact: String = "$group:spine-mc-java-plugins:$version" - - /** - * The `mc-java-base` artifact with the [version]. - */ - val base = base(version) - - /** - * The `mc-java-base` artifact with the given [version]. - */ - fun base(version: String): String = "$group:spine-mc-java-base:$version" -} diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoData.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoData.kt deleted file mode 100644 index c4f06c8fd..000000000 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoData.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.dependency.local - -/** - * Dependencies on ProtoData modules. - * - * To use a locally published ProtoData version instead of the version from a public plugin - * registry, set the `PROTODATA_VERSION` and/or the `PROTODATA_DF_VERSION` environment variables - * and stop the Gradle daemons so that Gradle observes the env change: - * ``` - * export PROTODATA_VERSION=0.43.0-local - * export PROTODATA_DF_VERSION=0.41.0 - * - * ./gradle --stop - * ./gradle build # Conduct the intended checks. - * ``` - * - * Then, to reset the console to run the usual versions again, remove the values of - * the environment variables and stop the daemon: - * ``` - * export PROTODATA_VERSION="" - * export PROTODATA_DF_VERSION="" - * - * ./gradle --stop - * ``` - * - * See [`SpineEventEngine/ProtoData`](https://github.com/SpineEventEngine/ProtoData/). - */ -@Suppress( - "unused" /* Some subprojects do not use ProtoData directly. */, - "ConstPropertyName" /* We use custom convention for artifact properties. */, - "MemberVisibilityCanBePrivate" /* The properties are used directly by other subprojects. */, -) -object ProtoData { - const val pluginGroup = Spine.group - const val group = "io.spine.protodata" - const val pluginId = "io.spine.protodata" - - /** - * Identifies ProtoData as a `classpath` dependency under `buildScript` block. - * - * The dependency is obtained from https://plugins.gradle.org/m2/. - */ - const val module = "io.spine:protodata" - - /** - * The version of ProtoData dependencies. - */ - val version: String - private const val fallbackVersion = "0.96.4" - - /** - * The distinct version of ProtoData used by other build tools. - * - * When ProtoData is used both for building the project and as a part of the Project's - * transitional dependencies, this is the version used to build the project itself. - */ - val dogfoodingVersion: String - private const val fallbackDfVersion = "0.96.4" - - /** - * The artifact for the ProtoData Gradle plugin. - */ - val pluginLib: String - - /** - * The artifact to be used during experiments when publishing locally. - * - * @see ProtoData - */ - private fun pluginLib(version: String): String = - "$group:gradle-plugin:$version" - - fun api(version: String): String = - "$group:protodata-api:$version" - - val api - get() = api(version) - - val backend - get() = "$group:protodata-backend:$version" - - val params - get() = "$group:protodata-params:$version" - - val protocPlugin - get() = "$group:protodata-protoc:$version" - - val gradleApi - get() = "$group:protodata-gradle-api:$version" - - val cliApi - get() = "$group:protodata-cli-api:$version" - - val javaModule = "$group:protodata-java" - - fun java(version: String): String = - "$javaModule:$version" - - val java - get() = java(version) - - val fatCli - get() = "$group:protodata-fat-cli:$version" - - val testlib - get() = "$group:protodata-testlib:$version" - - /** - * An env variable storing a custom [version]. - */ - private const val VERSION_ENV = "PROTODATA_VERSION" - - /** - * An env variable storing a custom [dogfoodingVersion]. - */ - private const val DF_VERSION_ENV = "PROTODATA_DF_VERSION" - - /** - * Sets up the versions and artifacts for the build to use. - * - * If either [VERSION_ENV] or [DF_VERSION_ENV] is set, those versions are used instead of - * the hardcoded ones. Also, in this mode, the [pluginLib] coordinates are changed so that - * it points at a locally published artifact. Otherwise, it points at an artifact that would be - * published to a public plugin registry. - */ - init { - val experimentVersion = System.getenv(VERSION_ENV) - val experimentDfVersion = System.getenv(DF_VERSION_ENV) - if (experimentVersion?.isNotBlank() == true || experimentDfVersion?.isNotBlank() == true) { - version = experimentVersion ?: fallbackVersion - dogfoodingVersion = experimentDfVersion ?: fallbackDfVersion - - pluginLib = pluginLib(version) - println(""" - - ❗ Running an experiment with ProtoData. ❗ - ----------------------------------------- - Regular version = v$version - Dogfooding version = v$dogfoodingVersion - - ProtoData Gradle plugin can now be loaded from Maven Local. - - To reset the versions, erase the `$$VERSION_ENV` and `$$DF_VERSION_ENV` environment variables. - - """.trimIndent()) - } else { - version = fallbackVersion - dogfoodingVersion = fallbackDfVersion - pluginLib = "$pluginGroup:protodata:$version" - } - } -} diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoTap.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoTap.kt index 21a603105..d9d23e772 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoTap.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoTap.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ package io.spine.dependency.local ) object ProtoTap { const val group = Spine.toolsGroup - const val version = "0.14.0" + const val version = "0.16.0" const val gradlePluginId = "io.spine.prototap" const val api = "$group:prototap-api:$version" const val gradlePlugin = "$group:prototap-gradle-plugin:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt index c0c4238d9..54c6ef906 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt @@ -34,8 +34,8 @@ package io.spine.dependency.local @Suppress("ConstPropertyName", "unused") object ToolBase { const val group = Spine.toolsGroup - const val version = "2.0.0-SNAPSHOT.381" - const val dogfoodingVersion = "2.0.0-SNAPSHOT.381" + const val version = "2.0.0-SNAPSHOT.400" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.400" const val lib = "$group:tool-base:$version" const val classicCodegen = "$group:classic-codegen:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/Cli.kt b/buildSrc/src/main/kotlin/io/spine/gradle/Cli.kt index 7e0c22748..9424ea1ff 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/Cli.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/Cli.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,11 +63,14 @@ class Cli(private val workingFolder: File) { redirectError(PIPE) }.start() - val exitCode = process.run { - inputStream!!.pourTo(outWriter) - errorStream!!.pourTo(errWriter) - waitFor() - } + val outReader = process.inputStream!!.pourTo(outWriter) + val errReader = process.errorStream!!.pourTo(errWriter) + val exitCode = process.waitFor() + // `waitFor()` returns on process exit but does not wait for the reader + // threads to finish draining the pipes; join them so the buffers hold + // the complete output before it is read below. + outReader.join() + errReader.join() if (exitCode == 0) { return outWriter.toString() @@ -83,14 +86,14 @@ class Cli(private val workingFolder: File) { } /** - * Asynchronously reads all lines from this [InputStream] and appends them - * to the passed [StringWriter]. + * Starts a background thread that reads all lines from this [InputStream] and + * appends them to [dest], returning the thread so the caller can [join][Thread.join] + * it once the process has exited, ensuring the buffer holds the complete output. */ -private fun InputStream.pourTo(dest: StringWriter) { +private fun InputStream.pourTo(dest: StringWriter): Thread = Thread { val sc = Scanner(this) while (sc.hasNextLine()) { dest.append(sc.nextLine()) } - }.start() -} + }.also { it.start() } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt index f95f23ea0..14e0ef8c2 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt @@ -250,7 +250,7 @@ class GitRepository( class ClonedRepo( /** - * Origin Git repository which is cloned. + * Origin Git repository that is cloned. */ private val repo: GitRepository, @@ -269,7 +269,7 @@ class ClonedRepo( * The original `buildSrc` folder, if it exists in this cloned repo, is renamed * to `buildSrc-original`. * - * Optionally, takes an [ignoredFolder] which will be excluded from the [source] paths + * Optionally, takes an [ignoredFolder] that will be excluded from the [source] paths * when copying. * * @@ -289,7 +289,7 @@ class ClonedRepo( * The original `config` folder, if it exists in this cloned repo, is renamed * to `config-original`. * - * Optionally, takes an [ignoredFolder] which will be excluded from the [source] paths + * Optionally, takes an [ignoredFolder] that will be excluded from the [source] paths * when copying. * * Returns this instance of `ClonedRepo`, for call chaining. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/RunGradle.kt b/buildSrc/src/main/kotlin/io/spine/gradle/RunGradle.kt index 894263026..5d05b4493 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/RunGradle.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/RunGradle.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ import org.gradle.api.tasks.TaskAction import org.gradle.internal.os.OperatingSystem /** - * A Gradle task which runs another Gradle build. + * A Gradle task that runs another Gradle build. * * Launches Gradle wrapper under a given [directory] with the specified [taskNames] names. * The `clean` task is also run if current build includes a `clean` task. @@ -56,7 +56,7 @@ open class RunGradle : DefaultTask() { } /** - * Path to the directory which contains a Gradle wrapper script. + * Path to the directory that contains a Gradle wrapper script. */ @Internal lateinit var directory: String diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartEnvironment.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartEnvironment.kt index 4e4ae839c..0e622e2cf 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartEnvironment.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartEnvironment.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ interface DartEnvironment { val projectName: String /** - * A directory which all artifacts are generated into. + * A directory that all artifacts are generated into. * * Default value: "$projectDir/build". */ @@ -73,7 +73,7 @@ interface DartEnvironment { .resolve(projectName) /** - * A directory which contains integration test Dart sources. + * A directory that contains integration test Dart sources. * * Default value: "$projectDir/integration-test". */ diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartExtension.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartExtension.kt index 57a2bcfd2..002531d9b 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartExtension.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartExtension.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,7 +69,7 @@ import org.gradle.kotlin.dsl.findByType * There are two ways to modify the environment: * * 1. Modify [DartEnvironment] interface directly. Go with this option when it is a global change - * that should affect all projects which use this extension. + * that should affect all projects that use this extension. * 2. Use [DartExtension.environment] scope — for temporary and custom overridings. * * An example of a property overriding: @@ -92,7 +92,7 @@ import org.gradle.kotlin.dsl.findByType * be named after a task it registers or a task group if several tasks are registered at once. * Then this extension is called in a project's `build.gradle.kts`. * - * `DartTasks` and `DartPlugins` scopes extend [DartContext] which provides access + * `DartTasks` and `DartPlugins` scopes extend [DartContext] that provides access * to the current [DartEnvironment] and shortcuts for running `pub` tool. * * Below is the simplest example of how to create a primitive `printPubVersion` task. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt index 997bcef53..21aa28b65 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt @@ -41,7 +41,7 @@ private val integrationTestName = TaskName.of("integrationTest", Exec::class) * * The task runs integration tests of the `spine-dart` library against a sample * Spine-based application. The tests are run in Chrome browser because they use `WebFirebaseClient` - * which only works in web environment. + * that only works in web environment. * * A sample Spine-based application is run from the `test-app` module before integration * tests start and is stopped as the tests complete. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt b/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt index e0ce8275f..8ce4d72a5 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,6 +53,7 @@ import org.gradle.api.Project * This configuration determines what ends up in the `author` and `committer` fields of a commit. * @property currentBranch The currently checked-out branch. */ +@Suppress("TooManyFunctions") // A cohesive wrapper over many small `git` commands. class Repository private constructor( private val project: Project, private val sshUrl: String, @@ -86,6 +87,7 @@ class Repository private constructor( * Checks out the branch by its name. * * IMPORTANT. The branch must exist in the upstream repository. + * Use [checkoutOrCreate] to check out a branch that may not exist yet. */ fun checkout(branch: String) { repoExecute("git", "checkout", branch) @@ -94,6 +96,112 @@ class Repository private constructor( currentBranch = branch } + /** + * Checks out the [branch], creating it in the remote repository if it does + * not exist yet. + * + * If the branch is already present on the remote, it is [checked out][checkout] + * as usual. Otherwise, it is created as an orphan branch seeded with + * [initialFiles] and pushed to the remote, so that subsequent commits with the + * documentation have a branch to append to. + * + * Creating the branch on the fly makes the very first documentation publication + * of a repository self-sufficient: the [documentation branch][Branch.documentation] + * no longer needs to be created manually beforehand. + * + * @param branch the name of the branch to check out or create. + * @param initialFiles the files — paths relative to the repository root mapped + * to their content — to add to the initial commit when the branch is created. + * Ignored when the branch already exists. + */ + fun checkoutOrCreate(branch: String, initialFiles: Map = emptyMap()) { + if (remoteHasBranch(branch)) { + // `remoteHasBranch` queries the remote directly via `git ls-remote`, + // which does not populate `refs/remotes/origin/*`. In a parallel + // build another module may have created the branch after this clone, + // so fetch first to make the `origin/$branch` ref available; + // otherwise `git checkout` cannot guess it and fails with a + // pathspec error. + repoExecute("git", "fetch", "origin") + checkout(branch) + } else { + createOrphanBranch(branch, initialFiles) + } + } + + /** + * Tells whether the remote repository has a branch with the given [name]. + * + * Queries the fully-qualified ref `refs/heads/$name` rather than the bare + * [name]: `git ls-remote` treats a bare name as a tail glob and would also + * match a namespaced branch such as `feature/$name`. Relies on `git ls-remote` + * returning an empty output with a zero exit code when the branch is absent, + * so the check does not raise an exception. + */ + private fun remoteHasBranch(name: String): Boolean { + val output = repoExecute("git", "ls-remote", "--heads", "origin", "refs/heads/$name") + return output.isNotBlank() + } + + /** + * Creates the [branch] as an orphan branch seeded with [initialFiles] and + * pushes it to the remote. + * + * `git switch --orphan` starts a new history with an empty working tree, so + * the source code of the default branch does not leak into the created branch. + * The [initialFiles] are written into this clean tree and staged before the + * initial commit, which stays `--allow-empty` to support seeding no files. + */ + private fun createOrphanBranch(branch: String, initialFiles: Map) { + repoExecute("git", "switch", "--orphan", branch) + initialFiles.forEach { (path, content) -> + location.toFile().resolve(path).writeText(content) + repoExecute("git", "add", path) + } + repoExecute( + "git", + "commit", + "--allow-empty", + "--message=Initialize the `$branch` branch." + ) + currentBranch = branch + pushNewBranch(branch) + } + + /** + * Pushes the just-created [branch] to the remote, setting up the upstream tracking. + * + * If the push is rejected because a concurrently running publication created + * the branch first (e.g., another module publishing documentation in the same + * parallel build), the remote branch is [adopted][adoptRemoteBranch] instead. + * Otherwise, the failure is genuine, and the original exception is rethrown. + */ + private fun pushNewBranch(branch: String) { + try { + repoExecute("git", "push", "--set-upstream", "origin", branch) + } catch (e: IllegalStateException) { + // `Cli.execute` surfaces every non-zero `git` exit as an + // `IllegalStateException`, so this branch handles a rejected push. + // If the branch now exists on the remote, another module won the + // creation race and we adopt its branch; otherwise the failure is + // genuine and is rethrown. + repoExecute("git", "fetch", "origin") + if (!remoteHasBranch(branch)) { + throw e + } + adoptRemoteBranch(branch) + } + } + + /** + * Discards the local orphan branch in favour of the same-named branch that + * already exists on the remote, keeping the local branch in sync with it. + */ + private fun adoptRemoteBranch(branch: String) { + repoExecute("git", "reset", "--hard", "origin/$branch") + repoExecute("git", "branch", "--set-upstream-to=origin/$branch", branch) + } + /** * Configures the username and the email of the user. * @@ -154,7 +262,8 @@ class Repository private constructor( * See [configureUser] documentation for more information. * * Performs checkout of the branch in case it was passed. - * By default, [master][Branch.master] is checked out. + * By default, [master][Branch.master] is checked out. A non-default branch + * that does not exist yet is created and seeded with [initialFiles]. * * @throws IllegalArgumentException if SSH URL is an empty string. */ @@ -163,6 +272,7 @@ class Repository private constructor( sshUrl: String, user: UserInfo, branch: String = Branch.master, + initialFiles: Map = emptyMap(), ): Repository { require(sshUrl.isNotBlank()) { "SSH URL cannot be an empty string." } @@ -171,7 +281,7 @@ class Repository private constructor( repo.configureUser(user) if (branch != Branch.master) { - repo.checkout(branch) + repo.checkoutOrCreate(branch, initialFiles) } return repo diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt index de7529548..682d6478c 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,8 +38,10 @@ import org.gradle.api.Project * * The repository's GitHub SSH URL is derived from the `REPO_SLUG` environment * variable. The [branch][Branch.documentation] dedicated to publishing documentation - * is automatically checked out in this repository. Also, the username and the email - * of the git user are automatically configured. + * is automatically checked out in this repository, and created if it does not exist + * yet. A freshly created branch is seeded with a `CNAME` file so that GitHub Pages + * serves the documentation under the `spine.io` custom domain. Also, the username + * and the email of the git user are automatically configured. * * The username is set to `"UpdateGitHubPages Plugin"`, and the email is derived from * the `FORMAL_GIT_HUB_PAGES_AUTHOR` environment variable. @@ -56,5 +58,10 @@ internal fun Repository.Factory.forPublishingDocumentation(project: Project): Re val branch = Branch.documentation - return clone(project, host, user, branch) + // When the `gh-pages` branch is created from scratch, seed it with a `CNAME` + // file so that GitHub Pages serves the documentation under the `spine.io` + // custom domain. + val initialFiles = mapOf("CNAME" to "spine.io\n") + + return clone(project, host, user, branch, initialFiles) } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt index 68be42a07..653a9f950 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ internal class SshKey( val gitHubAccessKey = gitHubKey() log { "Obtained the key file at ${gitHubAccessKey.absolutePath}." } val sshConfigFile = sshConfigFile() - log { "Located the SSH key file at ${sshConfigFile.absolutePath}." } + log { "Located the SSH config file at ${sshConfigFile.absolutePath}." } sshConfigFile.appendPublisher(gitHubAccessKey) log { "SSH config file appended." } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt index 72b8fd37f..efea9e27a 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ package io.spine.gradle.github.pages object TaskName { /** - * The name of the task which updates the GitHub Pages. + * The name of the task that updates the GitHub Pages. */ const val updateGitHubPages = "updateGitHubPages" diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt index 9782980ac..0382e8364 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt @@ -42,7 +42,7 @@ import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider /** - * Registers the `updateGitHubPages` task which performs the update of + * Registers the `updateGitHubPages` task that performs the update of * the GitHub Pages with the documentation generated in Javadoc and HTML format * for a particular Gradle project. * @@ -53,7 +53,7 @@ import org.gradle.api.tasks.TaskProvider * repository root. It is recommended to encrypt it in the repository and then decrypt * it on CI upon publication. Also, the script uses the `FORMAL_GIT_HUB_PAGES_AUTHOR` * environment variable to set the author email for the commits. The `gh-pages` - * branch itself should exist before the plugin is run. + * branch is created automatically if it does not exist yet. * * NOTE: when changing the value of "FORMAL_GIT_HUB_PAGES_AUTHOR", one also must change * the SSH private (encrypted `deploy_key_rsa`) and the public @@ -67,7 +67,7 @@ import org.gradle.api.tasks.TaskProvider * REPO_SLUG: SpineEventEngine/base * ``` * - * @see UpdateGitHubPagesExtension for the extension which is used to configure + * @see UpdateGitHubPagesExtension for the extension that is used to configure * this plugin */ class UpdateGitHubPages : Plugin { @@ -113,7 +113,7 @@ class UpdateGitHubPages : Plugin { } /** - * Registers `updateGitHubPages` task which performs no actual update, but prints + * Registers `updateGitHubPages` task that performs no actual update, but prints * the message telling the update is skipped, since the project is in * its `SNAPSHOT` version. */ diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/java/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/java/Tasks.kt index 12d4d56cf..b2737f20c 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/java/Tasks.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/java/Tasks.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ import org.gradle.kotlin.dsl.named * * Runs the unit tests using JUnit or TestNG. * - * Depends on `testClasses`, and all tasks which produce the test runtime classpath. + * Depends on `testClasses`, and all tasks that produce the test runtime classpath. * * @see * Tasks | The Java Plugin diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt index 749367742..0732edc8c 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt @@ -36,7 +36,7 @@ import org.gradle.api.tasks.javadoc.Javadoc import org.gradle.external.javadoc.StandardJavadocDocletOptions /** - * The doclet which removes Javadoc for `@Internal` things in the Java code. + * The doclet that removes Javadoc for `@Internal` things in the Java code. */ @Suppress("ConstPropertyName") class ExcludeInternalDoclet { @@ -57,7 +57,7 @@ class ExcludeInternalDoclet { const val className = "io.spine.tools.javadoc.ExcludeInternalDoclet" /** - * The name of the helper task which configures the Javadoc processing + * The name of the helper task that configures the Javadoc processing * to exclude `@Internal` types. */ const val taskName = "noInternalJavadoc" @@ -68,7 +68,7 @@ class ExcludeInternalDoclet { } /** - * Creates a custom Javadoc task for the [project] which excludes the types + * Creates a custom Javadoc task for the [project] that excludes the types * annotated as `@Internal`. * * The task is registered under [taskName]. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsEnvironment.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsEnvironment.kt index c2dc68e41..7a649a899 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsEnvironment.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsEnvironment.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ interface JsEnvironment { get() = projectDir.resolve("test") /** - * A directory which all artifacts are generated into. + * A directory that all artifacts are generated into. * * Default value: "$projectDir/build". */ diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsExtension.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsExtension.kt index 69c3e2638..a849c7609 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsExtension.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsExtension.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ import org.gradle.kotlin.dsl.findByType * There are two ways to modify the environment: * * 1. Update [JsEnvironment] directly. Go with this option when it is a global change - * that should affect all projects which use this extension. + * that should affect all projects that use this extension. * 2. Use [JsExtension.environment] scope — for temporary and custom overridings. * * An example of a property overriding: @@ -93,7 +93,7 @@ import org.gradle.kotlin.dsl.findByType * named after a task it registers or a task group if several tasks are registered at once. * Then this extension is called in a project's `build.gradle.kts`. * - * `JsTasks` and `JsPlugins` scopes extend [JsContext] which provides access + * `JsTasks` and `JsPlugins` scopes extend [JsContext] that provides access * to the current [JsEnvironment] and shortcuts for running `npm` tool. * * Below is the simplest example of how to create a primitive `printNpmVersion` task. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.kt index fb0e183eb..3bb145480 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.kt @@ -142,7 +142,7 @@ private val installNodePackagesName = TaskName.of("installNodePackages") /** * Locates `installNodePackages` task in this [TaskContainer]. * - * The task installs Node packages which this module depends on using `npm install` command. + * The task installs Node packages that this module depends on using `npm install` command. * * The `npm install` command is executed with the vulnerability check disabled since * it cannot fail the task execution despite on vulnerabilities found. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt index 9b4ba3042..4c215f14b 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction /** - * A task which verifies that the current version of the library has not been published to the given + * A task that verifies that the current version of the library has not been published to the given * Maven repository yet. */ open class CheckVersionIncrement : DefaultTask() { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt index 152455dc3..6791d21a6 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ import org.gradle.api.publish.maven.MavenPublication * which is created for a module. Instead, since the publications are already declared, * this class only [assigns Maven coordinates][copyProjectAttributes]. * - * A module which declares custom publications must be specified in + * A module that declares custom publications must be specified in * the [SpinePublishing.modulesWithCustomPublishing] property. * * If a module with [publications] declared locally is not specified as one with custom publishing, diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt index df326b85d..c25868fc1 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,12 +82,12 @@ private fun Project.readGitHubToken(): String { } /** - * Read the personal access token for the `developers@spine.io` account which - * has only the permission to read public GitHub packages. + * Reads the personal access token for the `developers@spine.io` account. + * The token grants only read access to public GitHub packages. * * The token is extracted from the archive called `aus.weis` stored under `buildSrc`. * The archive has such an unusual name to avoid scanning for tokens placed in repositories - * which is performed by GitHub. Since we do not violate any security, it is OK to + * that is performed by GitHub. Since we do not violate any security, it is OK to * use such a workaround. */ private fun Project.readTokenFromArchive(): String { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt index 195a51463..24d6d0e2d 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt @@ -33,7 +33,7 @@ import org.gradle.api.Plugin import org.gradle.api.Project /** - * Gradle plugin which adds a [CheckVersionIncrement] task. + * Gradle plugin that adds a [CheckVersionIncrement] task. * * The task is called `checkVersionIncrement` inserted before the `check` task. */ @@ -67,7 +67,7 @@ class IncrementGuard : Plugin { * 2. The job is a pull request targeting a default (`master` or `main`) or * a release-line (e.g. `2.x-jdk8-master`) branch. * - * It is the responsibility of a branch which aims to merge into a default + * It is the responsibility of a branch that aims to merge into a default * (or otherwise protected) branch to bump the version. Auxiliary branches do not * deal with the versions in the release cycle, so pull requests targeting them, * direct pushes, and tag builds do not run the check. This also prevents unexpected diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt index 1bf0c0bf7..37b8bfb01 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ package io.spine.gradle.publish /** - * A DSL element of [SpinePublishing] extension which allows enabling publishing + * A DSL element of [SpinePublishing] extension that allows enabling publishing * of [testJar] artifact. * * This artifact contains compilation output of `test` source set. By default, it is not published. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt index 5dd8dc54e..c9fbdd651 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt @@ -236,7 +236,7 @@ sealed class PublicationHandler( * If the handler for the given [project] was already created, the handler * gets new [destinations], [overwriting][publishTo] previously specified. * - * @return the handler for the given project which would handle publishing to + * @return the handler for the given project that would handle publishing to * the specified [destinations]. */ fun serving(project: Project, destinations: Set, vararg params: Any): H { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt index 56221d57c..974542c3f 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt @@ -318,7 +318,7 @@ internal fun TaskContainer.getOrCreate(name: String, init: Jar.() -> Unit): Task * Obtains as a set of [Jar] tasks, output of which is used as Maven artifacts. * * By default, only a jar with Java compilation output is included into publication. This method - * registers tasks which produce additional artifacts according to the values of [jarFlags]. + * registers tasks that produce additional artifacts according to the values of [jarFlags]. * * @return the list of the registered tasks. */ diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt index a12c1d20f..bb412ed63 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt @@ -65,7 +65,7 @@ import org.gradle.kotlin.dsl.findByType * ### Filtering out test-only modules * * Sometimes a functional or an integration test requires a significant amount of - * configuration code which is better understood when isolated into a separate module. + * configuration code that is better understood when isolated into a separate module. * Conventionally, we use the `-tests` suffix for naming such modules. * * In order to avoid publishing of such a test-only module, we use the following extensions diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt index e6c3f677d..8877dc615 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ package io.spine.gradle.repo import io.spine.gradle.publish.PublishingRepos import java.net.URI import org.gradle.api.artifacts.dsl.RepositoryHandler +import org.gradle.api.artifacts.repositories.ArtifactRepository import org.gradle.api.artifacts.repositories.MavenArtifactRepository import org.gradle.kotlin.dsl.maven @@ -94,24 +95,34 @@ fun RepositoryHandler.spineArtifacts(): MavenArtifactRepository = maven { } val RepositoryHandler.intellijReleases: MavenArtifactRepository - get() = maven("https://www.jetbrains.com/intellij-repository/releases") - -val RepositoryHandler.jetBrainsCacheRedirector: MavenArtifactRepository - get() = maven("https://cache-redirector.jetbrains.com/intellij-dependencies") + get() = maven("https://www.jetbrains.com/intellij-repository/releases") { + includeIntelliJPlatformOnly() + } val RepositoryHandler.intellijDependencies: MavenArtifactRepository get() = maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") { - content { - includeGroupByRegex("com\\.jetbrains.*") - includeGroupByRegex("org\\.jetbrains.*") - includeGroupByRegex("com\\.intellij.*") - } + includeIntelliJPlatformOnly() } /** * Applies repositories commonly used by Spine Event Engine projects. */ fun RepositoryHandler.standardToSpineSdk() { + // + // General-purpose, highly available repositories come first. Gradle stops at + // the first repository that can serve an artifact, so keeping these ahead of + // the special-purpose ones means coordinates shared with them (such as + // `org.jetbrains:annotations`) resolve here and never reach a less reliable + // JetBrains mirror. + // + // `io.spine.*` modules are served only by the Spine repositories below, so + // they are excluded here. Otherwise Gradle would query Central / the Plugin + // Portal for every Spine module first, adding pointless lookups and making + // Spine resolution depend on the health of repositories that never host it. + // + mavenCentral { excludeSpine() } + gradlePluginPortal { excludeSpine() } + spineArtifacts() @Suppress("DEPRECATION") // Still use `CloudRepo` for earlier versions. @@ -131,16 +142,22 @@ fun RepositoryHandler.standardToSpineSdk() { } } + // IntelliJ Platform repositories. `intellijReleases` serves the platform + // artifacts (`com.jetbrains.intellij.*`); `intellijDependencies` serves the + // repackaged third-party dependencies (`org.jetbrains.intellij.deps.*` and + // JetBrains-internal builds). Each is restricted to the coordinates it serves + // (see `includeIntelliJPlatformOnly`), so a transient 5xx from one of them + // cannot break the resolution of unrelated artifacts. intellijReleases - jetBrainsCacheRedirector intellijDependencies maven { url = URI(Repos.sonatypeSnapshots) + // This repository only ever serves snapshots; restrict it so it is not + // queried (and cannot fail the build) for release artifacts. + mavenContent { snapshotsOnly() } } - mavenCentral() - gradlePluginPortal() mavenLocal().includeSpineOnly() } @@ -180,3 +197,35 @@ private fun MavenArtifactRepository.includeSpineOnly() { includeGroupByRegex("io\\.spine.*") } } + +/** + * Excludes Spine artifact groups from this repository. + * + * `io.spine.*` modules are published only to the Spine repositories (each scoped + * via [includeSpineOnly]). Excluding them from a general-purpose repository keeps + * Gradle from querying it — and depending on its health — for coordinates it + * never hosts. + */ +private fun ArtifactRepository.excludeSpine() { + content { + excludeGroupByRegex("io\\.spine.*") + } +} + +/** + * Restricts a JetBrains/IntelliJ Platform repository to the coordinates it + * actually serves. + * + * These hosts periodically answer with HTTP 5xx. Once Gradle sees such an error, + * it disables the repository for the rest of the build and fails the resolution + * instead of falling back to another repository. Without this filter such a + * repository is queried for every artifact, so a single 502 on an unrelated POM + * (such as `com.fasterxml.jackson:jackson-parent`) would break the whole build. + */ +private fun MavenArtifactRepository.includeIntelliJPlatformOnly() { + content { + includeGroupByRegex("com\\.jetbrains.*") + includeGroupByRegex("org\\.jetbrains.*") + includeGroupByRegex("com\\.intellij.*") + } +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt index a586ffdb9..d345725b0 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,12 +33,12 @@ import org.gradle.api.Project /** * A Maven repository. * - * @param name The human-readable name which is also used in the publishing task names + * @param name The human-readable name that is also used in the publishing task names * for identifying the target repository. * The name must match the [regex]. * @param releases The URL for publishing release versions of artifacts. * @param snapshots The URL for publishing [snapshot][io.spine.gradle.isSnapshot] versions. - * @param credentialsFile The path to the file which contains the credentials for the registry. + * @param credentialsFile The path to the file that contains the credentials for the registry. * @param credentialValues The function to obtain an instance of [Credentials] from * a Gradle [Project], if [credentialsFile] is not specified. */ @@ -116,7 +116,7 @@ data class Repository( val password = properties.getProperty("user.password") return Credentials(username, password) } - + override fun equals(other: Any?): Boolean = when { this === other -> true other !is Repository -> false diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt index 596be9c68..aa7e65f44 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt @@ -64,7 +64,7 @@ import org.gradle.kotlin.dsl.the object LicenseReporter { /** - * The name of the Gradle task which generates the reports for a specific Gradle project. + * The name of the Gradle task that generates the reports for a specific Gradle project. */ private const val projectTaskName = "generateLicenseReport" @@ -96,6 +96,20 @@ object LicenseReporter { renderers = arrayOf(MarkdownReportRenderer(Paths.outputFilename)) } + + // The rendered report embeds the project's Maven coordinates — including its + // version — in the report header (see `Template.writeHeader`). The + // `generateLicenseReport` task is a `@CacheableTask` that keys its up-to-date check + // and build-cache entry on the resolved dependencies only, not on the project version. + // Without the version as an explicit input, a version-only change leaves the task + // `UP-TO-DATE` (or restorable from the build cache), so the report keeps the previous + // version while `pom.xml`, produced by an always-running task, is updated. Declaring + // the version as an input invalidates the cached output when it changes, so the report + // is regenerated. The value is read lazily so it reflects the version resolved at + // execution time, regardless of when `project.version` is assigned during configuration. + project.tasks.generateLicenseReport.configure { + inputs.property("projectVersion", project.provider { project.version.toString() }) + } } /** @@ -104,7 +118,7 @@ object LicenseReporter { * * The merge result is placed according to [Paths]. * - * Registers a `mergeAllLicenseReports` which is specified to be executed after `build`. + * Registers a `mergeAllLicenseReports` that is specified to be executed after `build`. */ fun mergeAllReports(project: Project) { val rootProject = project.rootProject diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt index 91247e242..e8518fd20 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -92,7 +92,7 @@ private fun MarkdownDocument.print( } /** - * Prints the URL to the project which provides the dependency. + * Prints the URL to the project that provides the dependency. * * If the passed project URL is `null` or empty, it is not printed. */ diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt index 1d1632752..3c750b31a 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt @@ -46,7 +46,7 @@ internal object Paths { * as the result of the [LicenseReporter] work. * * Its contents describe the licensing information for each of the Java dependencies - * which are referenced by Gradle projects in the repository. + * that are referenced by Gradle projects in the repository. */ internal const val outputFilename = "dependencies.md" diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt index 79d00c629..db2bf761a 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,9 @@ import org.gradle.kotlin.dsl.withGroovyBuilder * ``` * * When there are several versions of the same dependency, only the one with - * the newest version is retained. + * the newest version is retained. If the retained version is used in several + * configurations, the highest-ranking Maven scope is reported, e.g. `compile` + * wins over `test`. * * @see PomGenerator */ @@ -65,7 +67,7 @@ private constructor( internal companion object { /** - * Creates the `ProjectDependenciesAsXml` for the passed [project]. + * Creates the `DependencyWriter` for the passed [project]. */ fun of(project: Project): DependencyWriter { return DependencyWriter(project.dependencies()) @@ -75,7 +77,7 @@ private constructor( /** * Writes the dependencies in their `pom.xml` format to the passed [out] writer. * - *

Used writer will not be closed. + * The used writer will not be closed. */ fun writeXmlTo(out: Writer) { val xml = MarkupBuilder(out) @@ -86,7 +88,12 @@ private constructor( "dependency" { "groupId" { xml.text(dependency.group) } "artifactId" { xml.text(dependency.name) } - "version" { xml.text(dependency.version) } + // A BOM-managed dependency carries no explicit version. + // Omit the element rather than emit `null`, + // since `null` is not a valid Maven version. + dependency.version?.let { version -> + "version" { xml.text(version) } + } if (scopedDep.hasDefinedScope()) { "scope" { xml.text(scopedDep.scopeName()) } } @@ -170,17 +177,30 @@ private fun Dependency.isExternal(): Boolean { * But for our `pom.xml`, which has clearly representative character, a single version * of a dependency is quite enough. * + * Versions are compared by [VersionComparator] rather than as plain text, so `10.0.0` + * is recognized as newer than `9.2.0`, and `2.0.0-SNAPSHOT.100` — as newer + * than `2.0.0-SNAPSHOT.99`. + * + * When the newest version comes from several configurations, the occurrence with + * the highest-ranking Maven scope (as defined by [ScopedDependency.dependencyPriority]) + * is retained. For example, a dependency declared via `api` in one module and via + * `testImplementation` in another is reported with the `compile` scope, so a production + * dependency is not misrepresented as a test-scoped one. Likewise, an artifact coming + * from `compileOnly` or `annotationProcessor` in one module and from a test + * configuration in another is reported as `provided`. + * * The rejected duplicates are logged. */ private fun Project.deduplicate(dependencies: Set): List { - val groups = dependencies.distinctBy { it.gav } - .groupBy { it.run { "$group:$name" } } + val groups = dependencies.groupBy { it.run { "$group:$name" } } - logDuplicates(groups) + logDuplicates(groups.mapValues { (_, deps) -> deps.distinctBy { it.gav } }) - val filtered = groups.map { group -> - group.value.maxByOrNull { dep -> dep.version ?: "" } - }.filterNotNull() + val filtered = groups.values.map { sameArtifact -> + val newest = sameArtifact.maxWith(compareBy(VersionComparator) { it.version ?: "" }) + sameArtifact.filter { it.version == newest.version } + .minBy { it.scoped.dependencyPriority() } + } return filtered } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt index 7c67a32ca..c969ce500 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ package io.spine.gradle.report.pom import io.spine.gradle.report.pom.DependencyScope.compile import io.spine.gradle.report.pom.DependencyScope.provided import io.spine.gradle.report.pom.DependencyScope.runtime +import io.spine.gradle.report.pom.DependencyScope.system import io.spine.gradle.report.pom.DependencyScope.test import io.spine.gradle.report.pom.DependencyScope.undefined import org.gradle.api.artifacts.Configuration @@ -106,17 +107,18 @@ private constructor( configurationName.startsWith("test", ignoreCase = true) /** - * Performs comparison of {@code DependencyWithScope} instances according to these rules: + * Performs comparison of `ScopedDependency` instances according to these rules: * - * * Compares the scope of the dependency first. Dependency with lower scope priority - * number goes first. + * * Compares the scope of the dependency first. Dependency with a lower scope priority + * number goes first. * - * * For dependencies with same scope, does the lexicographical group name comparison. + * * For dependencies with the **same scope** does the lexicographical group + * name comparison. * - * * For dependencies within the same group, does the lexicographical artifact + * * For dependencies within the **same group**, does the lexicographical artifact * name comparison. * - * * For dependencies with the same artifact name, does the lexicographical artifact + * * For dependencies with the **same artifact name**, does the lexicographical artifact * version comparison. */ private val COMPARATOR: Comparator = @@ -138,7 +140,7 @@ private constructor( return dependency } - /** Obtains the scope name of this dependency .*/ + /** Obtains the scope name of this dependency. */ fun scopeName(): String { return scope.name } @@ -147,14 +149,24 @@ private constructor( * Obtains the layout priority of a scope. * * Layout priority determines what scopes come first in the generated `pom.xml` file. - * Dependencies with a lower priority number go on top. + * Dependencies with a lower priority number go on top, following the conventional + * Maven scope order: `compile`, `provided`, `runtime`, `test`, and `system`. + * Dependencies with an undefined scope go last. + * + * The same ordering also drives the scope selection when the same dependency + * comes from several configurations: the occurrence with the lowest priority + * number is reported. So, a scope required by production code wins over `test`, + * and a known scope wins over an undefined one. */ + @Suppress("MagicNumber") // Reason: the values encode the relative scope order. internal fun dependencyPriority(): Int { return when (scope) { compile -> 0 - runtime -> 1 - test -> 2 - else -> 3 + provided -> 1 + runtime -> 2 + test -> 3 + system -> 4 + undefined -> 5 } } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/VersionComparator.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/VersionComparator.kt new file mode 100644 index 000000000..c6984bb65 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/VersionComparator.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.report.pom + +/** + * Compares dependency version strings by their meaning rather than lexicographically. + * + * Numeric segments are ordered as numbers, so `10.0.0` is newer than `9.2.0`, and + * `2.0.0-SNAPSHOT.100` is newer than `2.0.0-SNAPSHOT.99`. A plain `String` comparison + * would order both pairs the other way around. + * + * The rules follow Semantic Versioning where it applies: + * + * 1. A version consists of a release part and an optional qualifier, separated by + * the first `-`: for `2.0.0-SNAPSHOT.100` these are `2.0.0` and `SNAPSHOT.100`. + * 2. Both parts are compared segment by segment, as split by `.`, and also by `-` + * within a qualifier. Two numeric segments are compared as numbers, two textual + * ones as case-insensitive text, and a numeric segment is older than a textual one. + * 3. When one version runs out of segments, it is the older one: `1.0.1` is newer + * than `1.0`, and `1.0.0-RC.1` is newer than `1.0.0-RC`. + * 4. When the release parts are equal, a version without a qualifier is newer than + * a version with one: `2.0.0` is newer than `2.0.0-SNAPSHOT.100`. + * + * Unlike full Maven semantics, qualifiers carry no special meaning: `RC`, `SNAPSHOT`, + * and the like are ordered as plain text. This keeps the comparison simple and + * predictable for the report, where only the relative recency of the versions + * of the same artifact matters. + */ +internal object VersionComparator : Comparator { + + override fun compare(left: String, right: String): Int { + val (leftRelease, leftQualifier) = left.parse() + val (rightRelease, rightQualifier) = right.parse() + val byRelease = compareSegments(leftRelease, rightRelease) + if (byRelease != 0) { + return byRelease + } + return when { + leftQualifier == null && rightQualifier == null -> 0 + leftQualifier == null -> 1 + rightQualifier == null -> -1 + else -> compareSegments(leftQualifier, rightQualifier) + } + } + + /** + * Splits this version into the segments of its release part and the segments + * of its qualifier, the latter being `null` when the version has no qualifier. + */ + private fun String.parse(): Pair, List?> { + val release = substringBefore('-') + val qualifier = if ('-' in this) substringAfter('-') else null + return release.split('.') to qualifier?.split('.', '-') + } + + private fun compareSegments(left: List, right: List): Int { + for (index in 0 until maxOf(left.size, right.size)) { + val bySegment = compareSegment( + left.getOrElse(index) { "" }, + right.getOrElse(index) { "" } + ) + if (bySegment != 0) { + return bySegment + } + } + return 0 + } + + /** + * Compares single segments, ordering an absent (empty) segment below any present + * one, a numeric segment below a textual one, numbers by their value, and text + * case-insensitively. + * + * Keeping the empty, numeric, and textual segments in distinct buckets makes + * the order transitive: comparing a numeric pair as numbers, but a mixed pair + * as text, would order `2` < `10` < `1a` < `2`. + */ + private fun compareSegment(left: String, right: String): Int { + if (left.isEmpty() || right.isEmpty()) { + return left.length.compareTo(right.length) + } + val leftNumber = left.toLongOrNull() + val rightNumber = right.toLongOrNull() + return when { + leftNumber != null && rightNumber != null -> leftNumber.compareTo(rightNumber) + leftNumber != null -> -1 + rightNumber != null -> 1 + else -> left.compareTo(right, ignoreCase = true) + } + } +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt index 96f90bb2b..9682596d8 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt @@ -127,7 +127,7 @@ internal const val TESTKIT_COVERAGE_DIR: String = "jacoco-testkit" /** * The name of the system property carrying the absolute path to the JaCoCo - * agent JAR which the test harness attaches to TestKit worker JVMs. + * agent JAR that the test harness attaches to TestKit worker JVMs. * * The value is read by `plugin-testlib` at test runtime. * diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt new file mode 100644 index 000000000..0c4b23355 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt @@ -0,0 +1,312 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.report.pom + +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.ints.shouldBeLessThan +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain +import java.io.StringWriter +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@DisplayName("`DependencyWriter` should") +internal class DependencyWriterSpec { + + private val rootProject: Project = ProjectBuilder.builder().withName("root").build() + + /** + * Creates a subproject of the [rootProject] with the given name. + * + * The names of the subprojects in the tests below are chosen so that + * a module using a dependency in a lower-ranked scope — as defined by + * [ScopedDependency.dependencyPriority] — sorts first, and is thus + * encountered first when the dependencies are collected. This way, + * the tests prove that the merged scope does not depend on the order + * in which project modules are traversed. + */ + private fun subproject(name: String): Project = + ProjectBuilder.builder().withParent(rootProject).withName(name).build() + + /** + * Declares a dependency with the given [notation] in the named [configuration], + * creating it if it does not exist. + */ + private fun Project.declare(configuration: String, notation: String) { + configurations.maybeCreate(configuration) + dependencies.add(configuration, notation) + } + + @Nested inner class + `merge an artifact duplicated across modules` { + + @Test + fun `preferring the 'compile' scope over the 'test' one`() { + subproject("a-tests").declare("testImplementation", SPINE_BASE) + subproject("b-lib").declare("api", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.scopeName() shouldBe DependencyScope.compile.name + } + + @Test + fun `preferring the 'runtime' scope over the 'test' one`() { + subproject("a-tests").declare("testImplementation", SPINE_BASE) + subproject("b-lib").declare("runtimeOnly", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.scopeName() shouldBe DependencyScope.runtime.name + } + + @Test + fun `preferring the 'compile' scope over the 'runtime' one`() { + subproject("a-run").declare("runtimeOnly", SPINE_BASE) + subproject("b-lib").declare("implementation", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.scopeName() shouldBe DependencyScope.compile.name + } + + @Test + fun `preferring the 'provided' scope over the 'test' one`() { + subproject("a-tests").declare("testImplementation", SPINE_BASE) + subproject("b-lib").declare("compileOnly", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.scopeName() shouldBe DependencyScope.provided.name + } + + @Test + fun `reporting 'annotationProcessor' and 'testAnnotationProcessor' usages as 'provided'`() { + subproject("a-tests").declare("testAnnotationProcessor", SPINE_BASE) + subproject("b-codegen").declare("annotationProcessor", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.scopeName() shouldBe DependencyScope.provided.name + } + + @Test + fun `preferring the 'compile' scope over the 'provided' one`() { + subproject("a-tools").declare("compileOnly", SPINE_BASE) + subproject("b-lib").declare("implementation", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.scopeName() shouldBe DependencyScope.compile.name + } + + @Test + fun `preferring the 'provided' scope over the 'runtime' one`() { + subproject("a-run").declare("runtimeOnly", SPINE_BASE) + subproject("b-tools").declare("compileOnly", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.scopeName() shouldBe DependencyScope.provided.name + } + + @Test + fun `retaining the newest version with the widest of its scopes`() { + subproject("a-tests").declare("testImplementation", SPINE_BASE_NEWER) + subproject("b-lib").declare("api", SPINE_BASE_NEWER) + subproject("c-old").declare("api", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.dependency().version shouldBe "2.0.1" + dependency.scopeName() shouldBe DependencyScope.compile.name + } + + @Test + fun `comparing versions semantically rather than as text`() { + subproject("a-lib").declare("api", "io.spine:spine-base:9.2.0") + subproject("b-lib").declare("api", "io.spine:spine-base:10.0.0") + + val dependency = rootProject.dependencies().single() + + dependency.dependency().version shouldBe "10.0.0" + } + + @Test + fun `ordering pre-release increments numerically`() { + subproject("a-old").declare("api", "io.spine:spine-base:2.0.0-SNAPSHOT.99") + subproject("b-new").declare("api", "io.spine:spine-base:2.0.0-SNAPSHOT.100") + + val dependency = rootProject.dependencies().single() + + dependency.dependency().version shouldBe "2.0.0-SNAPSHOT.100" + } + + @Test + fun `preferring a release over its pre-release`() { + subproject("a-snapshot").declare("api", "io.spine:spine-base:2.0.0-SNAPSHOT.100") + subproject("b-release").declare("api", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.dependency().version shouldBe "2.0.0" + } + + /** + * The `api` usage of the older `9.2.0` must affect neither the version + * nor the scope: both come from the usages of the newest `10.0.0`, + * which would lose to `9.2.0` in a plain text comparison. + */ + @Test + fun `taking the widest scope from the usages of the numerically newest version`() { + subproject("a-lib").declare("api", "io.spine:spine-base:9.2.0") + subproject("b-tests").declare("testImplementation", "io.spine:spine-base:10.0.0") + subproject("c-run").declare("runtimeOnly", "io.spine:spine-base:10.0.0") + + val dependency = rootProject.dependencies().single() + + dependency.dependency().version shouldBe "10.0.0" + dependency.scopeName() shouldBe DependencyScope.runtime.name + } + + /** + * When the newest version of an artifact occurs only in test configurations, + * the `test` scope is reported even if an older version is a production + * dependency: the report describes the retained version as it is used. + */ + @Test + fun `taking the scope only from the usages of the newest version`() { + subproject("a-tests").declare("testImplementation", SPINE_BASE_NEWER) + subproject("b-lib").declare("api", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.dependency().version shouldBe "2.0.1" + dependency.scopeName() shouldBe DependencyScope.test.name + } + + @Test + fun `keeping the 'test' scope for an artifact used only in tests`() { + subproject("a-tests").declare("testImplementation", SPINE_BASE) + subproject("b-tests").declare("testRuntimeOnly", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.scopeName() shouldBe DependencyScope.test.name + } + + @Test + fun `preferring a known scope over that of an unknown configuration`() { + subproject("a-tools").declare("spineCompiler", SPINE_BASE) + subproject("b-tests").declare("testImplementation", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.hasDefinedScope() shouldBe true + dependency.scopeName() shouldBe DependencyScope.test.name + } + + @Test + fun `preferring the 'provided' scope over that of an unknown configuration`() { + subproject("a-tools").declare("spineCompiler", SPINE_BASE) + subproject("b-lib").declare("compileOnly", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.hasDefinedScope() shouldBe true + dependency.scopeName() shouldBe DependencyScope.provided.name + } + } + + @Test + fun `omit the scope of a dependency coming only from an unknown configuration`() { + subproject("lib").declare("spineCompiler", SPINE_BASE) + + val dependency = rootProject.dependencies().single() + + dependency.hasDefinedScope() shouldBe false + } + + @Test + fun `omit the version of a dependency that declares none`() { + subproject("a-bom").declare("api", "io.grpc:grpc-stub") + subproject("b-lib").declare("api", SPINE_BASE) + + val out = StringWriter() + DependencyWriter.of(rootProject).writeXmlTo(out) + val xml = out.toString() + + xml shouldContain "grpc-stub" + xml shouldNotContain "null" + xml shouldContain "2.0.0" + } + + @Test + fun `write a production dependency as 'compile' even when it is also used in tests`() { + subproject("a-tests").declare("testImplementation", SPINE_BASE) + subproject("b-lib").declare("api", SPINE_BASE) + + val out = StringWriter() + DependencyWriter.of(rootProject).writeXmlTo(out) + val xml = out.toString() + + xml shouldContain "spine-base" + xml shouldContain "compile" + xml shouldNotContain "test" + } + + @Test + fun `lay out dependencies in the conventional Maven scope order`() { + subproject("a-tests").declare("testImplementation", "io.spine:spine-testlib:2.0.0") + subproject("b-run").declare("runtimeOnly", "io.spine:spine-logging:2.0.0") + subproject("c-tools").declare("annotationProcessor", "io.spine:spine-validate:2.0.0") + subproject("d-lib").declare("api", SPINE_BASE) + + val out = StringWriter() + DependencyWriter.of(rootProject).writeXmlTo(out) + val xml = out.toString() + + val compileAt = xml.indexOf("compile") + val providedAt = xml.indexOf("provided") + val runtimeAt = xml.indexOf("runtime") + val testAt = xml.indexOf("test") + + compileAt shouldBeGreaterThan -1 + compileAt shouldBeLessThan providedAt + providedAt shouldBeLessThan runtimeAt + runtimeAt shouldBeLessThan testAt + } + + private companion object { + const val SPINE_BASE = "io.spine:spine-base:2.0.0" + const val SPINE_BASE_NEWER = "io.spine:spine-base:2.0.1" + } +} diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/VersionComparatorSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/VersionComparatorSpec.kt new file mode 100644 index 000000000..54b643950 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/report/pom/VersionComparatorSpec.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.report.pom + +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.ints.shouldBeLessThan +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`VersionComparator` should") +internal class VersionComparatorSpec { + + /** + * Asserts that [newer] compares above [older], checking both directions. + */ + private fun assertNewer(newer: String, older: String) { + VersionComparator.compare(newer, older) shouldBeGreaterThan 0 + VersionComparator.compare(older, newer) shouldBeLessThan 0 + } + + @Test + fun `compare numeric segments as numbers`() { + assertNewer("10.0.0", "9.2.0") + assertNewer("2.10.0", "2.9.1") + assertNewer("1.0.10", "1.0.9") + } + + @Test + fun `compare numeric qualifier segments as numbers`() { + assertNewer("2.0.0-SNAPSHOT.100", "2.0.0-SNAPSHOT.99") + assertNewer("2.0.0-SNAPSHOT.100", "2.0.0-SNAPSHOT.070") + } + + @Test + fun `treat a release as newer than its pre-release`() { + assertNewer("2.0.0", "2.0.0-SNAPSHOT.100") + assertNewer("1.0.0", "1.0.0-RC.2") + } + + @Test + fun `treat a longer version as newer when the common segments are equal`() { + assertNewer("1.0.1", "1.0") + assertNewer("1.0.0-RC.1", "1.0.0-RC") + } + + @Test + fun `ignore the case of textual segments`() { + assertNewer("1.0.0-snapshot.10", "1.0.0-SNAPSHOT.2") + VersionComparator.compare("1.0.0-RC", "1.0.0-rc") shouldBe 0 + } + + @Test + fun `order a numeric segment before a textual one`() { + assertNewer("1.0.0-alpha", "1.0.0-1") + } + + @Test + fun `treat equal versions as equal`() { + VersionComparator.compare("2.0.0-SNAPSHOT.070", "2.0.0-SNAPSHOT.070") shouldBe 0 + VersionComparator.compare("31.1-jre", "31.1-jre") shouldBe 0 + } +} diff --git a/config b/config index 234233e0f..89ddc7591 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 234233e0fa407df296ff4742887723653c3dcc95 +Subproject commit 89ddc75919d75cc9ceee8822c2fdc93a24784e8b diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index c935deb65..d0ec489fe 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.tools:classic-codegen:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:classic-codegen:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -828,14 +828,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-plugin-api:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:gradle-plugin-api:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -1734,14 +1734,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-plugin-api-test-fixtures:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:gradle-plugin-api-test-fixtures:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -2212,14 +2212,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:54 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-root-plugin:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:gradle-root-plugin:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -3070,14 +3070,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:intellij-platform:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:intellij-platform:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -4151,14 +4151,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -5930,14 +5930,14 @@ This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:56 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:jvm-tool-plugins:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:jvm-tool-plugins:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -6780,14 +6780,14 @@ This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:jvm-tools:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:jvm-tools:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0. @@ -7547,14 +7547,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:plugin-base:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:plugin-base:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -8405,14 +8405,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:plugin-testlib:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:plugin-testlib:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : com.google.auto.value. **Name** : auto-value-annotations. **Version** : 1.11.1. @@ -9367,14 +9367,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:protobuf-setup-plugins:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:protobuf-setup-plugins:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -10237,14 +10237,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:psi:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:psi:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -11345,14 +11345,14 @@ This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:psi-java:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:psi-java:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -13167,14 +13167,14 @@ This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:56 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:tool-base:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:tool-base:2.0.0-SNAPSHOT.401` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -14054,6 +14054,6 @@ This report was generated on **Wed Jun 10 18:49:06 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jun 10 18:49:05 WEST 2026** using +This report was generated on **Fri Jun 19 11:10:55 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index a7b7e58f0..43922c44a 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine.tools tool-base -2.0.0-SNAPSHOT.400 +2.0.0-SNAPSHOT.401 2015 @@ -35,6 +35,12 @@ all modules and does not describe the project structure per-subproject. 33.6.0-jre compile + + com.google.protobuf + protobuf-gradle-plugin + 0.10.0 + compile + com.google.protobuf protobuf-java @@ -170,7 +176,7 @@ all modules and does not describe the project structure per-subproject. io.spine spine-base - 2.0.0-SNAPSHOT.404 + 2.0.0-SNAPSHOT.420 compile @@ -179,6 +185,12 @@ all modules and does not describe the project structure per-subproject. 2.0.0-SNAPSHOT.417 compile + + io.spine.tools + base-testlib + 2.0.0-SNAPSHOT.213 + compile + org.jboss.forge.roaster roaster-api @@ -197,6 +209,12 @@ all modules and does not describe the project structure per-subproject. 2.3.21 compile + + org.jetbrains.kotlin + kotlin-reflect + 2.3.21 + compile + org.jetbrains.kotlin kotlin-stdlib @@ -221,6 +239,42 @@ all modules and does not describe the project structure per-subproject. 1.0.0 compile + + org.junit.jupiter + junit-jupiter-api + 6.1.0 + compile + + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + com.google.errorprone + error_prone_annotations + 2.36.0 + provided + + + com.google.errorprone + error_prone_type_annotations + 2.36.0 + provided + + + org.checkerframework + checker-qual + 4.2.0 + provided + + + org.jetbrains.kotlin + kotlin-gradle-plugin-api + 2.3.21 + provided + com.google.guava guava-testlib @@ -258,9 +312,9 @@ all modules and does not describe the project structure per-subproject. test - io.spine.tools - base-testlib - 2.0.0-SNAPSHOT.213 + org.jacoco + org.jacoco.agent + 0.8.15 test @@ -275,12 +329,6 @@ all modules and does not describe the project structure per-subproject. 2.3.0 test - - org.junit.jupiter - junit-jupiter-api - 6.1.0 - test - org.junit.jupiter junit-jupiter-engine @@ -293,35 +341,11 @@ all modules and does not describe the project structure per-subproject. 6.1.0 test - - com.google.code.findbugs - jsr305 - 3.0.2 - provided - - - com.google.errorprone - error_prone_annotations - 2.36.0 - provided - com.google.errorprone error_prone_core 2.36.0 - - com.google.errorprone - error_prone_type_annotations - 2.36.0 - provided - - - com.google.protobuf - protobuf-gradle-plugin - 0.10.0 - provided - com.google.protobuf protoc @@ -362,17 +386,6 @@ all modules and does not describe the project structure per-subproject. pmd-java 7.25.0 - - org.checkerframework - checker-qual - 4.2.0 - provided - - - org.jacoco - org.jacoco.agent - 0.8.15 - org.jacoco org.jacoco.report @@ -423,17 +436,6 @@ all modules and does not describe the project structure per-subproject. kotlin-build-tools-impl 2.3.21 - - org.jetbrains.kotlin - kotlin-gradle-plugin-api - 2.3.21 - provided - - - org.jetbrains.kotlin - kotlin-reflect - 2.3.21 - org.jetbrains.kotlin kotlin-sam-with-receiver-compiler-plugin-embeddable diff --git a/gradle.properties b/gradle.properties index d9d857b8d..7c2bb5f20 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,18 +10,26 @@ org.gradle.parallel=true # Reuse task outputs from the local build cache. # On CI, `gradle/actions/setup-gradle` persists `caches/build-cache-1` across runs, # so cold builds skip work whose inputs are unchanged. -# -# Disabled for now: this repository's own build applies the *published* -# `protobuf-setup-plugins` (see the root `build.gradle.kts` buildscript classpath), -# which does not yet declare `generated/` and `desc.ref` as outputs of -# `generateProto`. With the cache on, a `clean build` restores `generateProto` from -# the cache without re-running its `doLast` actions, leaving the generated code -# missing (e.g., in `classic-codegen`). Re-enable after `ToolBase.version` in -# `buildSrc` points to a version containing the fix. -#org.gradle.caching=true +org.gradle.caching=true -# Dokka plugin eats more memory than usual. Therefore, all builds should have enough. -org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseParallelGC -Dfile.encoding=UTF-8 +# Extra JVM args for the Gradle daemon, for two unrelated reasons: +# +# 1. The Dokka plugin eats more memory than usual, so all builds get a generous heap. +# 2. The `--add-exports` / `--add-opens` flags expose the `jdk.compiler` internals that +# Error Prone needs on JDK 16+ (JEP 396). Passing them to the daemon here lets the +# `net.ltgt.errorprone` plugin run Error Prone in-process instead of forking a separate +# compiler JVM per task. See https://github.com/SpineEventEngine/config/issues/543 +org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseParallelGC -Dfile.encoding=UTF-8 \ + --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ + --add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \ + --add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED \ + --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ + --add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \ + --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED # suppress inspection "UnusedProperty" # The below property enables generation of XML reports for tests. diff --git a/init-submodules b/init-submodules new file mode 100755 index 000000000..0c12a2817 --- /dev/null +++ b/init-submodules @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +################################################################################ +# +# Materialize the *config-managed* submodules a fresh working tree is missing, so +# agent assets resolve. +# +# `git worktree add` — and some shallow CI / cloud checkouts — populate only the +# superproject's own tracked files; registered submodules are left UNinitialized. +# In a Spine repo that means the `config` and `.agents/shared` submodules are +# empty, the `.agents/skills` -> `.agents/shared/skills` symlink dangles, and no +# agent skills, scripts, or guidelines can be found. +# +# This script is the bootstrap that has to run BEFORE `./config/pull`: `pull` +# lives inside the `config` submodule, so on a fresh worktree it does not yet +# exist. `init-submodules`, by contrast, is a plain tracked file at the repo root +# (distributed by `config`), so `git worktree add` always checks it out — it can +# therefore bring `config` itself into existence. +# +# It initializes ONLY submodules that are BOTH: +# +# * not yet checked out — those `git submodule status` marks with a leading `-`, +# at the commit the branch pins; and +# +# * config-managed — `config` itself (the bootstrap target `pull` lives inside, +# which carries no tracked `branch` in a consumer's `.gitmodules`), plus every +# submodule that declares a tracked `branch` in `.gitmodules`. This is exactly +# the rule `./config/pull` uses to decide what it floats, so the two scripts +# can never disagree about what is shared. +# +# Consumer-owned submodules (a Hugo theme, a vendored library, documentation +# examples, ...) declare no tracked branch and are deliberately left untouched. +# Because a `SessionStart` hook runs this script automatically on every session, +# initializing them would mean trying to clone — or failing on credentials for — +# a submodule this project does not manage, on every single start. They are +# skipped (noted on stderr). +# +# Submodules already present are left exactly as they are, so a tree that floated +# `config` / `.agents/shared` to a branch tip via `./config/pull` is never +# silently rewound to the pin. That makes the script idempotent and safe to run on +# every session start. +# +# It does NOT float submodules to their branch tips — run `./config/pull` +# afterwards for that. Unlike `pull`, it depends on no pre-existing `config` +# submodule, so it can bootstrap a bare worktree where `./config/pull` does not +# yet exist. +# +################################################################################ + +set -u + +root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 +cd "$root" || exit 0 + +# Nothing to do in a repo without submodules. +[ -f .gitmodules ] || exit 0 + +# The set of config-managed submodule paths: `config` itself (handled specially — +# it carries no tracked branch, exactly as in `./config/pull`), plus every +# submodule declaring a tracked `branch` in `.gitmodules`. Mirrors `pull`'s rule. +config_managed_paths() { + printf '%s\n' 'config' + git config -f .gitmodules --get-regexp '^submodule\..*\.branch$' 2>/dev/null \ + | while read -r key _branch; do + name=${key#submodule.}; name=${name%.branch} + git config -f .gitmodules --get "submodule.$name.path" 2>/dev/null + done +} + +managed=$(config_managed_paths | sort -u) + +# `git submodule status` prefixes each uninitialized submodule with `-`; an +# initialized one starts with a space (at the pinned commit) or `+` (ahead of it). +# Act only on the `-` lines, taking the path from the second field, and only when +# that path is config-managed. +git submodule status 2>/dev/null | awk '$1 ~ /^-/ { print $2 }' | while read -r path; do + [ -n "$path" ] || continue + if printf '%s\n' "$managed" | grep -qxF -- "$path"; then + echo "init-submodules: initializing '$path'" + git submodule update --init --recursive -- "$path" \ + || echo "init-submodules: WARNING — could not initialize '$path' (offline?)." >&2 + else + echo "init-submodules: skipping consumer-owned '$path' (not config-managed)." >&2 + fi +done + +exit 0 diff --git a/version.gradle.kts b/version.gradle.kts index 30c92175f..be50dc957 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -24,4 +24,4 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -val versionToPublish: String by extra("2.0.0-SNAPSHOT.400") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.401")