Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Edit(version.gradle.kts)",
"Bash(./gradlew:*)",
"Bash(./config/gradlew:*)",
"Bash(git status:*)",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-on-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
# there anyway).
- name: Upload code coverage report
if: steps.codecov.outputs.available == 'true' || github.event_name == 'push'
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
Expand Down
70 changes: 65 additions & 5 deletions .github/workflows/increment-guard.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Ensures that the current lib version is not yet published by 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
Expand All @@ -15,26 +19,82 @@ name: Version Guard

on:
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:
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')
# 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@v6
with:
submodules: 'true'

# `checkVersionIncrement` reads `origin/<base>: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

- uses: gradle/actions/setup-gradle@v6

- name: Check version is not yet published
- 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/<base>`.
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"
15 changes: 15 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,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'."
57 changes: 57 additions & 0 deletions .github/workflows/revalidate-versions.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 1 addition & 5 deletions .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion buildSrc/src/main/kotlin/DependencyResolution.kt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -30,6 +30,7 @@ import io.spine.dependency.build.Dokka
import io.spine.dependency.build.ErrorProne
import io.spine.dependency.build.FindBugs
import io.spine.dependency.build.JSpecify
import io.spine.dependency.isDokka
import io.spine.dependency.lib.Asm
import io.spine.dependency.lib.AutoCommon
import io.spine.dependency.lib.AutoService
Expand Down Expand Up @@ -73,6 +74,9 @@ fun doForceVersions(configurations: ConfigurationContainer) {
*/
fun NamedDomainObjectContainer<Configuration>.forceVersions() {
all {
if (isDokka) {
return@all
}
resolutionStrategy {
failOnVersionConflict()
cacheChangingModulesFor(0, "seconds")
Expand Down
11 changes: 10 additions & 1 deletion buildSrc/src/main/kotlin/dokka-setup.gradle.kts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -41,6 +41,15 @@ tasks.withType<DokkaBaseTask>().configureEach {
}
}

// The Dokka Javadoc format does not support Kotlin Multiplatform source sets, so its
// publication task fails for KMP modules ("No source set found for <module>/jvmMain").
// KMP modules publish HTML documentation, so skip the Javadoc publication for them.
plugins.withId("org.jetbrains.kotlin.multiplatform") {
tasks.matching { it.name == "dokkaGeneratePublicationJavadoc" }.configureEach {
enabled = false
}
}

afterEvaluate {
dokka {
configureForKotlin(
Expand Down
11 changes: 11 additions & 0 deletions buildSrc/src/main/kotlin/io/spine/dependency/Dependency.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ abstract class DependencyWithBom : Dependency() {
fun Configuration.diagSuffix(project: Project): String =
"the configuration `$name` in the project: `${project.path}`."

/**
* Tells if this configuration belongs to Dokka's own generator/plugin classpath.
*
* Dokka resolves these `dokka*` configurations using dependency versions pinned by
* Dokka itself (for example, Jackson or Kotlin), which legitimately differ from the
* project's. Forcing the project's versions onto them breaks `dokkaGenerate`, so such
* configurations must be excluded from the project's version forcing.
*/
val Configuration.isDokka: Boolean
get() = name.startsWith("dokka")

private fun ResolutionStrategy.forceWithLogging(
project: Project,
configuration: Configuration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ package io.spine.dependency.boms
import io.gitlab.arturbosch.detekt.getSupportedKotlinVersion
import io.spine.dependency.DependencyWithBom
import io.spine.dependency.diagSuffix
import io.spine.dependency.isDokka
import io.spine.dependency.kotlinx.Coroutines
import io.spine.dependency.lib.Kotlin
import io.spine.dependency.test.JUnit
Expand Down Expand Up @@ -88,7 +89,7 @@ class BomsPlugin : Plugin<Project> {
applyBoms(project, Boms.core + Boms.testing)
}

matching { !supportsBom(it.name) }.all {
matching { !supportsBom(it.name) && !it.isDokka }.all {
resolutionStrategy.eachDependency {
if (requested.group == Kotlin.group) {
val kotlinVersion = Kotlin.runtimeVersion
Expand Down Expand Up @@ -170,7 +171,7 @@ private fun supportsBom(name: String) =
private fun Project.forceArtifacts() =
configurations.all {
resolutionStrategy {
if (!isDetekt) {
if (!isDetekt && !isDokka) {
val rs = this@resolutionStrategy
val project = this@forceArtifacts
val cfg = this@all
Expand Down
4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ package io.spine.dependency.local
*/
@Suppress("ConstPropertyName", "unused")
object Base {
const val version = "2.0.0-SNAPSHOT.420"
const val versionForBuildScript = "2.0.0-SNAPSHOT.420"
const val version = "2.0.0-SNAPSHOT.421"
const val versionForBuildScript = "2.0.0-SNAPSHOT.421"
const val group = Spine.group
private const val prefix = "spine"
const val libModule = "$prefix-base"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ object Compiler : Dependency() {
* The version of the Compiler dependencies.
*/
override val version: String
private const val fallbackVersion = "2.0.0-SNAPSHOT.054"
private const val fallbackVersion = "2.0.0-SNAPSHOT.057"

/**
* The distinct version of the Compiler used by other build tools.
Expand All @@ -81,7 +81,7 @@ object Compiler : Dependency() {
* transitive dependencies, this is the version used to build the project itself.
*/
val dogfoodingVersion: String
private const val fallbackDfVersion = "2.0.0-SNAPSHOT.054"
private const val fallbackDfVersion = "2.0.0-SNAPSHOT.057"

/**
* The artifact for the Compiler Gradle plugin.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ typealias CoreJava = CoreJvm
@Suppress("ConstPropertyName", "unused")
object CoreJvm {
const val group = Spine.group
const val version = "2.0.0-SNAPSHOT.380"
const val version = "2.0.0-SNAPSHOT.381"

const val coreArtifact = "spine-core"
const val clientArtifact = "spine-client"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ object CoreJvmCompiler {
/**
* The version used in the build classpath.
*/
const val dogfoodingVersion = "2.0.0-SNAPSHOT.079"
const val dogfoodingVersion = "2.0.0-SNAPSHOT.080"

/**
* The version to be used for integration tests.
*/
const val version = "2.0.0-SNAPSHOT.079"
const val version = "2.0.0-SNAPSHOT.080"

/**
* The ID of the Gradle plugin.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ package io.spine.dependency.local
*/
@Suppress("ConstPropertyName", "unused")
object Logging {
const val version = "2.0.0-SNAPSHOT.417"
const val version = "2.0.0-SNAPSHOT.419"
const val group = Spine.group

const val loggingArtifact = "spine-logging"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.401"
const val dogfoodingVersion = "2.0.0-SNAPSHOT.401"
const val version = "2.0.0-SNAPSHOT.402"
const val dogfoodingVersion = "2.0.0-SNAPSHOT.402"

const val lib = "$group:tool-base:$version"
const val classicCodegen = "$group:classic-codegen:$version"
Expand Down
Loading
Loading