From 17c6d9b5534a3d2fb054a0b67dbccda30b0b5ad1 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 26 Dec 2025 16:32:08 +0000 Subject: [PATCH 01/16] Extract `module` script plugin --- build.gradle.kts | 116 -------------------- buildSrc/src/main/kotlin/module.gradle.kts | 120 ++++++++++++++++++++- change/build.gradle.kts | 2 +- 3 files changed, 116 insertions(+), 122 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3649c43..055e8c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,27 +26,7 @@ @file:Suppress("RemoveRedundantQualifierName") -import com.google.protobuf.gradle.id -import io.spine.dependency.build.ErrorProne -import io.spine.dependency.kotlinx.Coroutines -import io.spine.dependency.lib.Grpc -import io.spine.dependency.lib.Jackson -import io.spine.dependency.lib.Kotlin -import io.spine.dependency.lib.KotlinPoet -import io.spine.dependency.lib.Protobuf -import io.spine.dependency.local.Base -import io.spine.dependency.local.CoreJvm -import io.spine.dependency.local.Logging -import io.spine.dependency.local.ToolBase -import io.spine.dependency.local.Validation -import io.spine.gradle.checkstyle.CheckStyleConfig -import io.spine.gradle.github.pages.updateGitHubPages -import io.spine.gradle.javac.configureErrorProne -import io.spine.gradle.javac.configureJavac -import io.spine.gradle.javadoc.JavadocConfig -import io.spine.gradle.kotlin.setFreeCompilerArgs import io.spine.gradle.publish.PublishingRepos -import io.spine.gradle.publish.PublishingRepos.gitHub import io.spine.gradle.publish.spinePublishing import io.spine.gradle.repo.standardToSpineSdk import io.spine.gradle.report.coverage.JacocoConfig @@ -121,104 +101,8 @@ spinePublishing { allprojects { apply(from = "$rootDir/version.gradle.kts") - group = "io.spine" version = extra["versionToPublish"]!! - - configurations { - forceVersions() - all { - exclude("io.spine:spine-validate") - resolutionStrategy { - val cfg = this@all - val rs = this@resolutionStrategy - Kotlin.StdLib.forceArtifacts(project, cfg, rs) - Kotlin.forceArtifacts(project, cfg, rs) - Coroutines.forceArtifacts(project, cfg, rs) - Jackson.forceArtifacts(project, cfg, rs) - Jackson.DataType.forceArtifacts(project, cfg, rs) - Jackson.DataFormat.forceArtifacts(project, cfg, rs) - Grpc.forceArtifacts(project, cfg, rs) - force( - Protobuf.javaLib, - Jackson.annotations, - Jackson.bom, - Grpc.bom, - Kotlin.bom, - KotlinPoet.lib, - ToolBase.lib, - Base.lib, - Base.annotations, - Validation.runtime, - Validation.javaBundle, - Logging.lib, - CoreJvm.server, - Protobuf.compiler - ) - } - } - } -} - -subprojects { - - apply { - plugin("java-library") - plugin("kotlin") - plugin("com.google.protobuf") - plugin("net.ltgt.errorprone") - plugin("pmd") - plugin("checkstyle") - plugin("idea") - plugin("pmd-settings") - plugin("jacoco") - plugin("module-testing") - plugin("dokka-setup") - } - - repositories { - gitHub("change") - standardToSpineSdk() - } - - dependencies { - errorprone(ErrorProne.core) - } - - val javaVersion = JavaVersion.VERSION_17 - - java { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - - tasks { - withType().configureEach { - configureJavac() - configureErrorProne() - } - withType().configureEach { - duplicatesStrategy = DuplicatesStrategy.INCLUDE - } - } - } - - kotlin { - explicitApi() - compilerOptions { - jvmTarget.set(BuildSettings.jvmTarget) - setFreeCompilerArgs() - } - } - - LicenseReporter.generateReportIn(project) - JavadocConfig.applyTo(project) - CheckStyleConfig.applyTo(project) - - updateGitHubPages() { - rootFolder.set(rootDir) - } - - project.configureTaskDependencies() } LicenseReporter.mergeAllReports(project) diff --git a/buildSrc/src/main/kotlin/module.gradle.kts b/buildSrc/src/main/kotlin/module.gradle.kts index b24b5b2..1996260 100644 --- a/buildSrc/src/main/kotlin/module.gradle.kts +++ b/buildSrc/src/main/kotlin/module.gradle.kts @@ -24,8 +24,118 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -// This is a template file for an actual script which should be -// defined by a project to which `config` is applied. -// -// The reason for having this file is that it is referenced as -// a plugin in `uber-jar-module.gradle.kts` +import io.spine.dependency.build.ErrorProne +import io.spine.dependency.kotlinx.Coroutines +import io.spine.dependency.lib.Grpc +import io.spine.dependency.lib.Jackson +import io.spine.dependency.lib.Kotlin +import io.spine.dependency.lib.KotlinPoet +import io.spine.dependency.lib.Protobuf +import io.spine.dependency.local.Base +import io.spine.dependency.local.CoreJvm +import io.spine.dependency.local.ToolBase +import io.spine.dependency.local.Validation +import io.spine.gradle.checkstyle.CheckStyleConfig +import io.spine.gradle.github.pages.updateGitHubPages +import io.spine.gradle.javac.configureErrorProne +import io.spine.gradle.javac.configureJavac +import io.spine.gradle.javadoc.JavadocConfig +import io.spine.gradle.kotlin.setFreeCompilerArgs +import io.spine.gradle.publish.PublishingRepos.gitHub +import io.spine.gradle.repo.standardToSpineSdk +import io.spine.gradle.report.license.LicenseReporter + + +plugins { + `java-library` + kotlin("jvm") + id("com.google.protobuf") + id("net.ltgt.errorprone") + id("pmd") + id("checkstyle") + id("idea") + id("pmd-settings") + id("jacoco") + id("module-testing") + id("dokka-setup") +} +LicenseReporter.generateReportIn(project) +JavadocConfig.applyTo(project) +CheckStyleConfig.applyTo(project) + +repositories { + gitHub("change") + standardToSpineSdk() +} + +dependencies { + errorprone(ErrorProne.core) +} + +val javaVersion = JavaVersion.VERSION_17 + +java { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + + tasks { + withType().configureEach { + configureJavac() + configureErrorProne() + } + withType().configureEach { + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + } +} + +kotlin { + explicitApi() + compilerOptions { + jvmTarget.set(BuildSettings.jvmTarget) + setFreeCompilerArgs() + } +} + +updateGitHubPages() { + rootFolder.set(rootDir) +} + +project.forceDependencies() +project.configureTaskDependencies() + +private fun Project.forceDependencies() { + configurations { + forceVersions() + all { + exclude("io.spine:spine-validate") + resolutionStrategy { + val cfg = this@all + val rs = this@resolutionStrategy + Kotlin.StdLib.forceArtifacts(project, cfg, rs) + Kotlin.forceArtifacts(project, cfg, rs) + Coroutines.forceArtifacts(project, cfg, rs) + Jackson.forceArtifacts(project, cfg, rs) + Jackson.DataType.forceArtifacts(project, cfg, rs) + Jackson.DataFormat.forceArtifacts(project, cfg, rs) + Grpc.forceArtifacts(project, cfg, rs) + force( + Protobuf.javaLib, + Jackson.annotations, + Jackson.bom, + Grpc.bom, + Kotlin.bom, + KotlinPoet.lib, + ToolBase.lib, + Base.lib, + Base.annotations, + Validation.runtime, + Validation.javaBundle, + io.spine.dependency.local.Logging.lib, + CoreJvm.server, + Protobuf.compiler + ) + } + } + } +} diff --git a/change/build.gradle.kts b/change/build.gradle.kts index 952e20d..08991c5 100644 --- a/change/build.gradle.kts +++ b/change/build.gradle.kts @@ -30,7 +30,7 @@ import io.spine.dependency.local.Validation import io.spine.gradle.publish.IncrementGuard plugins { - protobuf + module id(coreJvmCompiler.pluginId) `detekt-code-analysis` } From afffa581c966ce6138bf088025b9df791e859597 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 26 Dec 2025 16:32:19 +0000 Subject: [PATCH 02/16] Update dependency reports --- dependencies.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies.md b/dependencies.md index 7368e39..1ccb216 100644 --- a/dependencies.md +++ b/dependencies.md @@ -989,6 +989,6 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Dec 26 16:19:59 WET 2025** using +This report was generated on **Fri Dec 26 16:31:14 WET 2025** 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/pom.xml b/pom.xml index da6c687..56602c4 100644 --- a/pom.xml +++ b/pom.xml @@ -219,7 +219,7 @@ all modules and does not describe the project structure per-subproject. org.jetbrains.kotlin kotlin-build-tools-impl - 2.2.21 + null org.jetbrains.kotlin From 41c78795691b8d12ad56ba7d1c68d13531825a36 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 26 Dec 2025 16:34:17 +0000 Subject: [PATCH 03/16] Reorder forced dependencies --- buildSrc/src/main/kotlin/module.gradle.kts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/module.gradle.kts b/buildSrc/src/main/kotlin/module.gradle.kts index 1996260..cb417a7 100644 --- a/buildSrc/src/main/kotlin/module.gradle.kts +++ b/buildSrc/src/main/kotlin/module.gradle.kts @@ -33,6 +33,7 @@ import io.spine.dependency.lib.KotlinPoet import io.spine.dependency.lib.Protobuf import io.spine.dependency.local.Base import io.spine.dependency.local.CoreJvm +import io.spine.dependency.local.Logging import io.spine.dependency.local.ToolBase import io.spine.dependency.local.Validation import io.spine.gradle.checkstyle.CheckStyleConfig @@ -120,18 +121,18 @@ private fun Project.forceDependencies() { Jackson.DataFormat.forceArtifacts(project, cfg, rs) Grpc.forceArtifacts(project, cfg, rs) force( - Protobuf.javaLib, Jackson.annotations, Jackson.bom, Grpc.bom, Kotlin.bom, KotlinPoet.lib, + Protobuf.javaLib, ToolBase.lib, Base.lib, Base.annotations, Validation.runtime, Validation.javaBundle, - io.spine.dependency.local.Logging.lib, + Logging.lib, CoreJvm.server, Protobuf.compiler ) From c1102d694c32c0db7bdd60809a8a0d0113a6865b Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 26 Dec 2025 16:34:33 +0000 Subject: [PATCH 04/16] Remove extra empty line --- buildSrc/src/main/kotlin/module.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/module.gradle.kts b/buildSrc/src/main/kotlin/module.gradle.kts index cb417a7..bc83ba5 100644 --- a/buildSrc/src/main/kotlin/module.gradle.kts +++ b/buildSrc/src/main/kotlin/module.gradle.kts @@ -46,7 +46,6 @@ import io.spine.gradle.publish.PublishingRepos.gitHub import io.spine.gradle.repo.standardToSpineSdk import io.spine.gradle.report.license.LicenseReporter - plugins { `java-library` kotlin("jvm") From 97582c62321c31ebf746999ae9d2bc48c021eb3e Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 26 Dec 2025 16:35:31 +0000 Subject: [PATCH 05/16] Use import for the `Jar` task type --- buildSrc/src/main/kotlin/module.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/module.gradle.kts b/buildSrc/src/main/kotlin/module.gradle.kts index bc83ba5..4f88da5 100644 --- a/buildSrc/src/main/kotlin/module.gradle.kts +++ b/buildSrc/src/main/kotlin/module.gradle.kts @@ -45,6 +45,7 @@ import io.spine.gradle.kotlin.setFreeCompilerArgs import io.spine.gradle.publish.PublishingRepos.gitHub import io.spine.gradle.repo.standardToSpineSdk import io.spine.gradle.report.license.LicenseReporter +import org.gradle.jvm.tasks.Jar plugins { `java-library` @@ -83,7 +84,7 @@ java { configureJavac() configureErrorProne() } - withType().configureEach { + withType().configureEach { duplicatesStrategy = DuplicatesStrategy.INCLUDE } } From b2a27a46b3da3af7409056e1d249439135b3aa93 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 26 Dec 2025 16:39:27 +0000 Subject: [PATCH 06/16] Update build time --- dependencies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.md b/dependencies.md index 1ccb216..7376a19 100644 --- a/dependencies.md +++ b/dependencies.md @@ -989,6 +989,6 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Dec 26 16:31:14 WET 2025** using +This report was generated on **Fri Dec 26 16:36:02 WET 2025** 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 From eb6733dda33e7e0bd07115b26b4f098f5d2acbd9 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Thu, 23 Jul 2026 16:44:34 +0100 Subject: [PATCH 07/16] Update `config` and shared agent tooling Sync the config-distributed files with the latest `config` submodule and register the `.agents/shared` submodule: agent guidelines and skills (`.agents`, `.claude`, `.junie`, `AGENTS.md`, `CLAUDE.md`), CI workflows, IDE settings, the Gradle wrapper, and the `buildSrc` build infrastructure. Co-Authored-By: Claude Opus 4.8 --- .agents/_TOC.md | 16 - .agents/advanced-safety-rules.md | 6 - .agents/coding-guidelines.md | 39 - .agents/common-tasks.md | 6 - .agents/documentation-guidelines.md | 14 - .agents/documentation-tasks.md | 20 - .agents/guidelines | 1 + .agents/project-structure-expectations.md | 21 - .agents/project.md | 1 + .agents/quick-reference-card.md | 10 - .agents/refactoring-guidelines.md | 3 - .agents/running-builds.md | 18 - .agents/safety-rules.md | 7 - .agents/scripts | 1 + .agents/shared | 1 + .agents/skills | 1 + .agents/testing.md | 8 - .agents/version-policy.md | 30 - .claude/agents | 1 + .claude/commands | 1 + .claude/settings.json | 106 ++ .claude/skills | 1 + .codecov.yml | 6 +- .github/copilot-instructions.md | 45 + .github/workflows/build-on-ubuntu.yml | 74 +- .github/workflows/build-on-windows.yml | 19 +- .github/workflows/check-links.yml | 216 ++++ .github/workflows/ensure-reports-updated.yml | 26 +- .../workflows/gradle-wrapper-validation.yml | 6 +- .github/workflows/increment-guard.yml | 93 +- .github/workflows/publish.yml | 24 +- ...move-obsolete-artifacts-from-packages.yaml | 6 +- .github/workflows/revalidate-versions.yml | 57 ++ .github/workflows/secret-scan.yml | 70 ++ .gitignore | 107 +- .gitmodules | 6 + .idea/inspectionProfiles/Project_Default.xml | 12 + .idea/kotlinc.xml | 9 +- .idea/live-templates/README.md | 8 +- .idea/live-templates/User.xml | 2 +- .junie/guidelines.md | 4 +- .junie/skills | 1 + AGENTS.md | 134 ++- CLAUDE.md | 29 +- buildSrc/build.gradle.kts | 55 +- buildSrc/quality/checkstyle-suppressions.xml | 2 +- buildSrc/quality/checkstyle.xml | 2 +- buildSrc/quality/pmd.xml | 4 +- buildSrc/settings.gradle.kts | 2 +- buildSrc/src/main/kotlin/BuildExtensions.kt | 37 +- buildSrc/src/main/kotlin/BuildSettings.kt | 2 +- .../src/main/kotlin/DependencyResolution.kt | 8 +- .../src/main/kotlin/DocumentationSettings.kt | 2 +- buildSrc/src/main/kotlin/DokkaExts.kt | 13 +- buildSrc/src/main/kotlin/LicenseSettings.kt | 50 +- buildSrc/src/main/kotlin/Strings.kt | 6 +- .../src/main/kotlin/config-tester.gradle.kts | 11 +- .../kotlin/detekt-code-analysis.gradle.kts | 8 +- .../src/main/kotlin/dokka-setup.gradle.kts | 11 +- .../kotlin/io/spine/docs/MarkdownDocument.kt | 2 +- .../src/main/kotlin/io/spine/gradle/Build.kt | 2 +- .../src/main/kotlin/io/spine/gradle/Clean.kt | 2 +- .../src/main/kotlin/io/spine/gradle/Cli.kt | 25 +- .../kotlin/io/spine/gradle/ConfigTester.kt | 24 +- .../io/spine/gradle/ProjectExtensions.kt | 15 +- .../main/kotlin/io/spine/gradle/RunBuild.kt | 2 +- .../main/kotlin/io/spine/gradle/RunGradle.kt | 8 +- .../spine/gradle/SpineTaskGroup.kt} | 52 +- .../io/spine/gradle/StringExtensions.kt | 2 +- .../main/kotlin/io/spine/gradle/TaskName.kt | 2 +- .../io/spine/gradle/VersionComparator.kt | 115 +++ .../io/spine/gradle/VersionGradleFile.kt | 172 ++++ .../main/kotlin/io/spine/gradle/base/Tasks.kt | 2 +- .../gradle/checkstyle/CheckStyleConfig.kt | 2 +- .../io/spine/gradle/dart/DartContext.kt | 6 +- .../io/spine/gradle/dart/DartEnvironment.kt | 12 +- .../io/spine/gradle/dart/DartExtension.kt | 12 +- .../spine/gradle/dart/plugin/DartPlugins.kt | 6 +- .../io/spine/gradle/dart/plugin/Protobuf.kt | 4 +- .../kotlin/io/spine/gradle/dart/task/Build.kt | 21 +- .../io/spine/gradle/dart/task/DartTasks.kt | 21 +- .../spine/gradle/dart/task/IntegrationTest.kt | 14 +- .../io/spine/gradle/dart/task/Publish.kt | 17 +- .../spine/gradle/docs/UpdatePluginVersion.kt | 126 +++ .../kotlin/io/spine/gradle/fs/LazyTempPath.kt | 9 +- .../kotlin/io/spine/gradle/fs/SpineTempDir.kt | 92 ++ .../main/kotlin/io/spine/gradle/git/Branch.kt | 2 +- .../kotlin/io/spine/gradle/git/Repository.kt | 130 ++- .../kotlin/io/spine/gradle/git/UserInfo.kt | 2 +- .../spine/gradle/github/pages/AuthorEmail.kt | 2 +- .../github/pages/RepositoryExtensions.kt | 15 +- .../io/spine/gradle/github/pages/SshKey.kt | 4 +- .../io/spine/gradle/github/pages/TaskName.kt | 4 +- .../io/spine/gradle/github/pages/Update.kt | 2 +- .../gradle/github/pages/UpdateGitHubPages.kt | 27 +- .../pages/UpdateGitHubPagesExtension.kt | 2 +- .../kotlin/io/spine/gradle/java/Linters.kt | 4 +- .../main/kotlin/io/spine/gradle/java/Tasks.kt | 6 +- .../io/spine/gradle/javac/ErrorProne.kt | 4 +- .../kotlin/io/spine/gradle/javac/Javac.kt | 2 +- .../io/spine/gradle/javadoc/Encoding.kt | 2 +- .../gradle/javadoc/ExcludeInternalDoclet.kt | 14 +- .../io/spine/gradle/javadoc/JavadocConfig.kt | 12 +- .../io/spine/gradle/javadoc/JavadocTag.kt | 2 +- .../io/spine/gradle/javascript/JsContext.kt | 8 +- .../spine/gradle/javascript/JsEnvironment.kt | 14 +- .../io/spine/gradle/javascript/JsExtension.kt | 14 +- .../io/spine/gradle/javascript/plugin/Idea.kt | 4 +- .../gradle/javascript/plugin/JsPlugins.kt | 6 +- .../io/spine/gradle/javascript/plugin/McJs.kt | 4 +- .../gradle/javascript/plugin/Protobuf.kt | 6 +- .../spine/gradle/javascript/task/Assemble.kt | 13 +- .../io/spine/gradle/javascript/task/Check.kt | 11 +- .../io/spine/gradle/javascript/task/Clean.kt | 7 +- .../gradle/javascript/task/IntegrationTest.kt | 9 +- .../spine/gradle/javascript/task/JsTasks.kt | 22 +- .../gradle/javascript/task/LicenseReport.kt | 5 +- .../spine/gradle/javascript/task/Publish.kt | 13 +- .../spine/gradle/javascript/task/Webpack.kt | 5 +- .../io/spine/gradle/kotlin/KotlinConfig.kt | 27 +- .../gradle/publish/CheckVersionIncrement.kt | 163 ++- .../gradle/publish/CloudArtifactRegistry.kt | 6 +- .../io/spine/gradle/publish/CloudRepo.kt | 2 +- .../publish/CustomPublicationHandler.kt | 4 +- .../io/spine/gradle/publish/GitHubPackages.kt | 8 +- .../io/spine/gradle/publish/IncrementGuard.kt | 195 +++- .../kotlin/io/spine/gradle/publish/JarDsl.kt | 4 +- .../io/spine/gradle/publish/MavenMetadata.kt | 84 ++ .../io/spine/gradle/publish/ProtoExts.kt | 2 +- .../gradle/publish/PublicationHandler.kt | 52 +- .../io/spine/gradle/publish/PublishingExts.kt | 45 +- .../spine/gradle/publish/PublishingRepos.kt | 2 +- .../io/spine/gradle/publish/ShadowJarExts.kt | 87 +- .../spine/gradle/publish/SpinePublishing.kt | 77 +- .../publish/StandardJavaPublicationHandler.kt | 4 +- .../io/spine/gradle/repo/Credentials.kt | 2 +- .../kotlin/io/spine/gradle/repo/RepoSlug.kt | 2 +- .../io/spine/gradle/repo/Repositories.kt | 71 +- .../kotlin/io/spine/gradle/repo/Repository.kt | 8 +- .../gradle/report/coverage/KoverConfig.kt | 444 ++++++++ .../gradle/report/coverage/SiblingCoverage.kt | 116 +++ .../gradle/report/license/Configuration.kt | 2 +- .../gradle/report/license/LicenseReporter.kt | 36 +- .../report/license/MarkdownReportRenderer.kt | 2 +- .../report/license/ModuleDataExtensions.kt | 4 +- .../io/spine/gradle/report/license/Paths.kt | 22 +- .../report/license/ProjectDependencies.kt | 2 +- .../io/spine/gradle/report/license/Tasks.kt | 4 +- .../spine/gradle/report/license/Template.kt | 2 +- .../gradle/report/pom/DependencyScope.kt | 6 +- .../gradle/report/pom/DependencyWriter.kt | 164 ++- .../spine/gradle/report/pom/InceptionYear.kt | 2 +- .../gradle/report/pom/MarkupExtensions.kt | 2 +- .../gradle/report/pom/ModuleDependency.kt | 2 +- .../spine/gradle/report/pom/PomFormatting.kt | 2 +- .../spine/gradle/report/pom/PomGenerator.kt | 14 +- .../spine/gradle/report/pom/PomXmlWriter.kt | 4 +- .../gradle/report/pom/ProjectMetadata.kt | 38 +- .../gradle/report/pom/ScopedDependency.kt | 36 +- .../spine/gradle/report/pom/SpineLicense.kt | 2 +- .../kotlin/io/spine/gradle/testing/Logging.kt | 19 +- .../io/spine/gradle/testing/Multiproject.kt | 4 +- .../gradle/testing/SpineCompilerCoverage.kt | 175 ++++ .../kotlin/io/spine/gradle/testing/Tasks.kt | 7 +- .../spine/gradle/testing/TestKitCoverage.kt | 158 +++ .../src/main/kotlin/jacoco-kmm-jvm.gradle.kts | 72 -- .../src/main/kotlin/jvm-module.gradle.kts | 19 +- .../src/main/kotlin/kmp-module.gradle.kts | 36 +- .../src/main/kotlin/kmp-publish.gradle.kts | 2 +- .../src/main/kotlin/module-testing.gradle.kts | 4 +- .../src/main/kotlin/pmd-settings.gradle.kts | 2 +- .../src/main/kotlin/test-module.gradle.kts | 2 +- .../main/kotlin/uber-jar-module.gradle.kts | 14 +- .../src/main/kotlin/write-manifest.gradle.kts | 12 +- .../resources/dokka/styles/custom-styles.css | 2 +- .../io/spine/gradle/VersionComparatorSpec.kt | 87 ++ .../io/spine/gradle/VersionGradleFileSpec.kt | 108 ++ .../gradle/docs/UpdatePluginVersionTest.kt | 119 +++ .../io/spine/gradle/fs/LazyTempPathSpec.kt | 69 ++ .../io/spine/gradle/fs/SpineTempDirSpec.kt | 54 + .../gradle/publish/IncrementGuardTest.kt | 222 ++++ .../spine/gradle/publish/MavenMetadataSpec.kt | 53 + .../gradle/publish/SpinePublishingTest.kt | 260 +++++ .../report/coverage/FileExtensionsTest.kt | 128 +++ .../license/DependencyReportOutputTest.kt | 104 ++ .../gradle/report/pom/DependencyWriterSpec.kt | 425 ++++++++ config | 2 +- docs/dependencies/dependencies.md | 948 ++++++++++++++++++ docs/dependencies/pom.xml | 244 +++++ docs/project.md | 18 + gradle.properties | 26 +- gradle/wrapper/gradle-wrapper.jar | Bin 45633 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 9 +- gradlew.bat | 38 +- init-submodules | 99 ++ 196 files changed, 6822 insertions(+), 1097 deletions(-) delete mode 100644 .agents/_TOC.md delete mode 100644 .agents/advanced-safety-rules.md delete mode 100644 .agents/coding-guidelines.md delete mode 100644 .agents/common-tasks.md delete mode 100644 .agents/documentation-guidelines.md delete mode 100644 .agents/documentation-tasks.md create mode 120000 .agents/guidelines delete mode 100644 .agents/project-structure-expectations.md create mode 120000 .agents/project.md delete mode 100644 .agents/quick-reference-card.md delete mode 100644 .agents/refactoring-guidelines.md delete mode 100644 .agents/running-builds.md delete mode 100644 .agents/safety-rules.md create mode 120000 .agents/scripts create mode 160000 .agents/shared create mode 120000 .agents/skills delete mode 100644 .agents/testing.md delete mode 100644 .agents/version-policy.md create mode 120000 .claude/agents create mode 120000 .claude/commands create mode 100644 .claude/settings.json create mode 120000 .claude/skills create mode 100644 .github/copilot-instructions.md create mode 100644 .github/workflows/check-links.yml create mode 100644 .github/workflows/revalidate-versions.yml create mode 100644 .github/workflows/secret-scan.yml create mode 120000 .junie/skills rename buildSrc/src/main/kotlin/{jacoco-kotlin-jvm.gradle.kts => io/spine/gradle/SpineTaskGroup.kt} (51%) create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/VersionComparator.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/VersionGradleFile.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/docs/UpdatePluginVersion.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/fs/SpineTempDir.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/publish/MavenMetadata.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/testing/SpineCompilerCoverage.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt delete mode 100644 buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/VersionComparatorSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/VersionGradleFileSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/docs/UpdatePluginVersionTest.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/fs/LazyTempPathSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/fs/SpineTempDirSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/publish/MavenMetadataSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/publish/SpinePublishingTest.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/report/coverage/FileExtensionsTest.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/report/license/DependencyReportOutputTest.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt create mode 100644 docs/dependencies/dependencies.md create mode 100644 docs/dependencies/pom.xml create mode 100644 docs/project.md create mode 100755 init-submodules diff --git a/.agents/_TOC.md b/.agents/_TOC.md deleted file mode 100644 index 065df13..0000000 --- a/.agents/_TOC.md +++ /dev/null @@ -1,16 +0,0 @@ -# Table of Contents - -1. [Quick Reference Card](quick-reference-card.md) -2. [Project overview](project-overview.md) -3. [Coding guidelines](coding-guidelines.md) -4. [Documentation & comments](documentation-guidelines.md) -5. [Documentation tasks](documentation-tasks.md) -6. [Running builds](running-builds.md) -7. [Version policy](version-policy.md) -8. [Project structure expectations](project-structure-expectations.md) -9. [Testing](testing.md) -10. [Safety rules](safety-rules.md) -11. [Advanced safety rules](advanced-safety-rules.md) -12. [Refactoring guidelines](refactoring-guidelines.md) -13. [Common tasks](common-tasks.md) -14. [Java to Kotlin conversion](java-kotlin-conversion.md) diff --git a/.agents/advanced-safety-rules.md b/.agents/advanced-safety-rules.md deleted file mode 100644 index e410581..0000000 --- a/.agents/advanced-safety-rules.md +++ /dev/null @@ -1,6 +0,0 @@ -# ๐Ÿšจ Advanced safety rules - -- Do **not** auto-update external dependencies without explicit request. -- Do **not** inject analytics or telemetry code. -- Flag any usage of unsafe constructs (e.g., reflection, I/O on the main thread). -- Avoid generating blocking calls inside coroutines. diff --git a/.agents/coding-guidelines.md b/.agents/coding-guidelines.md deleted file mode 100644 index 3297d8a..0000000 --- a/.agents/coding-guidelines.md +++ /dev/null @@ -1,39 +0,0 @@ -# ๐Ÿงพ Coding guidelines - -## Core principles - -- Adhere to [Spine Event Engine Documentation][spine-docs] for coding style. -- Generate code that compiles cleanly and passes static analysis. -- Respect existing architecture, naming conventions, and project structure. -- Write clear, incremental commits with descriptive messages. -- Include automated tests for any code change that alters functionality. - -## Kotlin best practices - -### โœ… Prefer -- **Kotlin idioms** over Java-style approaches: - - Extension functions - - `when` expressions - - Smart casts - - Data classes and sealed classes - - Immutable data structures -- **Simple nouns** over composite nouns (`user` > `userAccount`) -- **Generic parameters** over explicit variable types (`val list = mutableList()`) -- **Java interop annotations** only when needed (`@file:JvmName`, `@JvmStatic`) -- **Kotlin DSL** for Gradle files - -### โŒ Avoid -- Mutable data structures -- Java-style verbosity (builders with setters) -- Redundant null checks (`?.let` misuse) -- Using `!!` unless clearly justified -- Type names in variable names (`userObject`, `itemList`) -- String duplication (use constants in companion objects) -- Mixing Groovy and Kotlin DSLs in build logic -- Reflection unless specifically requested - -## Text formatting - - โœ… Remove double empty lines in the code. - - โœ… Remove trailing space characters in the code. - -[spine-docs]: https://github.com/SpineEventEngine/documentation/wiki diff --git a/.agents/common-tasks.md b/.agents/common-tasks.md deleted file mode 100644 index 5ee954d..0000000 --- a/.agents/common-tasks.md +++ /dev/null @@ -1,6 +0,0 @@ -# ๐Ÿ“‹ Common tasks - -- **Adding a new dependency**: Update relevant files in `buildSrc` directory. -- **Creating a new module**: Follow existing module structure patterns. -- **Documentation**: Use KDoc style for public and internal APIs. -- **Testing**: Create comprehensive tests using Kotest assertions. diff --git a/.agents/documentation-guidelines.md b/.agents/documentation-guidelines.md deleted file mode 100644 index 914dcc8..0000000 --- a/.agents/documentation-guidelines.md +++ /dev/null @@ -1,14 +0,0 @@ -# Documentation & comments - -## Commenting guidelines -- Avoid inline comments in production code unless necessary. -- Inline comments are helpful in tests. -- When using TODO comments, follow the format on the [dedicated page][todo-comments]. -- File and directory names should be formatted as code. - -## Avoid widows, runts, orphans, or rivers - -Agents should **AVOID** text flow patters illustrated -on [this diagram](widow-runt-orphan-river.jpg). - -[todo-comments]: https://github.com/SpineEventEngine/documentation/wiki/TODO-comments diff --git a/.agents/documentation-tasks.md b/.agents/documentation-tasks.md deleted file mode 100644 index 8ac4660..0000000 --- a/.agents/documentation-tasks.md +++ /dev/null @@ -1,20 +0,0 @@ -# ๐Ÿ“„ Documentation tasks - -1. Ensure all public and internal APIs have KDoc examples. -2. Add in-line code blocks for clarity in tests. -3. Convert inline API comments in Java to KDoc in Kotlin: - ```java - // Literal string to be inlined whenever a placeholder references a non-existent argument. - private final String missingArgumentMessage = "[MISSING ARGUMENT]"; - ``` - transforms to: - ```kotlin - /** - * Literal string to be inlined whenever a placeholder references a non-existent argument. - */ - private val missingArgumentMessage = "[MISSING ARGUMENT]" - ``` - -4. Javadoc -> KDoc conversion tasks: - - Remove `

` tags in the line with text: `"

This"` -> `"This"`. - - Replace `

` with empty line if the tag is the only text in the line. diff --git a/.agents/guidelines b/.agents/guidelines new file mode 120000 index 0000000..6f9d966 --- /dev/null +++ b/.agents/guidelines @@ -0,0 +1 @@ +shared/guidelines \ No newline at end of file diff --git a/.agents/project-structure-expectations.md b/.agents/project-structure-expectations.md deleted file mode 100644 index 81b8e1a..0000000 --- a/.agents/project-structure-expectations.md +++ /dev/null @@ -1,21 +0,0 @@ -# ๐Ÿ“ Project structure expectations - -```yaml -.github -buildSrc/ - - src/ - โ”œโ”€โ”€ main/ - โ”‚ โ”œโ”€โ”€ kotlin/ # Kotlin source files - โ”‚ โ””โ”€โ”€ java/ # Legacy Java code - โ”œโ”€โ”€ test/ - โ”‚ โ””โ”€โ”€ kotlin/ # Unit and integration tests - build.gradle.kts # Kotlin-based build configuration - - -build.gradle.kts # Kotlin-based build configuration -settings.gradle.kts # Project structure and settings -README.md # Project overview -AGENTS.md # Entry point for LLM agent instructions -version.gradle.kts # Declares the project version. -``` diff --git a/.agents/project.md b/.agents/project.md new file mode 120000 index 0000000..7e0bf9b --- /dev/null +++ b/.agents/project.md @@ -0,0 +1 @@ +../docs/project.md \ No newline at end of file diff --git a/.agents/quick-reference-card.md b/.agents/quick-reference-card.md deleted file mode 100644 index 6c25b9a..0000000 --- a/.agents/quick-reference-card.md +++ /dev/null @@ -1,10 +0,0 @@ -# ๐Ÿ“ Quick Reference Card - -``` -๐Ÿ”‘ Key Information: -- Kotlin/Java project with CQRS architecture -- Use ChatGPT for documentation, Codex for code generation, GPT-4o for complex analysis -- Follow coding guidelines in Spine Event Engine docs -- Always include tests with code changes -- Version bump required for all PRs -``` diff --git a/.agents/refactoring-guidelines.md b/.agents/refactoring-guidelines.md deleted file mode 100644 index 191db49..0000000 --- a/.agents/refactoring-guidelines.md +++ /dev/null @@ -1,3 +0,0 @@ -# โš™๏ธ Refactoring guidelines - -- Do NOT replace Kotest assertions with standard Kotlin's built-in test assertions. diff --git a/.agents/running-builds.md b/.agents/running-builds.md deleted file mode 100644 index db0338d..0000000 --- a/.agents/running-builds.md +++ /dev/null @@ -1,18 +0,0 @@ -# Running builds - -1. When modifying code, run: - ```bash - ./gradlew build - ``` - -2. If Protobuf (`.proto`) files are modified run: - ```bash - ./gradlew clean build - ``` - -3. Documentation-only changes in Kotlin or Java sources run: - ```bash - ./gradlew dokka - ``` - -4. Documentation-only changes do not require running tests! diff --git a/.agents/safety-rules.md b/.agents/safety-rules.md deleted file mode 100644 index 08e9b33..0000000 --- a/.agents/safety-rules.md +++ /dev/null @@ -1,7 +0,0 @@ -# Safety rules - -- โœ… All code must compile and pass static analysis. -- โœ… Do not auto-update external dependencies. -- โŒ Never use reflection or unsafe code without an explicit approval. -- โŒ No analytics or telemetry code. -- โŒ No blocking calls inside coroutines. diff --git a/.agents/scripts b/.agents/scripts new file mode 120000 index 0000000..96bf06e --- /dev/null +++ b/.agents/scripts @@ -0,0 +1 @@ +shared/scripts \ No newline at end of file diff --git a/.agents/shared b/.agents/shared new file mode 160000 index 0000000..2369ab8 --- /dev/null +++ b/.agents/shared @@ -0,0 +1 @@ +Subproject commit 2369ab816bb5d0feee562bfd6395b0b2852a3a9d diff --git a/.agents/skills b/.agents/skills new file mode 120000 index 0000000..f14734d --- /dev/null +++ b/.agents/skills @@ -0,0 +1 @@ +shared/skills \ No newline at end of file diff --git a/.agents/testing.md b/.agents/testing.md deleted file mode 100644 index f81bdbf..0000000 --- a/.agents/testing.md +++ /dev/null @@ -1,8 +0,0 @@ -# ๐Ÿงช Testing - -- Do not use mocks, use stubs. -- Prefer [Kotest assertions][kotest-assertions] over assertions from JUnit or Google Truth. -- Generate unit tests for APIs (handles edge cases/scenarios). -- Supply scaffolds for typical Kotlin patterns (`when`, sealed classes). - -[kotest-assertions]: https://kotest.io/docs/assertions/assertions.html diff --git a/.agents/version-policy.md b/.agents/version-policy.md deleted file mode 100644 index 65dc457..0000000 --- a/.agents/version-policy.md +++ /dev/null @@ -1,30 +0,0 @@ -# Version policy - -## We use semver -The version of the project is kept in the `version.gradle.kts` file in the root of the project. - -The version numbers in these files follow the conventions of -[Semantic Versioning 2.0.0](https://semver.org/). - -## Quick checklist for versioning -1. Increment the patch version in `version.gradle.kts`. - Retain zero-padding if applicable: - - Example: `"2.0.0-SNAPSHOT.009"` โ†’ `"2.0.0-SNAPSHOT.010"` -2. Commit the version bump separately with this comment: - ```text - Bump version โ†’ `$newVersion` - ``` -3. Rebuild using `./gradlew clean build`. -4. Update `pom.xml`, `dependencies.md` and commit changes with: `Update dependency reports` - -Remember: PRs without version bumps will fail CI (conflict resolution detailed above). - -## Resolving conflicts in `version.gradle.kts` -A branch conflict over the version number should be resolved as described below. - * If a merged branch has a number which is less than that of the current branch, the version of - the current branch stays. - * If the merged branch has the number which is greater or equal to that of the current branch, - the number should be increased by one. - -## When to bump the version? - - When a new branch is created. diff --git a/.claude/agents b/.claude/agents new file mode 120000 index 0000000..18e96c9 --- /dev/null +++ b/.claude/agents @@ -0,0 +1 @@ +../.agents/shared/claude/agents \ No newline at end of file diff --git a/.claude/commands b/.claude/commands new file mode 120000 index 0000000..ad85cd8 --- /dev/null +++ b/.claude/commands @@ -0,0 +1 @@ +../.agents/shared/claude/commands \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..3c7da68 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,106 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "plansDirectory": ".claude/plans", + "permissions": { + "allow": [ + "Edit(version.gradle.kts)", + "Bash(./gradlew:*)", + "Bash(./config/gradlew:*)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git branch:*)", + "Bash(git switch:*)", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(git restore:*)", + "Bash(git stash:*)", + "Bash(git fetch:*)", + "Bash(git push:*)", + "Bash(git rev-parse:*)", + "Bash(git ls-files:*)", + "Bash(git mv:*)", + "Bash(git submodule status:*)", + "Bash(ls:*)", + "Bash(cat:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(wc:*)", + "Bash(find:*)", + "Bash(rg:*)", + "Bash(grep:*)", + "Bash(mkdir:*)", + "Bash(touch:*)", + "Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*)", + "Bash(.agents/skills/version-bumped/scripts/version-bumped.sh)", + "Bash(./config/pull)", + "Bash(./config/migrate)", + "Skill(pre-pr)", + "Skill(pre-pr:*)" + ], + "deny": [ + "Bash(git reset --hard:*)", + "Bash(git clean -fdx:*)", + "Bash(rm -rf /:*)", + "Bash(rm -rf ~:*)", + "Bash(gh pr merge:*)", + "Bash(gh release create:*)" + ], + "ask": [ + "Bash(git commit:*)", + "Bash(git rebase:*)", + "Bash(git merge:*)", + "Bash(git cherry-pick:*)", + "Bash(./gradlew publish:*)", + "Bash(./gradlew uploadArtifacts:*)", + "Bash(./gradlew clean:*)" + ] + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/init-submodules" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/secret-scan-gate.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/pre-pr-gate.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/publish-version-gate.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/sanitize-source-code.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/update-copyright.sh" + } + ] + } + ] + } +} diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.codecov.yml b/.codecov.yml index b5739f8..e05fc77 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -12,15 +12,15 @@ coverage: - "**/test/**/*" status: # https://docs.codecov.com/docs/github-checks#yaml-configuration-for-github-checks-and-codecov - patch: false + patch: off # https://docs.codecov.com/docs/commit-status project: default: target: auto threshold: 0.05% - base: auto paths: - - "src" + - "src/main/**/*" + - "**/src/main/**" if_ci_failed: error informational: false only_pulls: true diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..039657b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,45 @@ +# GitHub Copilot Instructions + +## Repository context + +This repository is part of the Spine SDK organisation (~40 repos). + +Universal agent instructions are in [`AGENTS.md`](../AGENTS.md) at the +repository root โ€” read it first. + +If `.agents/project.md` exists, read it before reviewing. It provides the +language, architecture, role, and code review checklist for this specific repo. + +Additional guidelines are in `.agents/guidelines/` โ€” see +`.agents/guidelines/_TOC.md` for the index. + +## Do not review + +Never review `gradlew` or `gradlew.bat` in any repository, including `config`. +These files are provided by Gradle and are not edited manually. + +If the current repository is `config`, review its files normally unless noted +above: they are authoritative there. In other repositories, the following files are managed by +the `config` submodule and must be reviewed in the `config` repository, not +here. In those consumer repositories, skip them without comment: + +- `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` +- `.agents/**` (except `.agents/project.md`) +- `.claude/**`, `.idea/**`, `.junie/**` +- `.github/copilot-instructions.md` +- `buildSrc/**` (except `buildSrc/src/main/kotlin/module.gradle.kts`) +- `gradle/`, `gradlew`, `gradlew.bat` +- `.codecov.yml`, `.gitignore`, `gradle.properties`, `lychee.toml` +- `.github/workflows/` โ€” unless the workflow was introduced by this repo + +## Universal rules + +**Do not suggest:** +- Any git history operation โ€” `git commit`, `git push`, `git tag`, + `git rebase`, `git merge`, `git cherry-pick`, `gh pr merge`, or any other + command that writes to history โ€” leave these to the developer. +- Auto-updating dependency versions outside a dedicated update task. +- Feature flags, backwards-compatibility shims, or fallbacks for scenarios + that cannot occur in the current codebase. +- Analytics, telemetry, or tracking code. +- Reflection or unsafe code without explicit approval. diff --git a/.github/workflows/build-on-ubuntu.yml b/.github/workflows/build-on-ubuntu.yml index f8c2493..b07bf0f 100644 --- a/.github/workflows/build-on-ubuntu.yml +++ b/.github/workflows/build-on-ubuntu.yml @@ -1,27 +1,59 @@ -name: Build under Ubuntu +name: Ubuntu CI -on: push +# Triggers: +# * push to a default or release-line branch โ€” those ending in `master` or +# `main`, the same set `increment-guard.yml` guards as PR bases (e.g. +# `master`, `2.x-jdk8-master`). These post-merge runs are the only source +# of base-branch coverage, since `Publish` runs `publish -x test` and +# uploads none; `target: auto` in `.codecov.yml` compares each pull request +# against its base baseline. +# * pull_request โ€” gates a change on its merge result, not the branch tip. +on: + push: + branches: + - '**master' + - '**main' + pull_request: jobs: build: - name: Build under Ubuntu + name: Build on Ubuntu runs-on: ubuntu-latest + concurrency: # Avoid canceling in-progress runs for the same branch. + group: ubuntu-ci-${{ github.ref }} + cancel-in-progress: false 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 - - name: Build project and run tests + - uses: gradle/actions/setup-gradle@v6 + + # Mirrors the pagefile step in build-on-windows.yml. The Linux runner + # ships with effectively no swap, so a memory peak becomes an instant + # OOM kill; this gives the kernel somewhere to fall back to. + - name: Add swap space + uses: pierotofy/set-swap-space@v1.0 + with: + swap-size-gb: 8 + + - name: Build project, run tests shell: bash run: ./gradlew build --stacktrace + # `build` does not run Dokka โ€” its tasks are gated to the publishing + # graph โ€” so `dokkaGenerate` is appended to surface documentation + # warnings before merge, instead of only in the post-merge `Publish` + # job. `failOnWarning` is enabled in the Dokka setup. + - name: Check documentation + run: ./gradlew dokkaGenerate --stacktrace + # See: https://github.com/marketplace/actions/junit-report-action - name: Publish Test Report uses: mikepenz/action-junit-report@v4.0.3 @@ -30,9 +62,33 @@ jobs: report_paths: '**/build/test-results/**/TEST-*.xml' require_tests: true # will fail workflow if test reports not found + # Probe whether the upload token is available without exposing its value + # to the build/test steps. Scoping `CODECOV_TOKEN` to this trivial step + # (and to the upload step's `with.token`) keeps PR-authored code in + # `./gradlew build` from ever seeing the secret. The `secrets` context is + # not available in a step `if:`, so the upload gates on this output. + - name: Detect Codecov token + id: codecov + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + run: | + if [ -n "$CODECOV_TOKEN" ]; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + fi + + # On `push` (master) always upload โ€” these runs are the only source of + # the `target: auto` baseline, so an absent token there is a + # misconfiguration that should fail loudly rather than silently stop + # refreshing coverage. On `pull_request`, skip when the token is absent: + # forked and Dependabot PRs run without secrets, and `fail_ci_if_error` + # would otherwise redden a healthy PR (coverage gating is meaningless + # there anyway). - name: Upload code coverage report - uses: codecov/codecov-action@v4 + if: steps.codecov.outputs.available == 'true' || github.event_name == 'push' + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false + fail_ci_if_error: true verbose: true diff --git a/.github/workflows/build-on-windows.yml b/.github/workflows/build-on-windows.yml index 4e6b57f..0a07cfa 100644 --- a/.github/workflows/build-on-windows.yml +++ b/.github/workflows/build-on-windows.yml @@ -1,22 +1,24 @@ -name: Build under Windows +name: Windows CI on: pull_request jobs: build: - name: Build under Windows runs-on: windows-latest + name: Build on Windows steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: - submodules: 'true' + 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 @@ -24,8 +26,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 @@ -33,4 +34,4 @@ jobs: if: always() # always run even if the previous step fails with: report_paths: '**/build/test-results/**/TEST-*.xml' - require_tests: true # will fail workflow if test reports not found + require_tests: true diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 0000000..6755ff9 --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,216 @@ +name: Check Links + +# Trigger only when the docs site, the link checker config, or this workflow +# itself changes โ€” unrelated PRs do not need to pay the build+check cost. +on: + pull_request: + paths: + - 'docs/**' + - 'site/**' + - 'lychee.toml' + - '.github/workflows/check-links.yml' + workflow_dispatch: + +env: + HUGO_VERSION: 0.161.1 + LYCHEE_RELEASE: "lychee-x86_64-unknown-linux-gnu.tar.gz" + LYCHEE_VERSION_TAG: "lychee-v0.24.2" + # SHA256 of the above tarball, pinned at download time. Update alongside + # LYCHEE_VERSION_TAG whenever the binary is upgraded. + LYCHEE_SHA256: "1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a" + # Force Hugo to write its module cache where the cache step actually + # restores from. Hugo's default on Linux is `~/.cache/hugo_cache` + # (or `$TMPDIR/hugo_cache_$USER`), neither of which matches the + # `path: /tmp/hugo_cache` cache step below โ€” without this env var, + # the cache would silently never hit. + HUGO_CACHEDIR: /tmp/hugo_cache + +jobs: + check-links: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - name: Checkout + 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 + # `config/` or `config/_default/` subdirectory (both layouts are valid). + # Outputs `present=true|false` and `work_dir` (the directory where + # `npm ci` / `hugo` commands should run โ€” either `$dir/_preview` for + # repos that use a separate preview sub-tree, or `$dir` for repos whose + # Node/Hugo setup lives at the site root). + # When neither directory has a Hugo config, the job short-circuits to a + # success so that this shared workflow stays green on repos that do not + # host a Hugo site at all. + - name: Detect docs site + id: docs + run: | + for dir in docs site; do + for cfg in hugo.toml hugo.yaml \ + config/hugo.toml config/hugo.yaml \ + config/_default/hugo.toml config/_default/hugo.yaml; do + if [ -f "$dir/$cfg" ]; then + if [ -f "$dir/_preview/package-lock.json" ]; then + echo "work_dir=$dir/_preview" >> "$GITHUB_OUTPUT" + echo "present=true" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo site found under $dir/ (work_dir: $dir/_preview)" + elif [ -f "$dir/package-lock.json" ]; then + echo "work_dir=$dir" >> "$GITHUB_OUTPUT" + echo "present=true" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo site found under $dir/ (work_dir: $dir)" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo config found in $dir/ but no package-lock.json found โ€” skipping link check." + fi + exit 0 + fi + done + done + echo "present=false" >> "$GITHUB_OUTPUT" + echo "::notice::No Hugo site found under docs/ or site/ โ€” skipping link check." + + - name: Setup Hugo + if: steps.docs.outputs.present == 'true' + uses: peaceiris/actions-hugo@v3 + with: + hugo-version: ${{ env.HUGO_VERSION }} + extended: true + + # `actions/setup-node@v4` ships with built-in npm caching that hashes + # the lockfile and restores `~/.npm`. We use that instead of a + # standalone `actions/cache@v4` block so there is only one source of + # truth for the cache key (no drift between two layers). + - name: Setup Node + if: steps.docs.outputs.present == 'true' + uses: actions/setup-node@v4 + with: + node-version: '26' + cache: 'npm' + cache-dependency-path: ${{ steps.docs.outputs.work_dir }}/package-lock.json + + # `HUGO_CACHEDIR=/tmp/hugo_cache` (set in `env:` above) makes Hugo + # actually write to the path this step restores from. The key hashes + # both possible go.sum locations so adding/removing a Hugo module + # invalidates the cache deterministically regardless of site root. + - name: Cache Hugo Modules + if: steps.docs.outputs.present == 'true' + uses: actions/cache@v4 + with: + path: /tmp/hugo_cache + key: ${{ runner.os }}-hugomod-${{ hashFiles('docs/**/go.sum', 'site/**/go.sum') }} + restore-keys: | + ${{ runner.os }}-hugomod- + + - name: Install Dependencies + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.work_dir }} + run: npm ci + + - name: Build docs preview site + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.work_dir }} + run: hugo -e development + + # Cache Lychee results to avoid hitting rate limits. + # Key on the lychee.toml hash so that exclude-list edits (e.g. removing + # an exclude pattern) invalidate the cache deterministically; otherwise + # stale `200 OK` entries for the now-checked URLs would be trusted until + # `max_cache_age` expires. + - name: Cache Lychee results + if: steps.docs.outputs.present == 'true' + uses: actions/cache@v4 + with: + path: .lycheecache + key: cache-lychee-${{ runner.os }}-${{ hashFiles('lychee.toml') }} + restore-keys: | + cache-lychee-${{ runner.os }}- + + # The cache key includes LYCHEE_VERSION_TAG so a version bump + # automatically pulls a fresh binary instead of reusing the old one. + # The restore-keys fallback lets a release-filename tweak (rare) reuse + # the existing cached binary for the same version-tag instead of paying + # for a fresh download. + - name: Cache Lychee executable + if: steps.docs.outputs.present == 'true' + id: cache-lychee + uses: actions/cache@v4 + with: + path: lychee + key: ${{ runner.os }}-${{ env.LYCHEE_VERSION_TAG }}-${{ env.LYCHEE_RELEASE }} + restore-keys: | + ${{ runner.os }}-${{ env.LYCHEE_VERSION_TAG }}- + + # We use Lychee directly instead of a GitHub Action because it + # must have access to the local Hugo server, which is not visible + # from the Docker-based action. + # + # `if:` gating uses `hashFiles('lychee/lychee')` rather than + # `steps.cache-lychee.outputs.cache-hit != 'true'`. Per `actions/cache` + # docs, `cache-hit` is only `'true'` on an EXACT key match โ€” a restore + # via `restore-keys` reports `cache-hit == 'false'`, even though the + # binary is present in the workspace. Re-downloading in that case + # would defeat the point of the fallback. `hashFiles` returns an empty + # string when the file is absent, so this guard runs the download iff + # neither the exact key nor any restore-key restored the binary. + - name: Download Lychee executable + uses: robinraju/release-downloader@v1.7 + if: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' + with: + repository: "lycheeverse/lychee" + tag: ${{ env.LYCHEE_VERSION_TAG }} + fileName: ${{ env.LYCHEE_RELEASE }} + + - name: Verify Lychee checksum + if: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' + run: | + echo "${{ env.LYCHEE_SHA256 }} ${{ env.LYCHEE_RELEASE }}" | sha256sum --check --strict + + # The v0.24.2 tarball contains a top-level directory + # (e.g. `lychee-x86_64-unknown-linux-gnu/lychee`), so `--strip-components=1` + # flattens it to `lychee/lychee` โ€” matching what the companion + # `check-links` skill does locally and what the next step expects. + - name: Extract Lychee executable + if: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' + run: | + mkdir -p lychee && + tar -xzf ${{ env.LYCHEE_RELEASE }} --strip-components=1 -C lychee + + # 1. In the generated HTML, some inner links will have absolute URLs and + # the link checker will attempt to fetch them. That's why we need + # a server. Sadly, link checkers have no settings to address this. + # 2. Output redirection is necessary for nohup in GitHub Actions. + # 3. Sleep + `curl` readiness check make sure the server is actually + # serving HTTP before the next step runs Lychee. Without the curl + # probe a silent startup failure (port already bound, missing + # Hugo module, build error surfacing after `nohup` returns 0) + # would manifest 60 s later as "every URL unreachable" Lychee + # errors instead of pointing at the real cause. Mirrors the + # `pgrep -F` guard in the companion `check-links` skill. + # 4. `--port 1313` is set explicitly (not relying on Hugo's default) so + # the coupling with `--base-url http://localhost:1313/` in the next + # Lychee step is visible โ€” change one, change the other. + - name: Start Hugo server + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.work_dir }} + run: | + nohup hugo server \ + --environment development \ + --port 1313 \ + > nohup.out 2> nohup.err < /dev/null & + sleep 5 + if ! curl -sf http://localhost:1313/ > /dev/null; then + echo "ERROR: Hugo server did not respond on port 1313." >&2 + echo "--- stdout ---" >&2; cat nohup.out >&2 || true + echo "--- stderr ---" >&2; cat nohup.err >&2 || true + exit 1 + fi + + - name: Check links + if: steps.docs.outputs.present == 'true' + run: | + ./lychee/lychee --config lychee.toml --timeout 60 \ + --base-url http://localhost:1313/ \ + '${{ steps.docs.outputs.work_dir }}/public/**/*.html' diff --git a/.github/workflows/ensure-reports-updated.yml b/.github/workflows/ensure-reports-updated.yml index fdd8b8e..20deb3f 100644 --- a/.github/workflows/ensure-reports-updated.yml +++ b/.github/workflows/ensure-reports-updated.yml @@ -1,25 +1,35 @@ # 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: Ensure license reports updated +name: License Reports on: pull_request: - branches: - - '**' jobs: - build: - name: Ensure license reports updated + 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. + # Configure the checkout of all branches so that it is possible to run the comparison. fetch-depth: 0 # Check out the `config` submodule to fetch the required script file. submodules: true - - name: Check that both `pom.xml` and license report files are modified + - name: Check that dependency report files are modified shell: bash run: chmod +x ./config/scripts/ensure-reports-updated.sh && ./config/scripts/ensure-reports-updated.sh diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 858cebb..fc0872b 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -1,4 +1,4 @@ -name: Validate Gradle Wrapper +name: Gradle Wrapper validation on: push: branches: @@ -9,11 +9,11 @@ on: jobs: validation: - name: Gradle Wrapper Validation + name: Validate Gradle Wrapper 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 1993841..9a6333f 100644 --- a/.github/workflows/increment-guard.yml +++ b/.github/workflows/increment-guard.yml @@ -1,29 +1,100 @@ -# Ensures that the current lib version is not yet published but executing the Gradle -# `checkVersionIncrement` task. +# Guards the project version by executing the Gradle `checkVersionIncrement` task, +# which verifies that the version is both (a) strictly greater than the base branch +# version in `version.gradle.kts` and (b) not already published. The result is +# published as the `Version Guard` commit status โ€” the context required by branch +# protection and re-published by `revalidate-versions.yml` on the heads of other open +# PRs when the base branch advances (so a stale duplicate bump turns red before merge). +# +# 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: Check version increment +name: Version Guard on: - push: - branches: - - '**' + pull_request: + # Beyond the default activity types (`opened`, `synchronize`, `reopened`), two more are + # needed because they change what the guard must compare against without a new head SHA, + # which would otherwise leave a stale-green `Version Guard` status mergeable: + # * `ready_for_review` โ€” a draft that went stale while in draft becomes ready; and + # * `edited` โ€” the base branch is retargeted (e.g. a release line -> `master`), so the + # strict comparison must be recomputed against the new base. + types: [opened, synchronize, reopened, ready_for_review, edited] jobs: - build: + check: name: Check version increment runs-on: ubuntu-latest + # Default and release-line branches, e.g. `master`, `main`, `2.x-jdk8-master`. For an + # `edited` event, run only when the base actually changed (a retarget carries + # `changes.base.ref.from`); title/body edits carry no `changes.base` and are skipped, so + # the guard is not rebuilt needlessly. + if: >- + (endsWith(github.base_ref, 'master') || endsWith(github.base_ref, 'main')) + && (github.event.action != 'edited' || github.event.changes.base.ref.from != '') + + # `statuses: write` lets the job publish the `Version Guard` commit status that + # branch protection requires. + permissions: + contents: read + statuses: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'true' - - uses: actions/setup-java@v4 + # `checkVersionIncrement` reads `origin/:version.gradle.kts`. The pull request + # checkout does not include the base branch, so fetch its tip into the expected ref. + - name: Fetch the base branch + shell: bash + run: | + git fetch --no-tags --depth=1 \ + origin "+refs/heads/${GITHUB_BASE_REF}:refs/remotes/origin/${GITHUB_BASE_REF}" + + - uses: actions/setup-java@v5 with: java-version: 17 distribution: zulu - cache: gradle - - name: Check version is not yet published + - uses: gradle/actions/setup-gradle@v6 + + - name: Check version increment + id: guard shell: bash + # `VERSION_GUARD` enables the strict base-branch comparison in `checkVersionIncrement`. + # Only this workflow fetches the base ref (the step above), so the comparison is gated + # to it: other CI builds pull the task in via `publishToMavenLocal` on a shallow + # checkout and must not attempt to read `origin/`. + env: + VERSION_GUARD: "true" run: ./gradlew checkVersionIncrement --stacktrace + + # Publish the verdict as the `Version Guard` commit status on the PR head. Posting it + # on every run (success or failure) is what lets a later re-bump clear a failure that + # `revalidate-versions.yml` set when the base branch advanced. + # + # Skipped for fork PRs: `GITHUB_TOKEN` is read-only for them, so the status cannot be + # posted (and a fork head SHA is not in this repo). The Spine agent workflow pushes PR + # branches to the same repository; fork contributions are handled by a maintainer, who + # owns the version bump. + - name: Report the Version Guard status + if: always() && github.event.pull_request.head.repo.fork == false + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + state=failure + if [ "${{ steps.guard.outcome }}" = "success" ]; then + state=success + fi + gh api -X POST "repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }}" \ + -f state="${state}" \ + -f context="Version Guard" \ + -f description="Version increment check" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7de0c51..27c11c0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,17 +6,20 @@ on: jobs: publish: + name: Publish to Maven repositories 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 @@ -61,3 +64,18 @@ jobs: REPO_SLUG: ${{ github.repository }} # e.g. SpineEventEngine/core-jvm GOOGLE_APPLICATION_CREDENTIALS: ./maven-publisher.json NPM_TOKEN: ${{ secrets.NPM_SECRET }} + + # A failed publication on `master` is most often a version collision: a stale + # duplicate bump merged before `revalidate-versions.yml` could turn it red (the + # narrow auto-merge race). The artifact is safe โ€” the registry rejects the + # overwrite โ€” but the fix needs a human/agent, so make the failure loud and + # actionable instead of a quiet red run. + - name: Report a failed publication + if: failure() + shell: bash + run: | + echo "::error title=Publish failed::Publishing to Maven failed on the base branch. If this is a version collision, the version is already published (immutable). Bump 'version.gradle.kts' on the base branch (e.g. via a small PR) and re-run this workflow." + echo "Publish failed. If the cause is a version collision:" + echo " 1. Bump 'version.gradle.kts' on the base branch to the next free version." + echo " 2. Re-run this 'Publish' workflow." + echo "Stale duplicate bumps are normally caught before merge by 'revalidate-versions.yml'." diff --git a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml index fe8ad84..62242c7 100644 --- a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml +++ b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml @@ -34,12 +34,12 @@ env: jobs: retrieve-package-names: - name: Retrieve the package names published from this repository + name: Retrieve package names runs-on: ubuntu-latest outputs: package-names: ${{ steps.request-package-names.outputs.package-names }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'true' @@ -54,7 +54,7 @@ jobs: echo "package-names=$(<./package-names.json)" >> $GITHUB_OUTPUT delete-obsolete-artifacts: - name: Remove obsolete artifacts published from this repository to GitHub Packages + name: Delete obsolete artifacts needs: retrieve-package-names runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/revalidate-versions.yml b/.github/workflows/revalidate-versions.yml new file mode 100644 index 0000000..2d2ecb9 --- /dev/null +++ b/.github/workflows/revalidate-versions.yml @@ -0,0 +1,57 @@ +# Re-judges every other open pull request when the base branch advances. +# +# Publishing runs on every push to a release base branch, so once one pull request merges +# and bumps the version, any other open PR that bumped to the same (or a lower) value is +# now stale: its publish would collide. GitHub does not re-run a PR's checks when its base +# advances, so this workflow does it actively โ€” for each other open PR whose +# `version.gradle.kts` version is `<=` the new base version, it posts a failing +# `Version Guard` commit status on the PR head, blocking the merge until the author +# re-bumps. The status self-clears: the re-bump push runs `increment-guard.yml`, which +# posts a fresh `success` on the new head. +# +# This narrows, but does not close, the race against auto-merge. A PR that is already +# mergeable can merge in the seconds before this fan-out marks it stale; that late merge +# produces a publish collision which the immutable Maven registry rejects (a loud, +# recoverable red Publish), never an overwrite. The deterministic guarantee is the +# registry's immutability, not this signal. + +name: Revalidate Versions + +on: + push: + # Matches the PR guard's `endsWith(base_ref, 'master'|'main')` and the same scope as + # `build-on-ubuntu.yml`. `**` (unlike `*`) also crosses `/`, so slash-named release + # lines such as `release/2.x-master` are covered. Keeping these definitions identical + # ensures every branch guarded on the PR side is also revalidated here. + branches: + - '**master' + - '**main' + +permissions: + contents: read + statuses: write + pull-requests: read + +concurrency: + # Only the newest tip of a given base matters; a later push to the same ref cancels an + # in-flight fan-out for it. Different bases run independently. + group: revalidate-versions-${{ github.ref }} + cancel-in-progress: true + +jobs: + revalidate: + name: Revalidate open PRs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: 'true' + + - name: Revalidate open PRs against the new base version + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + BASE_REF: ${{ github.ref_name }} + # Invoked via `bash` so it does not depend on the script's committed executable bit. + run: bash ./config/scripts/revalidate-versions.sh diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 0000000..6f0130e --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,70 @@ +name: Secret scan + +# Defense-in-depth behind the local `secret-scan` pre-commit hook and the +# `.gitignore` secret patterns: if a credential is committed despite those, this +# fails the pull request before it can merge. Distributed to every Spine repo by +# `./config/pull`. + +on: + pull_request: + push: + branches: + - master + - main + +permissions: + contents: read + +jobs: + gitleaks: + name: gitleaks + runs-on: ubuntu-latest + env: + # Pinned gitleaks version โ€” bump through the usual dependency-update process. + GITLEAKS_VERSION: "8.21.2" + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + # Full history so a pull request's commit range can be scanned. + fetch-depth: 0 + + - name: Install gitleaks + # Run gitleaks as the runner user against the checkout it owns โ€” no + # container, so no "dubious ownership" git error and no GitHub Action + # org-licence requirement. + run: | + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xzf - gitleaks + ./gitleaks version + + - name: Scan + env: + EVENT: ${{ github.event_name }} + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + BEFORE: ${{ github.event.before }} + AFTER: ${{ github.sha }} + run: | + if [ "$EVENT" = pull_request ]; then + # Scan the PR's own commit RANGE: a secret added in one commit and + # deleted in a later commit of the same PR is still caught (a + # working-tree scan would miss it, yet merging keeps the secret-bearing + # commit reachable), while already-rotated secrets in older history + # outside base..head are not re-flagged. + ./gitleaks git --log-opts="$BASE..$HEAD" --redact --verbose --exit-code=1 . + else + # Push to a default branch: scan the pushed commit RANGE (before..after) + # so an add-then-remove batch is caught here too, not only on PRs โ€” the + # leaked commit would otherwise stay reachable on the default branch. A + # branch's first push reports an all-zero `before` (no range); fall back + # to a working-tree scan then. + if [ -n "$BEFORE" ] && [ "$BEFORE" != "0000000000000000000000000000000000000000" ]; then + ./gitleaks git --log-opts="$BEFORE..$AFTER" --redact --verbose --exit-code=1 . + else + # Branch's first push (all-zero `before`): no range to diff against, so + # scan the whole history reachable from the pushed tip as the initial + # import โ€” an add-then-remove within those commits is still caught. + ./gitleaks git --log-opts="$AFTER" --redact --verbose --exit-code=1 . + fi + fi diff --git a/.gitignore b/.gitignore index 48de9f2..ed1b0ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# >>> shared config (managed by ./config/pull -- do not edit inside this block) >>> # # Copyright 2025, TeamDev. All rights reserved. # @@ -39,6 +40,7 @@ # Internal tool directories. .fleet/ +.junie/memory/ # Kotlin temp directories. **/.kotlin/ @@ -52,8 +54,12 @@ .idea/modules .idea/shelf +# `.idea/misc.xml` is intentionally NOT re-included below. It is project-local โ€” +# it holds the per-project JDK name and IDEA's own churn (entry-point list +# indices, external-storage toggles) โ€” so `.idea/*.xml` above keeps it ignored. +# `./config/pull` (via `migrate`) untracks any copy an earlier pull committed. + # Do not ignore the following IDEA settings -!.idea/misc.xml !.idea/codeStyleSettings.xml !.idea/codeStyles/ !.idea/copyright/ @@ -102,14 +108,48 @@ gradle-app.setting # Spine internal directory for storing intermediate artifacts **/.spine/** -# Login details to Maven repository. -# Each workstation should have developer's login defined in this file. +# --------------------------------------------------------------------------- +# Secrets โ€” NEVER commit these. +# +# Encrypted credentials live under `.github/keys/*.gpg` and ARE committed. +# `config/scripts/decrypt.sh` turns each into its PLAINTEXT twin at build / CI / +# publish time (e.g. `spine-dev-framework-ci.json.gpg` -> `spine-dev.json`). The +# decrypted twins below โ€” and any private key or service-account file โ€” must stay +# out of Git. The shared `secret-scan` pre-commit hook is the backstop if one ever +# slips past these patterns. +# --------------------------------------------------------------------------- + +# Maven repository login details; each workstation defines its own. credentials.tar credentials.properties cloudrepo.properties deploy_key_rsa gcs-auth-key.json +# Decrypted Google / GCP service-account keys (plaintext twins of *.gpg). +spine-dev.json +spine-dev-*.json +maven-publisher.json +firebase-sa.json +*-sa.json +*service-account*.json + +# Decrypted credential property files and portal / publisher secrets. +*.secret.properties + +# Private SSH keys (public keys are *.pub and remain committable). +*_rsa +*_dsa +*_ecdsa +*_ed25519 +id_rsa +id_dsa +id_ecdsa +id_ed25519 + +# ...but always keep the committed ENCRYPTED forms. +!*.gpg + # Log files *.log @@ -130,3 +170,64 @@ pubspec.lock /tmp .gradle-test-kit/ + +# Python cache +__pycache__/ +*.pyc + +# Claude working files +/.claude/worktrees/ +# Ephemeral plan-mode scratch (durable task docs live in `.agents/tasks/`). +/.claude/plans/ + +# Personal, per-developer Claude Code settings overrides (never committed; +# the distributed `.claude/settings.json` is the shared, committed layer). +/.claude/settings.local.json + +# Auto-downloaded Lychee binary used by the `check-links` skill. +/.agents/skills/check-links/.cache/ + +# Lychee link-checker cache (created by the `check-links` skill and +# the `Check Links` workflow when run locally). +.lycheecache + +# Hugo docs preview site build artifacts (used by the `check-links` +# skill and the `Check Links` workflow in repos that contain a +# `docs/_preview` Hugo site). +docs/_preview/node_modules/ +docs/_preview/public/ +docs/_preview/resources/ +# <<< shared config <<< + +# >>> repo-local entries (preserved across ./config/pull) >>> +!.idea/codeStyleSettings.xml +!.idea/codeStyles/ +!.idea/copyright/ +!**/src/**/build/** +!gradle-wrapper.jar +# Login details to Maven repository. +# Each workstation should have developer's login defined in this file. +# <<< repo-local entries <<< + +# >>> secret ignores re-asserted last (managed by ./config/pull -- do not edit) >>> +credentials.tar +credentials.properties +cloudrepo.properties +deploy_key_rsa +gcs-auth-key.json +spine-dev.json +spine-dev-*.json +maven-publisher.json +firebase-sa.json +*-sa.json +*service-account*.json +*.secret.properties +*_rsa +*_dsa +*_ecdsa +*_ed25519 +id_rsa +id_dsa +id_ecdsa +id_ed25519 +# <<< secret ignores <<< diff --git a/.gitmodules b/.gitmodules index 5072da7..cf4b76d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "config"] path = config url = https://github.com/SpineEventEngine/config.git +[submodule ".agents/shared"] + path = .agents/shared + url = https://github.com/SpineEventEngine/agents.git + branch = master + update = merge + ignore = all diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 0bd1d9d..7be402d 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -255,6 +255,18 @@