From 6ecbdbc90612610994997cfec55a2f6a75011b6c Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Tue, 9 Jun 2026 12:22:25 -0400 Subject: [PATCH 1/8] ci: build CLI only when it changes, reuse prior release otherwise #36080 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trunk (cicd_3-trunk.yml): gate the native CLI build on the `cli` path filter and gate deploy-cli on the same predicate, so the 3-OS GraalVM matrix (incl. two macOS runners) is skipped when tools/dotcms-cli/** is unchanged. Release (cicd_release-cli.yml): detect whether the CLI changed since the previous release tag (fail-safe: build when uncertain) and, when unchanged, skip the native build + JReleaser and republish the prior release's binaries under the new version across all channels — npm, the dotcms-cli- GitHub release, and Artifactory. Remove the Slack announcement from the CLI release cycle. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/cicd_3-trunk.yml | 15 +- .github/workflows/cicd_release-cli.yml | 196 ++++++++++++++++++++++--- 2 files changed, 189 insertions(+), 22 deletions(-) diff --git a/.github/workflows/cicd_3-trunk.yml b/.github/workflows/cicd_3-trunk.yml index bcfe6cb7c0b9..43553ed15efe 100644 --- a/.github/workflows/cicd_3-trunk.yml +++ b/.github/workflows/cicd_3-trunk.yml @@ -112,11 +112,17 @@ jobs: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} # CLI Build job - builds CLI artifacts - # Skipped when java-version is overridden due to GraalVM/Quarkus compatibility requirements + # Skipped when java-version is overridden due to GraalVM/Quarkus compatibility requirements. + # Also skipped when the CLI sources are unchanged: the CLI is self-contained under + # tools/dotcms-cli/** (its only com.dotcms dependency, dotcms-api-data-model, lives in that + # same subtree), so the `cli` path filter is a correct change-key. This avoids the expensive + # 3-OS native matrix (incl. two macOS runners) on every trunk push when the CLI didn't change. + # When skipped, the deployment job's deploy-cli input is also disabled (see below) so it does + # not try to download CLI artifacts that were never produced. build-cli: name: CLI Build needs: [ initialize,test ] - if: always() && !failure() && !cancelled() && !inputs.java-version + if: always() && !failure() && !cancelled() && !inputs.java-version && fromJSON(needs.initialize.outputs.filters).cli == 'true' uses: ./.github/workflows/cicd_comp_cli-native-build-phase.yml with: buildNativeImage: true @@ -130,7 +136,10 @@ jobs: uses: ./.github/workflows/cicd_comp_deployment-phase.yml with: artifact-run-id: ${{ needs.initialize.outputs.artifact-run-id }} - deploy-cli: true + # Only deploy the CLI when it was actually (re)built this run. build-cli is skipped when the + # CLI sources are unchanged or java-version is overridden; in those cases there are no fresh + # cli-artifacts-* to deploy, so deploy-cli must be false to avoid a missing-artifact failure. + deploy-cli: ${{ !inputs.java-version && fromJSON(needs.initialize.outputs.filters).cli == 'true' }} publish-npm-sdk-libs: ${{ fromJSON(needs.initialize.outputs.filters).sdk_libs == 'true' && github.event_name != 'workflow_dispatch' }} environment: trunk # tag-identifier intentionally omitted: trunk uses only the environment name as its tag diff --git a/.github/workflows/cicd_release-cli.yml b/.github/workflows/cicd_release-cli.yml index be6f1fe66a6b..310a87052e53 100644 --- a/.github/workflows/cicd_release-cli.yml +++ b/.github/workflows/cicd_release-cli.yml @@ -13,7 +13,6 @@ # - Performs pre-checks and version management # - Builds and packages the CLI tool # - Publishes the CLI as an NPM package -# - Sends Slack notifications upon successful release # - Cleans up temporary branches after release name: Release - CLI @@ -66,6 +65,14 @@ jobs: RELEASE_VERSION: ${{ steps.version.outputs.RELEASE_VERSION }} HEAD: ${{ steps.version.outputs.HEAD }} AUXILIARY_BRANCH: ${{ steps.version.outputs.AUXILIARY_BRANCH }} + # 'true' -> CLI sources changed since the previous release: do a full native build. + # 'false' -> unchanged: skip the native build/JReleaser and republish the previously + # published binaries under the new version (npm: publish-npm-package; + # GitHub release + Artifactory: reuse-release). + CLI_CHANGED: ${{ steps.cli-changes.outputs.CLI_CHANGED }} + # Maven/JReleaser version of the previous release (tag without the leading 'v'), used by the + # reuse path to locate the prior release's artifacts. Empty when there is no previous tag. + PREV_RELEASE_VERSION: ${{ steps.cli-changes.outputs.PREV_RELEASE_VERSION }} steps: # Log GitHub context for debugging - name: 'Log GitHub context' @@ -87,8 +94,12 @@ jobs: echo "::endgroup::" # Checkout the repository + # fetch-depth: 0 brings full history + all tags so we can diff this release + # against the previous release tag to decide whether the CLI actually changed. - name: 'Checkout' uses: actions/checkout@v4 + with: + fetch-depth: 0 # Setup git configuration - name: 'Setup git config' @@ -96,6 +107,52 @@ jobs: git config user.name "${{ secrets.CI_MACHINE_USER }}" git config user.email "dotCMS-Machine-User@dotcms.com" + # Determine whether the CLI sources changed since the previous release. + # The CLI is self-contained under tools/dotcms-cli/** (its only com.dotcms + # dependency, dotcms-api-data-model, lives in that same subtree), so a path + # diff against the previous release tag is a correct change-key. + # + # Fail safe: any time we cannot confidently prove "unchanged" (manual dispatch, + # no previous tag, shallow history) we set CLI_CHANGED=true and do a full build. + # A false "unchanged" would silently ship stale binaries under a new version. + - name: 'Detect CLI source changes' + id: cli-changes + run: | + set -euo pipefail + CURRENT_TAG="${{ github.event.release.tag_name }}" + + if [[ -z "${CURRENT_TAG}" ]]; then + echo "::notice::No release tag in context (manual dispatch) - building CLI unconditionally" + echo "CLI_CHANGED=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git fetch --tags --quiet || true + + # Previous non-LTS v* tag by creation order, excluding the current one. + PREV_TAG=$(git tag --sort=-creatordate --list 'v*' \ + | grep -viE 'lts' \ + | grep -vxF "${CURRENT_TAG}" \ + | head -n1 || true) + + if [[ -z "${PREV_TAG}" ]]; then + echo "::notice::No previous release tag found - building CLI" + echo "CLI_CHANGED=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Expose the previous release version (without leading 'v') for the reuse path. + echo "PREV_RELEASE_VERSION=${PREV_TAG#v}" >> "$GITHUB_OUTPUT" + + echo "::notice::Comparing tools/dotcms-cli between ${PREV_TAG} and ${CURRENT_TAG}" + if git diff --quiet "${PREV_TAG}" "${CURRENT_TAG}" -- tools/dotcms-cli/; then + echo "::notice::No CLI source changes since ${PREV_TAG} - will reuse previously published binaries" + echo "CLI_CHANGED=false" >> "$GITHUB_OUTPUT" + else + echo "::notice::CLI source changed since ${PREV_TAG} - full native build" + echo "CLI_CHANGED=true" >> "$GITHUB_OUTPUT" + fi + # Set release version and create auxiliary branch - name: 'Set release version' id: version @@ -123,10 +180,13 @@ jobs: echo "AUXILIARY_BRANCH=$AUXILIARY_BRANCH" >> "$GITHUB_OUTPUT" # Build the release + # Skipped when the CLI is unchanged: the only downstream consumer in that case is the + # publish-npm-package reuse path, which pulls the previously published binaries and needs + # neither the core .m2 nor freshly built artifacts. build: name: Release Build needs: [ initialize, precheck ] - if: needs.initialize.outputs.found_artifacts == 'false' + if: needs.initialize.outputs.found_artifacts == 'false' && needs.precheck.outputs.CLI_CHANGED == 'true' uses: ./.github/workflows/cicd_comp_build-phase.yml with: core-build: true @@ -138,11 +198,12 @@ jobs: contents: read packages: write - # Build CLI artifacts + # Build CLI artifacts (the expensive 3-OS GraalVM native matrix, incl. two macOS runners). + # Skipped when the CLI is unchanged since the previous release - we reuse the prior binaries. build-cli: name: Release CLI Build needs: [ initialize, precheck, build ] - if: always() && !failure() && !cancelled() + if: always() && !failure() && !cancelled() && needs.precheck.outputs.CLI_CHANGED == 'true' uses: ./.github/workflows/cicd_comp_cli-native-build-phase.yml with: buildNativeImage: true @@ -150,9 +211,11 @@ jobs: version: ${{ needs.precheck.outputs.RELEASE_VERSION }} branch: ${{ needs.precheck.outputs.AUXILIARY_BRANCH }} - # Perform the release + # Perform the release (JReleaser: native binaries -> GitHub release assets + Artifactory). + # Skipped when the CLI is unchanged; the previously published artifacts already cover this. release: needs: [ precheck, build-cli ] + if: always() && !failure() && !cancelled() && needs.precheck.outputs.CLI_CHANGED == 'true' runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} steps: - name: 'Check out repository' @@ -184,10 +247,87 @@ jobs: artifacts-from: ${{ env.ARTIFACT_RUN_ID }} version: ${{ needs.precheck.outputs.RELEASE_VERSION }} + # Reuse path (GitHub release + Artifactory parity). + # When the CLI is unchanged we skip the JReleaser `release` job above and instead republish the + # previous release's native artifacts under the new version, mirroring the two channels that + # JReleaser feeds (see tools/dotcms-cli/jreleaser.yml - keep this job in sync with it): + # 1. GitHub release -> tag `dotcms-cli-` with the native zips + runner jar attached + # 2. Artifactory -> libs-release-local/com/dotcms/dotcms-cli// + # The binaries are byte-identical to the prior release; only the version in the filename and + # coordinates advances. npm is handled separately in publish-npm-package. + # Note: JReleaser-generated checksums/signatures are NOT reproduced here - only the binary + # artifacts (.zip / .jar) are re-published. + reuse-release: + name: Reuse CLI Release + needs: [ precheck ] + if: needs.precheck.outputs.CLI_CHANGED == 'false' + runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} + permissions: + contents: write + env: + NEW_VERSION: ${{ needs.precheck.outputs.RELEASE_VERSION }} + PREV_VERSION: ${{ needs.precheck.outputs.PREV_RELEASE_VERSION }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: 'Checkout' + uses: actions/checkout@v4 + + - name: 'Download previous CLI release assets' + run: | + set -euo pipefail + mkdir -p prev new + echo "::notice::Reusing dotcms-cli-${PREV_VERSION} artifacts for new version ${NEW_VERSION}" + gh release download "dotcms-cli-${PREV_VERSION}" \ + --repo "${{ github.repository }}" \ + --dir prev + ls -l prev + + - name: 'Re-stamp artifacts to new version' + run: | + set -euo pipefail + shopt -s nullglob + artifacts=( prev/dotcms-cli-${PREV_VERSION}*.zip prev/dotcms-cli-${PREV_VERSION}*.jar ) + if [ ${#artifacts[@]} -eq 0 ]; then + echo "::error::No reusable .zip/.jar artifacts found in release dotcms-cli-${PREV_VERSION}" + exit 1 + fi + for f in "${artifacts[@]}"; do + base=$(basename "$f") + newname="${base/dotcms-cli-${PREV_VERSION}/dotcms-cli-${NEW_VERSION}}" + cp "$f" "new/${newname}" + done + echo "Re-stamped artifacts:" + ls -l new + + - name: 'Create GitHub release for new version' + run: | + set -euo pipefail + gh release create "dotcms-cli-${NEW_VERSION}" new/* \ + --repo "${{ github.repository }}" \ + --title "dotcms-cli - ${NEW_VERSION}" \ + --notes "Reused binaries from dotcms-cli-${PREV_VERSION} (CLI sources unchanged since the previous release)." + + - name: 'Upload artifacts to Artifactory' + env: + ARTIFACTORY_USER: ${{ secrets.EE_REPO_USERNAME }} + ARTIFACTORY_PASS: ${{ secrets.EE_REPO_PASSWORD }} + run: | + set -euo pipefail + BASE_URL="https://repo.dotcms.com/artifactory/libs-release-local/com/dotcms/dotcms-cli/${NEW_VERSION}" + for f in new/*; do + base=$(basename "$f") + echo "::notice::Uploading ${base} -> ${BASE_URL}/${base}" + curl -fsSL --user "${ARTIFACTORY_USER}:${ARTIFACTORY_PASS}" -T "$f" "${BASE_URL}/${base}" + done + # Publish NPM package + # Runs in both paths: the changed path consumes freshly built cli-artifacts-*, the unchanged + # path repackages the previously published binaries. Uses !failure() (not success()) so the + # intentionally-skipped build/build-cli/release jobs of the unchanged path don't block it, + # while still bailing if any job that DID run failed. publish-npm-package: name: "Publish NPM Package" - if: success() # Run only if explicitly indicated and successful + if: ${{ !cancelled() && !failure() && needs.precheck.result == 'success' }} needs: [ precheck, build, build-cli, release ] runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} steps: @@ -207,13 +347,15 @@ jobs: run: pip install jinja2-cli - name: 'Download all build artifacts' + if: needs.precheck.outputs.CLI_CHANGED == 'true' uses: actions/download-artifact@v4 with: path: ${{ github.workspace }}/artifacts pattern: cli-artifacts-* merge-multiple: true - # Determines the NPM package version and tag + # Determines the NPM package version and tag (runs in both paths - depends only on + # RELEASE_VERSION, not on build artifacts). # Distinguishes between snapshots and releases - name: 'Dynamic configuration of NPM package Version and Tag' env: @@ -302,6 +444,7 @@ jobs: # Adds the postinstall.js script # Generates the package.json file with Jinja2 - name: 'NPM Package setup' + if: needs.precheck.outputs.CLI_CHANGED == 'true' working-directory: ${{ github.workspace }}/tools/dotcms-cli/npm/ env: MVN_PACKAGE_VERSION: ${{ needs.precheck.outputs.RELEASE_VERSION }} @@ -326,9 +469,11 @@ jobs: echo "::endgroup::" - name: 'NPM Package tree' + if: needs.precheck.outputs.CLI_CHANGED == 'true' run: ls -R ${{ github.workspace }}/tools/dotcms-cli/npm/ - name: 'Publish to NPM registry' + if: needs.precheck.outputs.CLI_CHANGED == 'true' working-directory: ${{ github.workspace }}/tools/dotcms-cli/npm/ env: NPM_AUTH_TOKEN: ${{ secrets.NPM_ORG_TOKEN }} @@ -336,24 +481,37 @@ jobs: echo "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}" > ~/.npmrc npm publish --access public --tag ${NPM_PACKAGE_VERSION_TAG} - - name: Slack Notification - continue-on-error: true - uses: rtCamp/action-slack-notify@v2 + # Reuse path: CLI sources are unchanged since the previous release. Instead of the native + # rebuild, pull the currently published 'latest' tarball (which bundles the native binaries + # plus the postinstall wrapper), bump only its version field, and republish. The binaries + # stay byte-identical; only the version number advances to track the dotCMS release. + # We read the source version from npm ('latest') rather than reconstructing it from the git + # tag, sidestepping the leading-zero normalization applied to release versions above. + - name: 'Reuse previous CLI release and publish' + if: needs.precheck.outputs.CLI_CHANGED == 'false' + working-directory: ${{ runner.temp }} env: - SLACK_WEBHOOK: ${{ secrets.RELEASE_SLACK_WEBHOOK }} - SLACK_TITLE: "Important news!" - SLACK_MESSAGE: " This automated script is excited to announce the release of a new version of *dotCLI* `${{ needs.precheck.outputs.RELEASE_VERSION }}` :package: is available on the `NPM` registry!" - SLACK_USERNAME: dotBot - SLACK_MSG_AUTHOR: " " - MSG_MINIMAL: true - SLACK_FOOTER: "" - SLACK_ICON: https://avatars.slack-edge.com/temp/2021-12-08/2830145934625_e4e464d502865ff576e4.png + NPM_AUTH_TOKEN: ${{ secrets.NPM_ORG_TOKEN }} + run: | + set -euo pipefail + PACKAGE_FULL_NAME="${NPM_PACKAGE_SCOPE}/${NPM_PACKAGE_NAME}" + echo "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}" > ~/.npmrc + + PREV_NPM_VERSION=$(npm view "${PACKAGE_FULL_NAME}" version) + echo "::notice::CLI unchanged - repackaging ${PACKAGE_FULL_NAME}@${PREV_NPM_VERSION} as ${NPM_PACKAGE_VERSION} (tag: ${NPM_PACKAGE_VERSION_TAG})" + + npm pack "${PACKAGE_FULL_NAME}@${PREV_NPM_VERSION}" + tar -xzf ./*.tgz # extracts to ./package + cd package + # Bump only the version; binaries and scripts remain byte-identical to the prior release. + npm version "${NPM_PACKAGE_VERSION}" --no-git-tag-version --allow-same-version + npm publish --access public --tag "${NPM_PACKAGE_VERSION_TAG}" # Clean up temporary branches clean-up: name: "Clean Up" if: ${{ needs.precheck.outputs.AUXILIARY_BRANCH != '' }} - needs: [ precheck, build, build-cli, release, publish-npm-package ] + needs: [ precheck, build, build-cli, release, reuse-release, publish-npm-package ] runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} steps: - name: Checkout Repository From cc8eb135389de64a5766d26887e10c0320884621 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 16:31:35 +0000 Subject: [PATCH 2/8] fix(cli): select previous release tag by semver order instead of creation time Replaces --sort=-creatordate with --sort=-version:refname so PREV_TAG is determined by semantic version order rather than tag creation timestamp, which can be misleading when tags are backfilled or re-created. --- .github/workflows/cicd_release-cli.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cicd_release-cli.yml b/.github/workflows/cicd_release-cli.yml index 310a87052e53..ba8322cc3e3f 100644 --- a/.github/workflows/cicd_release-cli.yml +++ b/.github/workflows/cicd_release-cli.yml @@ -130,7 +130,7 @@ jobs: git fetch --tags --quiet || true # Previous non-LTS v* tag by creation order, excluding the current one. - PREV_TAG=$(git tag --sort=-creatordate --list 'v*' \ + PREV_TAG=$(git tag --sort=-version:refname --list 'v*' \ | grep -viE 'lts' \ | grep -vxF "${CURRENT_TAG}" \ | head -n1 || true) From 2a4b0e523ebb97f7cf03451ca8e43c9fa0680f14 Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Tue, 9 Jun 2026 13:04:28 -0400 Subject: [PATCH 3/8] ci: guard all CLI publish surfaces behind dry-run #36080 Extend the existing `dry-run` workflow_dispatch input to cover every publish/upload action so the unchanged-CLI reuse path can be rehearsed end-to-end with zero side effects: - reuse-release: skip `gh release create` and the Artifactory upload, logging what would happen instead. - publish-npm-package: pass `--dry-run` to `npm publish` on both the changed path and the reuse path. JReleaser already honored JRELEASER_DRY_RUN. Updated the input description to reflect that dry-run now skips npm, GitHub release, and Artifactory as well. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/cicd_release-cli.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cicd_release-cli.yml b/.github/workflows/cicd_release-cli.yml index ba8322cc3e3f..aed468677897 100644 --- a/.github/workflows/cicd_release-cli.yml +++ b/.github/workflows/cicd_release-cli.yml @@ -28,7 +28,7 @@ on: default: '1.0.0-SNAPSHOT' required: false dry-run: - description: 'Dry run' + description: 'Dry run - skip ALL publishing (npm, GitHub release, Artifactory) and JReleaser uploads' default: 'false' required: false @@ -268,6 +268,7 @@ jobs: NEW_VERSION: ${{ needs.precheck.outputs.RELEASE_VERSION }} PREV_VERSION: ${{ needs.precheck.outputs.PREV_RELEASE_VERSION }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRY_RUN: ${{ github.event.inputs.dry-run || 'false' }} steps: - name: 'Checkout' uses: actions/checkout@v4 @@ -302,6 +303,10 @@ jobs: - name: 'Create GitHub release for new version' run: | set -euo pipefail + if [ "${DRY_RUN}" = "true" ]; then + echo "::notice::[dry-run] would create release dotcms-cli-${NEW_VERSION} with assets: $(cd new && echo *)" + exit 0 + fi gh release create "dotcms-cli-${NEW_VERSION}" new/* \ --repo "${{ github.repository }}" \ --title "dotcms-cli - ${NEW_VERSION}" \ @@ -316,6 +321,10 @@ jobs: BASE_URL="https://repo.dotcms.com/artifactory/libs-release-local/com/dotcms/dotcms-cli/${NEW_VERSION}" for f in new/*; do base=$(basename "$f") + if [ "${DRY_RUN}" = "true" ]; then + echo "::notice::[dry-run] would upload ${base} -> ${BASE_URL}/${base}" + continue + fi echo "::notice::Uploading ${base} -> ${BASE_URL}/${base}" curl -fsSL --user "${ARTIFACTORY_USER}:${ARTIFACTORY_PASS}" -T "$f" "${BASE_URL}/${base}" done @@ -477,9 +486,11 @@ jobs: working-directory: ${{ github.workspace }}/tools/dotcms-cli/npm/ env: NPM_AUTH_TOKEN: ${{ secrets.NPM_ORG_TOKEN }} + DRY_RUN: ${{ github.event.inputs.dry-run || 'false' }} run: | echo "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}" > ~/.npmrc - npm publish --access public --tag ${NPM_PACKAGE_VERSION_TAG} + [ "${DRY_RUN}" = "true" ] && DRY_RUN_FLAG="--dry-run" || DRY_RUN_FLAG="" + npm publish --access public --tag ${NPM_PACKAGE_VERSION_TAG} ${DRY_RUN_FLAG} # Reuse path: CLI sources are unchanged since the previous release. Instead of the native # rebuild, pull the currently published 'latest' tarball (which bundles the native binaries @@ -492,6 +503,7 @@ jobs: working-directory: ${{ runner.temp }} env: NPM_AUTH_TOKEN: ${{ secrets.NPM_ORG_TOKEN }} + DRY_RUN: ${{ github.event.inputs.dry-run || 'false' }} run: | set -euo pipefail PACKAGE_FULL_NAME="${NPM_PACKAGE_SCOPE}/${NPM_PACKAGE_NAME}" @@ -505,7 +517,8 @@ jobs: cd package # Bump only the version; binaries and scripts remain byte-identical to the prior release. npm version "${NPM_PACKAGE_VERSION}" --no-git-tag-version --allow-same-version - npm publish --access public --tag "${NPM_PACKAGE_VERSION_TAG}" + [ "${DRY_RUN}" = "true" ] && DRY_RUN_FLAG="--dry-run" || DRY_RUN_FLAG="" + npm publish --access public --tag "${NPM_PACKAGE_VERSION_TAG}" ${DRY_RUN_FLAG} # Clean up temporary branches clean-up: From 1334fa396a3bcb3659a4db6ecddcd3980e1bfdca Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Tue, 9 Jun 2026 15:39:22 -0400 Subject: [PATCH 4/8] ci: run CLI tests only when the CLI or its REST endpoints change #36080 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI integration tests (tools/dotcms-cli, @QuarkusTest + testcontainers against a live dotCMS) were gated on `cli || backend`, so they ran on nearly every backend PR — the broad `backend` filter matches almost all of dotCMS/**. Add a `cli_test` filter = CLI sources (`cli`) + the server REST endpoints the CLI's @Path clients actually consume (assets, folder, site, contenttype, languages v2, workflow, authentication, user, analytics event) plus the asset-specific business APIs WebAssetHelper delegates to (fileassets, browser). Keep `cli` (= tools/dotcms-cli/**) untouched so endpoint-only changes don't re-trigger the GraalVM native build added in #36081. - PR + merge-queue now gate CLI tests on `cli_test` instead of `cli || backend`. - Register `cli_test` in the initialize-phase rewrite (build_test_filters + test_filters) so it defaults correctly under change-detection=disabled and honors CICD_SKIP_TESTS. - Safety net: trunk (which previously ran NO CLI tests) now runs them for any `backend` change, so a deeper business-layer regression that slips past the narrowed PR filter is still caught on main before a release. Nightly continues to run them unconditionally. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/filters.yaml | 29 +++++++++++++++++++ .github/workflows/cicd_1-pr.yml | 2 +- .github/workflows/cicd_2-merge-queue.yml | 2 +- .github/workflows/cicd_3-trunk.yml | 6 ++++ .../workflows/cicd_comp_initialize-phase.yml | 4 +-- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/.github/filters.yaml b/.github/filters.yaml index 621876a56197..06d3acef7174 100644 --- a/.github/filters.yaml +++ b/.github/filters.yaml @@ -34,6 +34,35 @@ frontend: &frontend cli: &cli - 'tools/dotcms-cli/**' +# Server-side REST endpoints the dotCMS CLI consumes at runtime. The CLI's +# integration tests (tools/dotcms-cli, @QuarkusTest + testcontainers) exercise a +# live dotCMS, so a change to one of these endpoints can break the CLI even when +# tools/dotcms-cli/** is untouched. +# +# Keep in sync with the @Path REST-client interfaces in +# tools/dotcms-cli/api-data-model/src/main/java/com/dotcms/api/*API.java +cli_endpoints: &cli_endpoints + - 'dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/**' # AssetAPI -> /v1/assets + - 'dotCMS/src/main/java/com/dotcms/rest/api/v1/folder/**' # FolderAPI -> /v1/folder + - 'dotCMS/src/main/java/com/dotcms/rest/api/v1/site/**' # SiteAPI -> /v1/site + - 'dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/**' # ContentTypeAPI -> /v1/contenttype + - 'dotCMS/src/main/java/com/dotcms/rest/api/v2/languages/**' # LanguageAPI -> /v2/languages + - 'dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/**' # WorkflowAPI -> /v1/workflow + - 'dotCMS/src/main/java/com/dotcms/rest/api/v1/authentication/**' # AuthenticationAPI -> /v1/authentication + - 'dotCMS/src/main/java/com/dotcms/rest/api/v1/user/**' # UserAPI -> /v1/users + - 'dotCMS/src/main/java/com/dotcms/rest/api/v1/analytics/event/**' # AnalyticsAPI -> /v1/analytics/content/event + # Asset push/pull is the CLI's heaviest workflow; WebAssetHelper (in the asset + # package above) delegates into these asset-specific business APIs. + - 'dotCMS/src/main/java/com/dotmarketing/portlets/fileassets/business/**' # FileAssetAPI + - 'dotCMS/src/main/java/com/dotcms/browser/**' # BrowserAPI + +# CLI test trigger: run the CLI integration tests when the CLI sources change OR +# when any server endpoint the CLI consumes changes. Distinct from `cli` (which +# gates the GraalVM native build) so endpoint-only changes don't force a rebuild. +cli_test: &cli_test + - *cli + - *cli_endpoints + sdk_libs: - 'core-web/libs/sdk/**' diff --git a/.github/workflows/cicd_1-pr.yml b/.github/workflows/cicd_1-pr.yml index ff6a18401831..5ca69e5f69b3 100644 --- a/.github/workflows/cicd_1-pr.yml +++ b/.github/workflows/cicd_1-pr.yml @@ -82,7 +82,7 @@ jobs: postman: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }} karate: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }} frontend: ${{ fromJSON(needs.initialize.outputs.filters).frontend == 'true' }} - cli: ${{ fromJSON(needs.initialize.outputs.filters).cli == 'true' || fromJSON(needs.initialize.outputs.filters).backend == 'true' }} + cli: ${{ fromJSON(needs.initialize.outputs.filters).cli_test == 'true' }} e2e: ${{ fromJSON(needs.initialize.outputs.filters).build == 'true' }} secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} diff --git a/.github/workflows/cicd_2-merge-queue.yml b/.github/workflows/cicd_2-merge-queue.yml index 0af9b08b50c1..fc374317ec05 100644 --- a/.github/workflows/cicd_2-merge-queue.yml +++ b/.github/workflows/cicd_2-merge-queue.yml @@ -31,7 +31,7 @@ jobs: postman: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }} karate: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }} frontend: ${{ fromJSON(needs.initialize.outputs.filters).frontend == 'true' }} - cli: ${{ fromJSON(needs.initialize.outputs.filters).cli == 'true' || fromJSON(needs.initialize.outputs.filters).backend == 'true' }} + cli: ${{ fromJSON(needs.initialize.outputs.filters).cli_test == 'true' }} e2e: false secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} diff --git a/.github/workflows/cicd_3-trunk.yml b/.github/workflows/cicd_3-trunk.yml index 43553ed15efe..d34e230eff93 100644 --- a/.github/workflows/cicd_3-trunk.yml +++ b/.github/workflows/cicd_3-trunk.yml @@ -95,6 +95,12 @@ jobs: java-version: ${{ github.event.inputs.java-version || '' }} maven-compiler-release: ${{ github.event.inputs.maven-compiler-release || '' }} artifact-suffix: ${{ github.event.inputs.artifact-suffix || '' }} + # Post-merge safety net for the CLI. The PR/merge-queue pipelines gate CLI + # tests on the narrow `cli_test` filter (CLI sources + the REST endpoints the + # CLI consumes), so a deeper business-layer change can slip through untested. + # On trunk we run the CLI tests for any backend change to catch that on main + # before it reaches a release. (Skipped on java-version overrides, like build-cli.) + cli: ${{ !github.event.inputs.java-version && fromJSON(needs.initialize.outputs.filters).backend == 'true' }} secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} permissions: diff --git a/.github/workflows/cicd_comp_initialize-phase.yml b/.github/workflows/cicd_comp_initialize-phase.yml index aae04e712f82..f0dd9d0949a6 100644 --- a/.github/workflows/cicd_comp_initialize-phase.yml +++ b/.github/workflows/cicd_comp_initialize-phase.yml @@ -163,9 +163,9 @@ jobs: # # test_filters: Subset of build_test_filters affected by CICD_SKIP_TESTS # ============================================================ - build_test_filters="frontend cli backend build jvm_unit_test" + build_test_filters="frontend cli cli_test backend build jvm_unit_test" info_filters="sdk_libs documentation cicd" - test_filters="frontend cli backend jvm_unit_test" + test_filters="frontend cli cli_test backend jvm_unit_test" declare -A results From a2a71f826ff4157726d14f240cfab733adf2205b Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Tue, 9 Jun 2026 15:45:49 -0400 Subject: [PATCH 5/8] ci: add TempFileAPI to cli_endpoints; gate LTS CLI tests on cli_test #36080 - TempFileAPI (rest/api/v1/temp) backs CLI file uploads on the asset push path via WebAssetHelper; same rationale as the fileassets/browser deps. - LTS workflow now gates CLI tests on `cli_test` (CLI sources + consumed endpoints) instead of bare `cli`, matching PR/merge-queue behavior so endpoint changes on an LTS branch still exercise the CLI suite. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/filters.yaml | 1 + .github/workflows/cicd_5-lts.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/filters.yaml b/.github/filters.yaml index 06d3acef7174..99016d348124 100644 --- a/.github/filters.yaml +++ b/.github/filters.yaml @@ -55,6 +55,7 @@ cli_endpoints: &cli_endpoints # package above) delegates into these asset-specific business APIs. - 'dotCMS/src/main/java/com/dotmarketing/portlets/fileassets/business/**' # FileAssetAPI - 'dotCMS/src/main/java/com/dotcms/browser/**' # BrowserAPI + - 'dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/**' # TempFileAPI (CLI file uploads on the push path) # CLI test trigger: run the CLI integration tests when the CLI sources change OR # when any server endpoint the CLI consumes changes. Distinct from `cli` (which diff --git a/.github/workflows/cicd_5-lts.yml b/.github/workflows/cicd_5-lts.yml index e592bcf95a93..252ba51def50 100644 --- a/.github/workflows/cicd_5-lts.yml +++ b/.github/workflows/cicd_5-lts.yml @@ -90,7 +90,7 @@ jobs: postman: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }} karate: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }} frontend: ${{ fromJSON(needs.initialize.outputs.filters).frontend == 'true' }} - cli: ${{ fromJSON(needs.initialize.outputs.filters).cli == 'true' }} + cli: ${{ fromJSON(needs.initialize.outputs.filters).cli_test == 'true' }} e2e: false secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} From 2a10433122b9a2e8b13e7355a6bdc300f5c6efd8 Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Tue, 9 Jun 2026 15:57:25 -0400 Subject: [PATCH 6/8] ci: fix publish-npm-package dependency + document CLI-test narrowing #36080 Address PR review findings: - publish-npm-package now depends on reuse-release. On the unchanged path (CLI_CHANGED=false) reuse-release publishes the GitHub release + Artifactory while publish-npm-package republishes to npm; previously they ran in parallel, so a reuse-release failure could leave npm published with no GitHub release. Adding it to `needs` makes !failure() block npm publish if reuse-release fails; on the changed path reuse-release is skipped (not failed) so the job still runs. - Document, at the PR and merge-queue CLI-test gates, why the trigger is narrow (cli_test) and that the trunk CLI run is the compensating control for deep backend changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/cicd_1-pr.yml | 4 ++++ .github/workflows/cicd_2-merge-queue.yml | 4 ++++ .github/workflows/cicd_release-cli.yml | 6 +++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cicd_1-pr.yml b/.github/workflows/cicd_1-pr.yml index 5ca69e5f69b3..0ad7d730bedb 100644 --- a/.github/workflows/cicd_1-pr.yml +++ b/.github/workflows/cicd_1-pr.yml @@ -82,6 +82,10 @@ jobs: postman: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }} karate: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }} frontend: ${{ fromJSON(needs.initialize.outputs.filters).frontend == 'true' }} + # Run CLI tests only when the CLI sources or a REST endpoint the CLI consumes + # change (cli_test filter). A deep backend/service-layer change that alters CLI + # behavior without touching a watched endpoint is covered by the trunk CLI run + # (cicd_3-trunk.yml gates CLI tests on the broad `backend` filter post-merge). cli: ${{ fromJSON(needs.initialize.outputs.filters).cli_test == 'true' }} e2e: ${{ fromJSON(needs.initialize.outputs.filters).build == 'true' }} secrets: diff --git a/.github/workflows/cicd_2-merge-queue.yml b/.github/workflows/cicd_2-merge-queue.yml index fc374317ec05..1a60f7a3bd16 100644 --- a/.github/workflows/cicd_2-merge-queue.yml +++ b/.github/workflows/cicd_2-merge-queue.yml @@ -31,6 +31,10 @@ jobs: postman: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }} karate: ${{ fromJSON(needs.initialize.outputs.filters).backend == 'true' }} frontend: ${{ fromJSON(needs.initialize.outputs.filters).frontend == 'true' }} + # Run CLI tests only when the CLI sources or a REST endpoint the CLI consumes + # change (cli_test filter). A deep backend/service-layer change that alters CLI + # behavior without touching a watched endpoint is covered by the trunk CLI run + # (cicd_3-trunk.yml gates CLI tests on the broad `backend` filter post-merge). cli: ${{ fromJSON(needs.initialize.outputs.filters).cli_test == 'true' }} e2e: false secrets: diff --git a/.github/workflows/cicd_release-cli.yml b/.github/workflows/cicd_release-cli.yml index aed468677897..18e17cf88b68 100644 --- a/.github/workflows/cicd_release-cli.yml +++ b/.github/workflows/cicd_release-cli.yml @@ -337,7 +337,11 @@ jobs: publish-npm-package: name: "Publish NPM Package" if: ${{ !cancelled() && !failure() && needs.precheck.result == 'success' }} - needs: [ precheck, build, build-cli, release ] + # reuse-release is included so that on the unchanged path (CLI_CHANGED=false), + # a failed GitHub-release/Artifactory reuse blocks the npm publish (via !failure()), + # avoiding an npm-published-but-no-release inconsistent state. On the changed path + # reuse-release is skipped (not failed), so this job still runs. + needs: [ precheck, build, build-cli, release, reuse-release ] runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} steps: - name: 'Checkout code' From 5daf07af6be514888140ee23103b5d115c8856e0 Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Tue, 9 Jun 2026 16:07:23 -0400 Subject: [PATCH 7/8] ci: clean up auxiliary release branch even on failure #36080 The clean-up job had no always() guard, so GitHub Actions skipped it whenever any upstream job failed, leaking the temporary version-update-- branch. Guard with always() (AUXILIARY_BRANCH is only set when precheck succeeded, so it still no-ops when there's nothing to clean up). Addresses PR review follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/cicd_release-cli.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cicd_release-cli.yml b/.github/workflows/cicd_release-cli.yml index 18e17cf88b68..f65c585b2ee4 100644 --- a/.github/workflows/cicd_release-cli.yml +++ b/.github/workflows/cicd_release-cli.yml @@ -527,7 +527,10 @@ jobs: # Clean up temporary branches clean-up: name: "Clean Up" - if: ${{ needs.precheck.outputs.AUXILIARY_BRANCH != '' }} + # always(): the temporary version-update branch must be deleted even when an + # upstream job fails (otherwise it leaks). AUXILIARY_BRANCH is only set when + # precheck succeeded, so this still no-ops when there is nothing to clean up. + if: ${{ always() && needs.precheck.outputs.AUXILIARY_BRANCH != '' }} needs: [ precheck, build, build-cli, release, reuse-release, publish-npm-package ] runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} steps: From 65b027af10965516c41c4c14138815f0bc7ea7c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 21:16:35 +0000 Subject: [PATCH 8/8] ci(cli): make publish-npm-package gate explicit with per-job result checks Replace the implicit !failure() gate with explicit (needs.reuse-release.result == 'success' || 'skipped') && (needs.release.result == 'success' || 'skipped') checks so the mutual-exclusion of the changed/unchanged paths is self-documenting and a failure in either upstream job reliably blocks the npm publish. https://claude.ai/code/session_018KKHBasNtegpRZy5G6cCD1 --- .github/workflows/cicd_release-cli.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cicd_release-cli.yml b/.github/workflows/cicd_release-cli.yml index f65c585b2ee4..9667faf2f366 100644 --- a/.github/workflows/cicd_release-cli.yml +++ b/.github/workflows/cicd_release-cli.yml @@ -336,11 +336,17 @@ jobs: # while still bailing if any job that DID run failed. publish-npm-package: name: "Publish NPM Package" - if: ${{ !cancelled() && !failure() && needs.precheck.result == 'success' }} - # reuse-release is included so that on the unchanged path (CLI_CHANGED=false), - # a failed GitHub-release/Artifactory reuse blocks the npm publish (via !failure()), - # avoiding an npm-published-but-no-release inconsistent state. On the changed path - # reuse-release is skipped (not failed), so this job still runs. + if: >- + ${{ + !cancelled() && + needs.precheck.result == 'success' && + (needs.reuse-release.result == 'success' || needs.reuse-release.result == 'skipped') && + (needs.release.result == 'success' || needs.release.result == 'skipped') + }} + # On the changed path (CLI_CHANGED=true): release runs and reuse-release is skipped. + # On the unchanged path (CLI_CHANGED=false): reuse-release runs and release is skipped. + # Requiring each to be 'success' or 'skipped' makes the mutual-exclusion explicit and + # ensures a failed release or reuse-release blocks the npm publish. needs: [ precheck, build, build-cli, release, reuse-release ] runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} steps: