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
87 changes: 75 additions & 12 deletions .github/workflows/check-links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@ on:
- 'docs/**'
- 'site/**'
- 'lychee.toml'
- '.lycheeignore'
- '.github/workflows/check-links.yml'
workflow_dispatch:

env:
HUGO_VERSION: 0.161.1
HUGO_VERSION: 0.169.0
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"
# Pinned to the release the Spine sites were checked with. Newer releases
# add Sass deprecations, which nobody has checked against those sites yet.
DART_SASS_VERSION: 1.99.0
# SHA256 of `dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz`, from the GitHub release.
# Update alongside DART_SASS_VERSION.
DART_SASS_SHA256: "f0a32ab99bc0a1eb62ea7140a056a1bca82d534390b7c3dba4f2016eb0077753"
# 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
Expand All @@ -38,6 +45,11 @@ jobs:
# 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).
# A repo may also keep its only Hugo config in the `_preview/` sub-tree
# (e.g. `docs/_preview/hugo.toml`), so the same paths count there too.
# Hugo finds such a config only when it runs in `$dir/_preview`, so it
# needs `$dir/_preview/package-lock.json`: the `$dir` fallback below
# is for site-root configs only.
# 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
Expand All @@ -51,19 +63,22 @@ jobs:
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
config/_default/hugo.toml config/_default/hugo.yaml \
_preview/hugo.toml _preview/hugo.yaml \
_preview/config/hugo.toml _preview/config/hugo.yaml \
_preview/config/_default/hugo.toml _preview/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
elif [ -f "$dir/package-lock.json" ] && [[ $cfg != _preview/* ]]; 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."
echo "::notice::Found $dir/$cfg but no package-lock.json to build it with — skipping link check."
fi
exit 0
fi
Expand All @@ -79,13 +94,38 @@ jobs:
hugo-version: ${{ env.HUGO_VERSION }}
extended: true

# `actions/setup-node@v4` ships with built-in npm caching that hashes
# The Spine Hugo theme compiles its SCSS with Dart Sass, because Hugo
# deprecated LibSass in v0.153.0 and will remove it. Hugo does not
# bundle Dart Sass: it runs the `sass` binary it finds on `PATH`, and
# a site on that theme fails to build without one. The release
# tarball is standalone, so no Node or Dart setup is needed. The
# checksum guards the download, as it does for Lychee below. Sites
# still on LibSass ignore the binary.
- name: Install Dart Sass
if: steps.docs.outputs.present == 'true'
run: |
archive="dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz"
curl -fsSLO "https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/${archive}"
echo "${DART_SASS_SHA256} ${archive}" | sha256sum --check --strict
tar -xzf "${archive}" -C "${RUNNER_TEMP}"
rm "${archive}"
echo "${RUNNER_TEMP}/dart-sass" >> "${GITHUB_PATH}"

# Hugo finds Dart Sass on `PATH`. `hugo env` lists it when it does.
# `GITHUB_PATH` only takes effect from the next step on, so this check
# cannot live in the install step above. It fails here, rather than
# mid-build, if the install did not put `sass` where Hugo looks.
- name: Check Dart Sass
if: steps.docs.outputs.present == 'true'
run: hugo env | grep 'github.com/sass/dart-sass'

# `actions/setup-node` 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
# standalone `actions/cache` 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
uses: actions/setup-node@v7
with:
node-version: '26'
cache: 'npm'
Expand All @@ -97,7 +137,7 @@ jobs:
# invalidates the cache deterministically regardless of site root.
- name: Cache Hugo Modules
if: steps.docs.outputs.present == 'true'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: /tmp/hugo_cache
key: ${{ runner.os }}-hugomod-${{ hashFiles('docs/**/go.sum', 'site/**/go.sum') }}
Expand All @@ -121,7 +161,7 @@ jobs:
# `max_cache_age` expires.
- name: Cache Lychee results
if: steps.docs.outputs.present == 'true'
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: .lycheecache
key: cache-lychee-${{ runner.os }}-${{ hashFiles('lychee.toml') }}
Expand All @@ -136,7 +176,7 @@ jobs:
- name: Cache Lychee executable
if: steps.docs.outputs.present == 'true'
id: cache-lychee
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: lychee
key: ${{ runner.os }}-${{ env.LYCHEE_VERSION_TAG }}-${{ env.LYCHEE_RELEASE }}
Expand All @@ -156,7 +196,7 @@ jobs:
# 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
uses: robinraju/release-downloader@v1.13
if: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == ''
with:
repository: "lycheeverse/lychee"
Expand Down Expand Up @@ -208,9 +248,32 @@ jobs:
exit 1
fi

# Lychee prints broken links to this step's log only; the run page
# shows just "Process completed with exit code 2". `tee` keeps the
# report in the log and saves a copy, which the next step adds to the
# run summary. (`--output` would not do: it moves the report from the
# log to the file.) `pipefail` keeps Lychee's exit code. Without it,
# the step would take `tee`'s 0 and pass despite broken links.
- name: Check links
if: steps.docs.outputs.present == 'true'
run: |
set -o pipefail
./lychee/lychee --config lychee.toml --timeout 60 \
--base-url http://localhost:1313/ \
'${{ steps.docs.outputs.work_dir }}/public/**/*.html'
'${{ steps.docs.outputs.work_dir }}/public/**/*.html' \
| tee lychee-report.txt

# Runs after a failed `Check links` too, which is when the report
# matters most. `hashFiles` skips this step when Lychee never ran.
# `!cancelled()` needs the `${{ }}` wrapper: a bare leading `!` is
# a YAML tag.
- name: Publish link report
if: ${{ !cancelled() && hashFiles('lychee-report.txt') != '' }}
run: |
{
echo '## Link check'
echo
echo '```text'
cat lychee-report.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
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"
100 changes: 100 additions & 0 deletions .github/workflows/publish-api-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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

# The same step as in `build-on-ubuntu.yml`. For a release version, the build
# runs Dokka for every published module, in parallel, and the Linux runner
# ships with effectively no swap, so a memory peak would be an instant OOM
# kill. It goes before the deploy key is decrypted, so that the action never
# runs with the key on disk.
- name: Add swap space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 8

- 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
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@3a0d13a6bc72f1d3b873a6c5f300aa9bf26ffe0a
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
Loading
Loading