diff --git a/.codecov.yml b/.codecov.yml index b5739f89b..e05fc77b6 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/workflows/build-on-ubuntu.yml b/.github/workflows/build-on-ubuntu.yml index 01252d9f6..6884a40f7 100644 --- a/.github/workflows/build-on-ubuntu.yml +++ b/.github/workflows/build-on-ubuntu.yml @@ -1,40 +1,57 @@ -name: Build under Ubuntu +name: Ubuntu CI on: push jobs: build: + 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@v2 + - uses: actions/checkout@v6 + with: + submodules: 'true' - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v5 with: - java-version: 11 + java-version: 17 distribution: zulu - cache: gradle - - uses: actions/setup-node@v2 - with: - node-version: 'lts/*' + - uses: gradle/actions/setup-gradle@v6 - - name: Pull config - run: git submodule update --init --recursive + # 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 and run tests + - 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 on each push, 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@v2.8.4 + uses: mikepenz/action-junit-report@v4.0.3 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/test/TEST-*.xml' + report_paths: '**/build/test-results/**/TEST-*.xml' + require_tests: true # will fail workflow if test reports not found - name: Upload code coverage report - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v4 with: - fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true verbose: true diff --git a/.github/workflows/build-on-windows.yml b/.github/workflows/build-on-windows.yml index 3506d13d6..0a07cfab6 100644 --- a/.github/workflows/build-on-windows.yml +++ b/.github/workflows/build-on-windows.yml @@ -1,35 +1,37 @@ -name: Build under Windows +name: Windows CI on: pull_request jobs: build: runs-on: windows-latest + name: Build on Windows steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 + with: + submodules: recursive + fetch-depth: 0 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v5 with: - java-version: 11 + java-version: 17 distribution: zulu - cache: gradle - - uses: actions/setup-node@v2 - with: - node-version: 'lts/*' + - uses: gradle/actions/setup-gradle@v6 - - name: Pull config - run: git submodule update --init --recursive + # See: https://github.com/al-cheb/configure-pagefile-action + - name: Configure Pagefile + uses: al-cheb/configure-pagefile-action@v1.3 - 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 - uses: mikepenz/action-junit-report@v2.8.4 + uses: mikepenz/action-junit-report@v4.0.3 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/test/TEST-*.xml' + report_paths: '**/build/test-results/**/TEST-*.xml' + require_tests: true diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 000000000..6755ff910 --- /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 new file mode 100644 index 000000000..20deb3f69 --- /dev/null +++ b/.github/workflows/ensure-reports-updated.yml @@ -0,0 +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: License Reports + +on: + pull_request: + +jobs: + check: + name: Ensure license reports are updated + runs-on: ubuntu-latest + # Default and release-line branches, e.g. `master`, `main`, `2.x-jdk8-master`. + if: endsWith(github.base_ref, 'master') || endsWith(github.base_ref, 'main') + + steps: + - uses: actions/checkout@v6 + with: + # 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 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 72435c635..fc0872b4c 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,10 +9,11 @@ on: jobs: validation: - name: Validation + name: Validate Gradle Wrapper runs-on: ubuntu-latest steps: - name: Checkout latest code - uses: actions/checkout@v2 + uses: actions/checkout@v6 + - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/actions/wrapper-validation@v4 diff --git a/.github/workflows/increment-guard.yml b/.github/workflows/increment-guard.yml index c96b081b0..f20b4bed6 100644 --- a/.github/workflows/increment-guard.yml +++ b/.github/workflows/increment-guard.yml @@ -1,28 +1,39 @@ -# Ensures that the current lib version is not yet published but executing the Gradle +# Ensures that the current lib version is not yet published by executing the Gradle # `checkVersionIncrement` task. +# +# The check runs only for pull requests targeting a default (`master`/`main`) or +# a release-line (e.g. `2.x-jdk8-master`) branch. It is the responsibility of a branch +# which aims to merge into such a branch to bump the version. Auxiliary branches +# do not deal with the versions in the release cycle and are not guarded. +# +# The base branch is checked inside the job rather than via the `branches` filter: +# a workflow skipped by branch filtering leaves its check in the `Pending` state, +# blocking PRs which require it, while a job skipped via `if` reports `skipped`, +# which satisfies required status checks. -name: Check version increment +name: Version Guard on: - push: - branches: - - '**' + pull_request: jobs: - build: + check: + name: Check version increment runs-on: ubuntu-latest + # Default and release-line branches, e.g. `master`, `main`, `2.x-jdk8-master`. + if: endsWith(github.base_ref, 'master') || endsWith(github.base_ref, 'main') steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 + with: + submodules: 'true' - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v5 with: - java-version: 11 + java-version: 17 distribution: zulu - cache: gradle - - name: Pull config - run: git submodule update --init --recursive + - uses: gradle/actions/setup-gradle@v6 - name: Check version is not yet published shell: bash diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c8954df48..df8f6cd01 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,21 +6,20 @@ on: jobs: publish: + name: Publish to Maven repositories runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + steps: + - uses: actions/checkout@v6 with: - java-version: 11 - distribution: zulu - cache: gradle + submodules: 'true' - - uses: actions/setup-node@v2 + - uses: actions/setup-java@v5 with: - node-version: 'lts/*' + java-version: 17 + distribution: zulu - - run: git submodule update --init --recursive + - 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 @@ -62,6 +61,6 @@ jobs: 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-java + REPO_SLUG: ${{ github.repository }} # e.g. SpineEventEngine/core-jvm GOOGLE_APPLICATION_CREDENTIALS: ./maven-publisher.json NPM_TOKEN: ${{ secrets.NPM_SECRET }} diff --git a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml new file mode 100644 index 000000000..62242c7bb --- /dev/null +++ b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml @@ -0,0 +1,73 @@ +# +# Periodically removes obsolete artifacts from GitHub Packages. +# +# Only non-release artifacts—those containing "SNAPSHOT" in their version name—are eligible +# for removal. The latest non-release artifacts will be retained, with the exact number determined +# by the `VERSION_COUNT_TO_KEEP` environment variable. +# +# Please note the following details: +# +# 1. An artifact cannot be deleted if it is public and has been downloaded more than 5,000 times. +# In this scenario, contact GitHub support for further assistance. +# +# 2. This workflow only applies to artifacts published from this repository. +# +# 3. A maximum of 100 artifacts can be removed per run from each package; +# if there are more than 100 obsolete artifacts, either manually restart the workflow +# or wait for the next scheduled removal. +# +# 4. When artifacts with version `x.x.x-SNAPSHOT` are published, GitHub automatically appends +# the current timestamp, resulting in versions like `x.x.x-SNAPSHOT.20241024.173759`. +# All such artifacts are grouped into one package and treated as a single package +# in GitHub Packages with the version `x.x.x-SNAPSHOT`. Consequently, it is not possible +# to remove obsolete versions within a package; only the entire package can be deleted. +# + +name: Remove obsolete Maven artifacts from GitHub Packages + +on: + schedule: + - cron: '0 0 * * *' # Run every day at midnight. + +env: + VERSION_COUNT_TO_KEEP: 5 # Number of most recent SNAPSHOT versions to retain. + +jobs: + retrieve-package-names: + name: Retrieve package names + runs-on: ubuntu-latest + outputs: + package-names: ${{ steps.request-package-names.outputs.package-names }} + steps: + - uses: actions/checkout@v6 + with: + submodules: 'true' + + - name: Retrieve the names of packages + id: request-package-names + shell: bash + run: | + repoName=$(echo ${{ github.repository }} | cut -d '/' -f2) + chmod +x ./config/scripts/request-package-names.sh + ./config/scripts/request-package-names.sh ${{ github.token }} \ + $repoName ${{ github.repository_owner }} ./package-names.json + echo "package-names=$(<./package-names.json)" >> $GITHUB_OUTPUT + + delete-obsolete-artifacts: + name: Delete obsolete artifacts + needs: retrieve-package-names + runs-on: ubuntu-latest + strategy: + matrix: + package-name: ${{ fromJson(needs.retrieve-package-names.outputs.package-names) }} + steps: + - name: Remove obsolete artifacts from '${{ matrix.package-name }}' package + uses: actions/delete-package-versions@v5 + with: + owner: ${{ github.repository_owner }} + package-name: ${{ matrix.package-name }} + package-type: 'maven' + token: ${{ github.token }} + min-versions-to-keep: ${{ env.VERSION_COUNT_TO_KEEP }} + # Ignores artifacts that do not contain the word "SNAPSHOT". + ignore-versions: '^(?!.+SNAPSHOT).*$' diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 809943cce..f60c2734a 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,6 @@ + - + \ No newline at end of file diff --git a/.idea/copyright/TeamDev_Open_Source.xml b/.idea/copyright/TeamDev_Open_Source.xml index 14d7385ef..cea7fede3 100644 --- a/.idea/copyright/TeamDev_Open_Source.xml +++ b/.idea/copyright/TeamDev_Open_Source.xml @@ -1,6 +1,6 @@ - diff --git a/.idea/dictionaries/common.xml b/.idea/dictionaries/common.xml index c6d06b803..d1c3a7bfe 100644 --- a/.idea/dictionaries/common.xml +++ b/.idea/dictionaries/common.xml @@ -5,6 +5,7 @@ arraybuffer aspx bytebuffer + callees closeables cqrs dartdocs @@ -23,6 +24,8 @@ handshaker hohpe idempotency + jspecify + kotest lempira liskov melnik @@ -44,15 +47,18 @@ processmanager procman proto's + protodata protos sfixed stderr stringifier stringifiers + substituter switchman testutil threeten tuples + unicast unregister unregistering unregisters @@ -62,4 +68,4 @@ yevsyukov - + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index d4e24101b..7be402da6 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -27,6 +27,9 @@