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
20 changes: 12 additions & 8 deletions .github/workflows/increment-guard.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# 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).
# published as the `Version bump` commit status, which `revalidate-versions.yml` also
# re-publishes on the heads of other open PRs when the base branch advances (so a stale
# duplicate bump turns red before merge).
#
# Branch protection must require the `Version bump` status, not the job's own
# `Check version increment` check run: only the status carries the revalidation verdict,
# and a job skipped by its `if` reports a passing check run.
#
# 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 @@ -21,7 +25,7 @@ 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:
# which would otherwise leave a stale-green `Version bump` 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.
Expand All @@ -39,7 +43,7 @@ jobs:
(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
# `statuses: write` lets the job publish the `Version bump` commit status that
# branch protection requires.
permissions:
contents: read
Expand Down Expand Up @@ -76,15 +80,15 @@ jobs:
VERSION_GUARD: "true"
run: ./gradlew checkVersionIncrement --stacktrace

# Publish the verdict as the `Version Guard` commit status on the PR head. Posting it
# Publish the verdict as the `Version bump` 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
- name: Report the Version bump status
if: always() && github.event.pull_request.head.repo.fork == false
shell: bash
env:
Expand All @@ -96,5 +100,5 @@ jobs:
fi
gh api -X POST "repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }}" \
-f state="${state}" \
-f context="Version Guard" \
-f context="Version bump" \
-f description="Version increment check"
90 changes: 90 additions & 0 deletions .github/workflows/publish-api-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Publish API docs

# Publishes the API documentation of the published modules to the `gh-pages`
# branch, which GitHub Pages serves under `spine.io`.
#
# The job runs apart from `publish.yml`, and neither waits for the other: a
# failure here leaves the Maven publication intact, and the other way round.
# `:publishAllApiDocs` is registered in the root project by `spinePublishing { }`,
# so it exists in every repository that publishes, and does nothing where no
# module documents itself. It runs the `publishApiDocs` task of each published
# module.
#
# Only release versions are published. For a snapshot version, which most merges
# produce, the task of each module only says that it skips the documentation,
# and the run ends green without generating or pushing any.
#
# A failed run can be re-run as it is: each module commits its documentation
# even when nothing changed, and retries a failed push after rebasing onto
# `gh-pages`.
#
# Each module clones only the tip of `gh-pages` and checks out only its own most
# recent documentation and the folder of its version, so that a run does not
# fetch the documentation of every version of every module.
#
# This job does not ask whether `publish.yml` could publish the version, as that
# would couple the two again. When `publish.yml` stops because the version is
# already taken, which the version checks of pull requests normally prevent,
# this job still copies the documentation of the commit over that of the version.
#
# Unlike `publish.yml`, this file is not a caller of a reusable workflow hosted
# by `config`. That split exists to make `config` the signer of the attestation
# of the published artifacts, and the documentation is not attested.

on:
push:
branches: [master]

jobs:
publish-api-docs:
name: Publish API docs
runs-on: ubuntu-latest

# One run at a time per repository, since every run pushes to `gh-pages`.
# `queue: max` keeps up to 100 runs waiting, instead of letting a newer run
# cancel the waiting one: each run also adds the documentation of its own
# version, which no later run would publish. The waiting runs may start in
# another order than their commits, so a run replaces the most recent
# documentation of a module only if no newer version is published already.
concurrency:
group: api-docs-publishing-${{ github.repository }}
queue: max

# The push goes over SSH with the deploy key, not with `GITHUB_TOKEN`, which
# only reads the dependencies published to GitHub Packages.
permissions:
contents: read
packages: read

steps:
- uses: actions/checkout@v6
with:
submodules: 'true'

- uses: actions/setup-java@v5
with:
java-version: 17
distribution: zulu

# The cache stays on, unlike in `publishing.yml`, which turns it off to keep
# its attestation trustworthy: nothing here is attested. A run on `master`
# reads only the caches written by the workflows that run on `master`.
- uses: gradle/actions/setup-gradle@v6

- name: Decrypt Git SSH credentials
run: ./config/scripts/decrypt.sh "$GIT_CREDENTIALS_KEY" ./.github/keys/deploy_key_rsa.gpg ./deploy_key_rsa
env:
GIT_CREDENTIALS_KEY: ${{ secrets.GIT_CREDENTIALS_KEY }}

# The SSH agent does not accept a key that others can read. The build runs
# `register-ssh-key.sh` to hand the key to the agent.
- name: Set file system permissions
run: chmod 400 ./deploy_key_rsa && chmod +x ./config/scripts/register-ssh-key.sh

- name: Publish API docs
run: ./gradlew :publishAllApiDocs --stacktrace
Comment thread
alexander-yevsyukov marked this conversation as resolved.
Resolved
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FORMAL_GIT_HUB_PAGES_AUTHOR: developers@spine.io
# https://docs.github.com/en/actions/reference/environment-variables
REPO_SLUG: ${{ github.repository }} # e.g. SpineEventEngine/core-jvm
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ on:
jobs:
publish:
name: Publish to Maven repositories
uses: SpineEventEngine/config/.github/workflows/publishing.yml@ad3dff0890c7716cbba3fe2ce17bf4fe5905a318
uses: SpineEventEngine/config/.github/workflows/publishing.yml@58cd2d4a314a79970e20c3587d70d90b69cf84bb
permissions:
contents: read
packages: write
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/revalidate-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# 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
# `Version bump` 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.
#
Expand Down Expand Up @@ -40,14 +40,14 @@ concurrency:

jobs:
revalidate:
name: Revalidate open PRs
name: Mark stale version bumps
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: 'true'

- name: Revalidate open PRs against the new base version
- name: Mark open PRs whose version is not above the new base
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/DokkaExts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ fun Task.isInPublishingGraph(): Boolean =
* Such a build usually feeds integration tests, which need the published code,
* but not its documentation. So the Dokka tasks are skipped when the execution graph
* contains a [PublishToMavenLocal] task and no task that ships the documentation
* elsewhere: `publish`, a [PublishToMavenRepository] task, or `updateGitHubPages`.
* elsewhere: `publish`, a [PublishToMavenRepository] task, or `publishApiDocs`.
* They still run if a task named on the command line selects a documentation task of
* the graph: one that has `dokka` in its name, ignoring case, or a documentation JAR,
* `javadocJar` or `htmlDocsJar`. The name may be the full one, e.g., `dokkaGenerate`,
Expand Down Expand Up @@ -276,7 +276,7 @@ fun Project.skipDokkaWhenPublishingToMavenLocal() {
/**
* The names of the lifecycle tasks that ship the documentation beyond Maven Local.
*/
private val docsShippingTaskNames = setOf("publish", TaskName.updateGitHubPages)
private val docsShippingTaskNames = setOf("publish", TaskName.publishApiDocs)

/**
* Tells if this graph publishes to Maven Local and does not contain a task that ships
Expand Down
41 changes: 27 additions & 14 deletions buildSrc/src/main/kotlin/io/spine/gradle/Cli.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import java.io.File
import java.io.InputStream
import java.io.StringWriter
import java.lang.ProcessBuilder.Redirect.PIPE
import java.util.*

/**
* Executes a process from Gradle code.
Expand All @@ -32,20 +31,34 @@ class Cli(private val workingFolder: File) {
/**
* Executes the given terminal command and retrieves the command output.
*
* [Executes][Runtime.exec] the given `String` array as a CLI command.
* Runs the given command, the program followed by its arguments, in
* the [workingFolder].
*
* If the execution is successful, returns the command output.
* Throws an [IllegalStateException] otherwise.
*
* @param command the command to execute.
* @return the command line output.
* @return the standard output as the command wrote it, line breaks included,
* decoded as UTF-8.
* @throws IllegalStateException if the execution fails.
*/
fun execute(vararg command: String): String {
fun execute(vararg command: String): String = execute(command.asList())

/**
* Executes the given terminal command and retrieves the command output.
*
* Works like the `vararg` overload, for a command assembled as a list.
*
* @param command the command to execute, the program followed by its arguments.
* @return the standard output as the command wrote it, line breaks included,
* decoded as UTF-8.
* @throws IllegalStateException if the execution fails.
*/
fun execute(command: List<String>): String {
val outWriter = StringWriter()
val errWriter = StringWriter()

val process = ProcessBuilder(*command).apply {
val process = ProcessBuilder(command).apply {
directory(workingFolder)
redirectOutput(PIPE)
redirectError(PIPE)
Expand All @@ -66,22 +79,22 @@ class Cli(private val workingFolder: File) {
val commandLine = command.joinToString(" ")
val nl = System.lineSeparator()
val errorMsg = "Command `$commandLine` finished with exit code $exitCode:" +
"$nl$errWriter" +
"$nl$outWriter."
"$nl${errWriter.toString().trimEnd()}" +
"$nl${outWriter.toString().trimEnd()}."
throw IllegalStateException(errorMsg)
}
}
}

/**
* Starts a background thread that reads all lines from this [InputStream] and
* appends them to [dest], returning the thread so the caller can [join][Thread.join]
* it once the process has exited, ensuring the buffer holds the complete output.
* Starts a background thread that reads all of this [InputStream] and appends it
* to [dest], returning the thread so the caller can [join][Thread.join] it once
* the process has exited, ensuring the buffer holds the complete output.
*
* The text is appended as the process wrote it, line breaks included, so that
* a multi-line output can be split into lines.
*/
private fun InputStream.pourTo(dest: StringWriter): Thread =
Thread {
val sc = Scanner(this)
while (sc.hasNextLine()) {
dest.append(sc.nextLine())
}
bufferedReader().use { dest.append(it.readText()) }
}.also { it.start() }
20 changes: 20 additions & 0 deletions buildSrc/src/main/kotlin/io/spine/gradle/TaskName.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package io.spine.gradle
import kotlin.reflect.KClass
import org.gradle.api.Task
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.named
import org.gradle.kotlin.dsl.register

Expand Down Expand Up @@ -45,3 +46,22 @@ internal fun <T : Task> TaskContainer.named(name: TaskName<T>) = named(name.valu
*/
internal fun <T : Task> TaskContainer.register(name: TaskName<T>, init: T.() -> Unit) =
register(name.value, name.clazz, init)

/**
* Returns the task named [name] in this container, registering it with [init]
* if it is not there yet.
*
* Unlike [register][TaskContainer.register], this does not fail when the task is
* already registered, so it suits code that may reach the same task more than once.
* [init] applies only when this call registers the task; for an existing task it is
* ignored, so a setup needed in both cases goes to [TaskProvider.configure].
*/
internal fun TaskContainer.getOrRegister(
name: String,
init: Task.() -> Unit = {}
): TaskProvider<Task> =
if (name in names) {
named(name)
} else {
register(name, init)
}
Loading
Loading