diff --git a/.agents/tasks/938-move-codegen-request-writer.md b/.agents/tasks/938-move-codegen-request-writer.md new file mode 100644 index 0000000000..476ce91fc8 --- /dev/null +++ b/.agents/tasks/938-move-codegen-request-writer.md @@ -0,0 +1,46 @@ +--- +slug: 938-move-codegen-request-writer +branch: claude/busy-dirac-wwflbm +owner: claude +status: in-progress +started: 2026-06-10 +--- + +## Goal + +`io.spine.code.proto.CodeGeneratorRequestWriter` is removed from `base` +(it is protoc-plugin tooling, not runtime API), and the build is green. +Closes [#938](https://github.com/SpineEventEngine/base-libraries/issues/938) +together with the receiving change in `tool-base`. + +## Context + +- The class moves to the `tool-base` module of the ToolBase repository under + `io.spine.tools.code.proto` (same-named branch there). +- The only consumers are the protoc-plugin entry points of the Compiler and + ProtoTap; they migrate by switching the import once both PRs are published. +- `CodeGeneratorRequestParsingSpec.kt` and `CodeGeneratorRequestsJavaSpec.java` + stay: they test `io.spine.type` parsing APIs which remain in `base`, and the + Java spec still uses the `constructRequest` helper declared in the former. +- Removing public API is a breaking change: the snapshot version advances to + the next multiple of 10. + +## Plan + +- [x] Remove `base/src/main/kotlin/io/spine/code/proto/CodeGeneratorRequestWriter.kt`. +- [x] Remove `base/src/test/kotlin/io/spine/code/proto/CodeGeneratorRequestWriterSpec.kt`. +- [x] Bump version `2.0.0-SNAPSHOT.404` -> `2.0.0-SNAPSHOT.410` (breaking). +- [ ] `./gradlew build` green; commit regenerated dependency reports if any. + - Blocked in the sandbox: all Spine artifact repositories return 403 for + the buildscript dependency `io.spine.tools:protobuf-setup-plugins`, so + no Gradle build can run here at all. Verification is delegated to PR CI. + - Repo-wide greps confirm no remaining references to the removed class; + the surviving `CodeGeneratorRequest*` specs do not use it. +- [x] Push and open a draft PR; merge after the tool-base PR. + +## Log + +- 2026-06-10 — drafted; executing autonomously per issue #938. +- 2026-06-10 — removal committed and version bumped; sandbox cannot resolve + Spine snapshot artifacts (403 on all repos), so the build runs on PR CI + instead. diff --git a/.github/workflows/build-on-ubuntu.yml b/.github/workflows/build-on-ubuntu.yml index 03be0f5326..27d9302b9d 100644 --- a/.github/workflows/build-on-ubuntu.yml +++ b/.github/workflows/build-on-ubuntu.yml @@ -12,11 +12,12 @@ jobs: with: submodules: 'true' - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: 17 distribution: zulu - cache: gradle + + - uses: gradle/actions/setup-gradle@v6 - name: Build project and run tests shell: bash diff --git a/.github/workflows/build-on-windows.yml b/.github/workflows/build-on-windows.yml index de72407ede..992272ce4f 100644 --- a/.github/workflows/build-on-windows.yml +++ b/.github/workflows/build-on-windows.yml @@ -21,11 +21,12 @@ jobs: submodules: recursive fetch-depth: 0 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: 17 distribution: zulu - cache: gradle + + - uses: gradle/actions/setup-gradle@v6 # See: https://github.com/al-cheb/configure-pagefile-action - name: Configure Pagefile @@ -33,8 +34,7 @@ jobs: - name: Build project and run tests shell: cmd - # For the reason on `--no-daemon` see https://github.com/actions/cache/issues/454 - run: gradlew.bat build --stacktrace --no-daemon + run: gradlew.bat build --stacktrace # See: https://github.com/marketplace/actions/junit-report-action - name: Publish Test Report diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 3fa235be0b..6755ff910e 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -33,7 +33,7 @@ jobs: cancel-in-progress: true steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Detect the Hugo site root (`docs/` or `site/`) by looking for a Hugo # config file. Hugo config may live directly in the site root or in a diff --git a/.github/workflows/ensure-reports-updated.yml b/.github/workflows/ensure-reports-updated.yml index 315cd202b7..20deb3f695 100644 --- a/.github/workflows/ensure-reports-updated.yml +++ b/.github/workflows/ensure-reports-updated.yml @@ -1,19 +1,29 @@ # Ensures that the license report files were modified in this PR. +# +# The check runs only for pull requests targeting a default (`master`/`main`) or +# a release-line (e.g. `2.x-jdk8-master`) branch. The report files embed the project +# version, so they are refreshed by the branches which bump it. Pull requests +# targeting auxiliary branches are not checked. +# +# The base branch is checked inside the job rather than via the `branches` filter: +# a workflow skipped by branch filtering leaves its check in the `Pending` state, +# blocking PRs which require it, while a job skipped via `if` reports `skipped`, +# which satisfies required status checks. name: License Reports on: pull_request: - branches: - - '**' jobs: check: name: Ensure license reports are updated runs-on: ubuntu-latest + # Default and release-line branches, e.g. `master`, `main`, `2.x-jdk8-master`. + if: endsWith(github.base_ref, 'master') || endsWith(github.base_ref, 'main') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: # Configure the checkout of all branches so that it is possible to run the comparison. fetch-depth: 0 diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 50eb05eb15..fc0872b4c9 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout latest code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Validate Gradle Wrapper uses: gradle/actions/wrapper-validation@v4 diff --git a/.github/workflows/increment-guard.yml b/.github/workflows/increment-guard.yml index 38ce6f4d3e..f20b4bed6e 100644 --- a/.github/workflows/increment-guard.yml +++ b/.github/workflows/increment-guard.yml @@ -1,28 +1,39 @@ -# Ensures that the current lib version is not yet published but executing the Gradle +# Ensures that the current lib version is not yet published by executing the Gradle # `checkVersionIncrement` task. +# +# The check runs only for pull requests targeting a default (`master`/`main`) or +# a release-line (e.g. `2.x-jdk8-master`) branch. It is the responsibility of a branch +# which aims to merge into such a branch to bump the version. Auxiliary branches +# do not deal with the versions in the release cycle and are not guarded. +# +# The base branch is checked inside the job rather than via the `branches` filter: +# a workflow skipped by branch filtering leaves its check in the `Pending` state, +# blocking PRs which require it, while a job skipped via `if` reports `skipped`, +# which satisfies required status checks. name: Version Guard on: - push: - branches: - - '**' + pull_request: jobs: check: name: Check version increment runs-on: ubuntu-latest + # Default and release-line branches, e.g. `master`, `main`, `2.x-jdk8-master`. + if: endsWith(github.base_ref, 'master') || endsWith(github.base_ref, 'main') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'true' - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: 17 distribution: zulu - cache: gradle + + - uses: gradle/actions/setup-gradle@v6 - name: Check version is not yet published shell: bash diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f7218c618e..df8f6cd01a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,15 +10,16 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'true' - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: 17 distribution: zulu - cache: gradle + + - uses: gradle/actions/setup-gradle@v6 - name: Decrypt CloudRepo credentials run: ./config/scripts/decrypt.sh "$CLOUDREPO_CREDENTIALS_KEY" ./.github/keys/cloudrepo.properties.gpg ./cloudrepo.properties diff --git a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml index f706171007..62242c7bbb 100644 --- a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml +++ b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml @@ -39,7 +39,7 @@ jobs: outputs: package-names: ${{ steps.request-package-names.outputs.package-names }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'true' diff --git a/.idea/misc.xml b/.idea/misc.xml index c6c419e979..264f823015 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -21,39 +21,24 @@ - - \ No newline at end of file + + diff --git a/base/src/main/java/io/spine/code/proto/FileName.java b/base/src/main/java/io/spine/code/proto/FileName.java index 4f595f6d92..dbbb5d3325 100644 --- a/base/src/main/java/io/spine/code/proto/FileName.java +++ b/base/src/main/java/io/spine/code/proto/FileName.java @@ -1,11 +1,11 @@ /* - * Copyright 2022, 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 @@ -105,7 +105,7 @@ private String nameOnly() { } /** - * Returns the file name with extension but without path. + * Returns the file name with an extension but without a path. */ public String nameWithExtension() { var fullName = value(); diff --git a/base/src/main/kotlin/io/spine/code/proto/CodeGeneratorRequestWriter.kt b/base/src/main/kotlin/io/spine/code/proto/CodeGeneratorRequestWriter.kt deleted file mode 100644 index afa339f294..0000000000 --- a/base/src/main/kotlin/io/spine/code/proto/CodeGeneratorRequestWriter.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2024, 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.code.proto - -import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest -import io.spine.io.replaceExtension -import io.spine.string.decodeBase64 -import io.spine.type.ExtensionRegistryHolder.extensionRegistry -import io.spine.type.parse -import io.spine.type.toJson -import java.io.File -import java.io.InputStream -import java.nio.file.StandardOpenOption.CREATE -import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING -import kotlin.io.path.writeBytes - -/** - * Parses a [CodeGeneratorRequest] from given [input] and writes it into - * files in [binary][writeBinary] and [JSON][writeJson] format. - * - * @param input The input stream containing binary version of the request. - */ -public class CodeGeneratorRequestWriter( - private val input: InputStream -) { - /** - * Lazily evaluated [CodeGeneratorRequest] parsed from [input] using [extensionRegistry]. - */ - public val request: CodeGeneratorRequest by lazy { - CodeGeneratorRequest::class.parse(input) - } - - /** - * The target file for writing the request in the binary form. - * - * The name of the request is passed as the [parameter][CodeGeneratorRequest.getParameter] of - * the request as a Base64 encoded file path. - */ - public val requestFile: File by lazy { - File(request.parameter.decodeBase64()) - } - - /** - * The path to the request file in JSON format. - * - * The file has the same name as [requestFile] and the extension of `".pb.json"`. - */ - public val requestFileInJson: File by lazy { - requestFile.replaceExtension("pb.json") - } - - /** - * Writes the request into the location specified in [requestFile]. - */ - public fun writeBinary() { - ensureDirectory() - requestFile.toPath().writeBytes(request.toByteArray(), CREATE, TRUNCATE_EXISTING) - } - - /** - * Writes the request in JSON format to the location specified in [requestFileInJson]. - */ - public fun writeJson() { - val json = request.toJson() - ensureDirectory() - requestFileInJson.writeText(json) - } - - private fun ensureDirectory() { - val targetDir = requestFile.parentFile - targetDir.mkdirs() - } -} diff --git a/base/src/test/kotlin/io/spine/base/MessageFileSpec.kt b/base/src/test/kotlin/io/spine/base/MessageFileSpec.kt new file mode 100644 index 0000000000..47a6f1d634 --- /dev/null +++ b/base/src/test/kotlin/io/spine/base/MessageFileSpec.kt @@ -0,0 +1,63 @@ +/* + * 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.base + +import com.google.protobuf.Any +import com.google.protobuf.DescriptorProtos.FileDescriptorProto +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@DisplayName("`MessageFile` should") +internal class MessageFileSpec { + + @Test + fun `expose the suffix required for the corresponding kind of files`() { + MessageFile.COMMANDS.suffix() shouldBe "commands.proto" + MessageFile.EVENTS.suffix() shouldBe "events.proto" + MessageFile.REJECTIONS.suffix() shouldBe "rejections.proto" + } + + @Nested internal inner class + `test a file descriptor` { + + @Test + fun `accepting the file with matching suffix`() { + val file = FileDescriptorProto.newBuilder() + .setName("given_events.proto") + .build() + MessageFile.EVENTS.test(file) shouldBe true + } + + @Test + fun `rejecting the file with non-matching suffix`() { + val file = Any.getDescriptor().file.toProto() + MessageFile.EVENTS.test(file) shouldBe false + } + } +} diff --git a/base/src/test/kotlin/io/spine/code/proto/CodeGeneratorRequestWriterSpec.kt b/base/src/test/kotlin/io/spine/code/proto/CodeGeneratorRequestWriterSpec.kt deleted file mode 100644 index e85231b8d1..0000000000 --- a/base/src/test/kotlin/io/spine/code/proto/CodeGeneratorRequestWriterSpec.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2024, 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.code.proto - -import io.kotest.matchers.shouldBe -import io.spine.io.replaceExtension -import io.spine.string.toBase64Encoded -import java.io.File -import java.io.InputStream -import java.nio.file.Path -import kotlin.io.path.inputStream -import kotlin.io.path.writeBytes -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.io.TempDir - -@DisplayName("`CodeGeneratorRequestWriter` should") -internal class CodeGeneratorRequestWriterSpec { - - private lateinit var requestFile: File - private lateinit var writer: CodeGeneratorRequestWriter - private lateinit var input: InputStream - - @BeforeEach - fun prepareInput(@TempDir dir: Path) { - val inputFile = dir.resolve("input.stream") - // Request the file in the directory which does not exist. - requestFile = dir.resolve("nested/request.binbp").toFile() - val request = constructRequest(requestFile.absolutePath.toBase64Encoded()) - inputFile.writeBytes(request.toByteArray()) - input = inputFile.inputStream() - writer = CodeGeneratorRequestWriter(input) - } - - @AfterEach - fun closeInput() { - input.close() - } - - @Test - fun `write binary version of the request`() { - writer.writeBinary() - requestFile.exists() shouldBe true - } - - @Test - fun `write JSON version of the request`() { - writer.writeJson() - requestFile.replaceExtension("pb.json").exists() shouldBe true - } -} diff --git a/buildSrc/src/main/kotlin/config-tester.gradle.kts b/buildSrc/src/main/kotlin/config-tester.gradle.kts index 7b64dac7d1..2463f738b2 100644 --- a/buildSrc/src/main/kotlin/config-tester.gradle.kts +++ b/buildSrc/src/main/kotlin/config-tester.gradle.kts @@ -42,10 +42,10 @@ val tempFolder = File("./tmp") ConfigTester(config, tasks, tempFolder) .addRepo(SpineRepos.baseTypes) // Builds `base-types` at `master`. .addRepo(SpineRepos.base) // Builds `base` at `master`. - .addRepo(SpineRepos.coreJava) // Builds `core-java` at `master`. + .addRepo(SpineRepos.coreJvm) // Builds `core-jvm` at `master`. // This is how one builds a specific branch of some repository: - // .addRepo(SpineRepos.coreJava, Branch("grpc-concurrency-fixes")) + // .addRepo(SpineRepos.coreJvm, Branch("grpc-concurrency-fixes")) // Register the produced task under the selected name to invoke manually upon need. .registerUnder("buildDependants") 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 787b8cfb7e..f2f0507d72 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.400" - const val versionForBuildScript = "2.0.0-SNAPSHOT.400" + const val version = "2.0.0-SNAPSHOT.404" + const val versionForBuildScript = "2.0.0-SNAPSHOT.404" 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/ToolBase.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt index c0c4238d92..6073038f35 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.399" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.399" const val lib = "$group:tool-base:$version" const val classicCodegen = "$group:classic-codegen:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt index c3bbfbe115..f95f23ea07 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt @@ -39,8 +39,8 @@ import org.gradle.api.tasks.TaskContainer * A tool to execute the Gradle `build` task in selected Git repositories * with the local version of [config] contents. * - * Checks out the content of selected repositories into the specified [tempFolder]. The folder - * is created if it does not exist. By default, uses `./tmp` as a temp folder. + * Checks out the content of selected repositories into the specified [tempFolder]. + * The folder is created if it does not exist. By default, uses `./tmp` as a temp folder. * * Replaces the `config` and `buildSrc` folders in the checked out repository by the local versions * of code. If the repository-under-test already contains its own `buildSrc` or `config` folders, @@ -356,7 +356,7 @@ object SpineRepos { val base: URI = library("base") val baseTypes: URI = library("base-types") - val coreJava: URI = library("core-java") + val coreJvm: URI = library("core-jvm") val web: URI = library("web") private fun library(repo: String) = URI(libsOrg + repo) 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 1243b04522..195a514631 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt @@ -40,7 +40,23 @@ import org.gradle.api.Project class IncrementGuard : Plugin { companion object { + const val taskName = "checkVersionIncrement" + + /** + * Tells whether the version increment must be verified for the given + * GitHub Actions event and the base branch of the pull request. + * + * The version is guarded only for pull requests targeting a default or + * a release-line branch, i.e. a branch with the name ending with `master` + * or `main`. For example: `master`, `main`, `2.x-jdk8-master`, `2.x-jdk8-main`. + */ + internal fun shouldCheckVersion(event: String?, baseBranch: String?): Boolean { + if (event != "pull_request" || baseBranch == null) { + return false + } + return baseBranch.endsWith("master") || baseBranch.endsWith("main") + } } /** @@ -48,11 +64,15 @@ class IncrementGuard : Plugin { * * The task is created anyway, but it is enabled only if: * 1. The project is built on GitHub CI, and - * 2. The job is a pull request. + * 2. The job is a pull request targeting a default (`master` or `main`) or + * a release-line (e.g. `2.x-jdk8-master`) branch. * - * The task only runs on non-master branches on GitHub Actions. - * This is done to prevent unexpected CI fails when re-building `master` multiple times, - * creating git tags, and in other cases that go outside the "usual" development cycle. + * It is the responsibility of a branch which 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 + * CI fails when re-building `master` multiple times, creating git tags, and in other + * cases that go outside the "usual" development cycle. */ override fun apply(target: Project) { val tasks = target.tasks @@ -64,7 +84,8 @@ class IncrementGuard : Plugin { if (!shouldCheckVersion()) { logger.info( - "The build does not represent a GitHub Actions feature branch job, " + + "The build does not represent a GitHub Actions pull request job " + + "targeting a default or a release-line branch, " + "the `checkVersionIncrement` task is disabled." ) this.enabled = false @@ -73,40 +94,19 @@ class IncrementGuard : Plugin { } /** - * Returns `true` if the current build is a GitHub Actions build which represents a push - * to a feature branch. - * - * Returns `false` if the associated reference is not a branch (e.g., a tag) or if it has - * the name which ends with `master` or `main`. + * Returns `true` if the current build is a GitHub Actions build of a pull request + * targeting a default (`master` or `main`) or a release-line branch, + * such as `2.x-jdk8-master`. * - * For example, on the following branches the method would return `false`: - * - * 1. `master`. - * 2. `main`. - * 3. `2.x-jdk8-master`. - * 4. `2.x-jdk8-main`. + * Returns `false` for all other builds, including direct pushes, tag builds, + * and pull requests targeting auxiliary branches. * * @see * List of default environment variables provided for GitHub Actions builds */ private fun shouldCheckVersion(): Boolean { val event = System.getenv("GITHUB_EVENT_NAME") - val reference = System.getenv("GITHUB_REF") - if (event != "push" || reference == null) { - return false - } - val branch = branchName(reference) - return when { - branch == null -> false - branch.endsWith("master") -> false - branch.endsWith("main") -> false - else -> true - } - } - - private fun branchName(gitHubRef: String): String? { - val matches = Regex("refs/heads/(.+)").matchEntire(gitHubRef) - val branch = matches?.let { it.groupValues[1] } - return branch + val baseBranch = System.getenv("GITHUB_BASE_REF") + return shouldCheckVersion(event, baseBranch) } } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt index ed2bd0290d..e3bd5b98dd 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt @@ -276,7 +276,7 @@ class KoverConfig private constructor( .flatMap { root -> root.walk() .filter { !it.isDirectory } - .flatMap { it.fqnsRelativeTo(root).asSequence() } + .flatMap { it.classNamesIn(root).asSequence() } } .distinct() .toList() @@ -359,7 +359,7 @@ private fun KotlinSourceSet.isMainSourceSet(): Boolean = * * Returns an empty list if this file is not under [root]. */ -private fun File.fqnsRelativeTo(root: File): List { +internal fun File.classNamesIn(root: File): List { if (!startsWith(root)) { return emptyList() } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt new file mode 100644 index 0000000000..1c9d1f54da --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt @@ -0,0 +1,114 @@ +/* + * 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.coverage + +import java.io.File +import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskCollection +import org.gradle.api.tasks.testing.Test + +/** + * Credits the test coverage produced by the [contributor] module for the classes + * of this project to this project's own Kover report. + * + * Some modules' production classes are exercised only by the tests of a sibling + * module — for example, the language-neutral `psi` classes are tested through + * the Java-PSI fixtures that live in `psi-java`. Kover's per-module report sees + * only this module's own test execution data, so that cross-module coverage is + * otherwise missing from the per-module report (which is what Codecov consumes), + * even though the root aggregated report already accounts for it. + * + * This function adds the [contributor]'s JaCoCo execution data to this project's + * `total` report as additional binary reports. Only this project's classes are + * credited from them — coverage of unrelated classes in the same execution data + * is ignored, because a Kover report is scoped to the owning project's classes. + * The report tasks are wired to run after the contributor's JVM test tasks so the + * data is present when a report is generated. + * + * The contributor's JVM test tasks are discovered by type rather than by name, so + * the helper works regardless of the module convention: a `jvm-module` contributes + * through its `test` task, a `kmp-module` through `jvmTest`, and any additional + * JVM test tasks are picked up as well. Non-JVM Kotlin test tasks (`*Native`, + * `*Js`, …) are not of type [Test] and are correctly ignored — Kover instruments + * only JVM test tasks. + * + * Requires the Kover plugin to be applied to this project. + * A cross-project **task** dependency is used, not a project dependency, + * so it does not introduce a dependency cycle even when the [contributor] + * already depends on this project. + */ +fun Project.creditTestCoverageFrom(contributor: Project) { + val contributorTests = contributor.tasks.withType(Test::class.java) + extensions.configure(KoverProjectExtension::class.java) { + reports { + total { + additionalBinaryReports.addAll(contributor.execFilesOf(contributorTests)) + } + } + } + tasks.matching { it.consumesCoverageBinaryReports() }.configureEach { + dependsOn(contributorTests) + } +} + +/** + * Lazy `Provider` of the JaCoCo execution-data files produced by [testTasks] + * of this project. + * + * When the coverage engine is pinned to JaCoCo via `useJacoco(...)`, Kover writes + * one binary report per instrumented JVM test task at `build/`[BIN_REPORTS_DIR] + * `/.exec`, so the file name follows the task name. Resolved at + * task-graph time, after the contributor's test tasks have been registered. + */ +private fun Project.execFilesOf(testTasks: TaskCollection): Provider> { + val binReports = layout.buildDirectory.dir(BIN_REPORTS_DIR) + return provider { + testTasks.map { binReports.get().file("${it.name}.exec").asFile } + } +} + +/** + * The directory under a module's `build/` where Kover writes the per-test-task + * binary execution-data files. + */ +private const val BIN_REPORTS_DIR: String = "kover/bin-reports" + +/** + * Tells whether this is a Kover task that reads the binary reports and therefore + * must run only after the [contributor's][creditTestCoverageFrom] test data exists. + * + * This matches both the report tasks (`koverXmlReport`, `koverHtmlReport`, + * `koverBinaryReport`) and the verification tasks (`koverVerify` and its + * cacheable companion `koverCachedVerify`) — the suffix test covers the + * `Cached*` variants Kover registers, which are the ones that actually consume + * the binary reports. + */ +private fun Task.consumesCoverageBinaryReports(): Boolean = + name.startsWith("kover") && (name.endsWith("Report") || name.endsWith("Verify")) diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt new file mode 100644 index 0000000000..5633c30d4d --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt @@ -0,0 +1,79 @@ +/* + * 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.publish + +import io.kotest.matchers.shouldBe +import io.spine.gradle.publish.IncrementGuard.Companion.shouldCheckVersion +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@DisplayName("`IncrementGuard` should") +class IncrementGuardTest { + + @Nested + inner class `require the version check` { + + @Test + fun `for pull requests targeting default branches`() { + shouldCheckVersion("pull_request", "master") shouldBe true + shouldCheckVersion("pull_request", "main") shouldBe true + } + + @Test + fun `for pull requests targeting release-line branches`() { + shouldCheckVersion("pull_request", "2.x-jdk8-master") shouldBe true + shouldCheckVersion("pull_request", "2.x-jdk8-main") shouldBe true + } + } + + @Nested + inner class `not require the version check` { + + @Test + fun `for pull requests targeting auxiliary branches`() { + shouldCheckVersion("pull_request", "epic-feature") shouldBe false + shouldCheckVersion("pull_request", "master-fixes") shouldBe false + } + + @Test + fun `for push events`() { + shouldCheckVersion("push", "master") shouldBe false + shouldCheckVersion("push", null) shouldBe false + } + + @Test + fun `for pull request events without a base branch`() { + shouldCheckVersion("pull_request", null) shouldBe false + } + + @Test + fun `outside GitHub Actions`() { + shouldCheckVersion(null, null) shouldBe false + } + } +} diff --git a/config b/config index a844dc7b77..234233e0fa 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit a844dc7b77989dd0a227f22e5625444acef5c09a +Subproject commit 234233e0fa407df296ff4742887723653c3dcc95 diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index cb32f01cab..863514c5cb 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.404` +# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0. @@ -760,14 +760,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 08 18:37:28 WEST 2026** using +This report was generated on **Wed Jun 10 21:05:52 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:spine-base:2.0.0-SNAPSHOT.404` +# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1604,14 +1604,14 @@ This report was generated on **Mon Jun 08 18:37:28 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 08 18:37:29 WEST 2026** using +This report was generated on **Wed Jun 10 21:05:52 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:spine-environment:2.0.0-SNAPSHOT.404` +# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2430,14 +2430,14 @@ This report was generated on **Mon Jun 08 18:37:29 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 08 18:37:28 WEST 2026** using +This report was generated on **Wed Jun 10 21:05:52 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:spine-format:2.0.0-SNAPSHOT.404` +# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -3336,6 +3336,6 @@ This report was generated on **Mon Jun 08 18:37:28 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Jun 08 18:37:28 WEST 2026** using +This report was generated on **Wed Jun 10 21:05:52 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 c487c5f049..df9b10ba9b 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 base-libraries -2.0.0-SNAPSHOT.404 +2.0.0-SNAPSHOT.410 2015 diff --git a/gradle.properties b/gradle.properties index 49ad30c59b..3e4fd9de7d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,10 @@ org.gradle.java.installations.auto-download=true # Use parallel builds for better performance. 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. #org.gradle.caching=true # Dokka plugin eats more memory than usual. Therefore, all builds should have enough. diff --git a/version.gradle.kts b/version.gradle.kts index c5226ba68a..36a5dde535 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -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. @@ -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.404") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.410")