From 48d609996d9d8643b1959f99db60aa08d3dbe47e Mon Sep 17 00:00:00 2001 From: Stephen Freudenthaler Date: Thu, 20 Aug 2026 11:44:26 -0400 Subject: [PATCH 1/2] feat(ci): add a manual trigger for changelog site publish The changelog site publish phase is workflow_call: only, so when the release pipeline skips it -- any upstream failure or cancellation, since the job is gated on success() -- the only recovery is running the publisher locally with DOTCMS_DEVSITE_URL, _RELEASENOTES_TOKEN and _RELEASENOTES_ACCOUNT in hand. Adds cicd_manual_changelog-site-publish.yml, a workflow_dispatch wrapper around the existing phase, mirroring cicd_ai-release-notes-backfill.yml. Credentials stay in repo vars/secrets; an operator supplies only the release tag. The wrapper derives release_version from the tag, the sha-tagged docker image from Docker Hub's public API, and released_date from the release's own publishedAt so backfilling an older release does not stamp today's date. The phase gains force and released_date inputs, both defaulting to current behavior, so the release pipeline path is unchanged. force makes the --force override the phase comments already describe reachable from CI. The publish job sits behind a no-op approval gate on the changelog-site-publish environment (required_reviewers: dotDevelopers): this writes to the public docs site with a service-account token, and workflow_dispatch is open to every account with write access on this repo. Closes: #37136 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EbgDBJuoBrpJxh5qLMPorL --- ...cicd_comp_changelog-site-publish-phase.yml | 24 +++- .../cicd_manual_changelog-site-publish.yml | 104 ++++++++++++++++++ 2 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/cicd_manual_changelog-site-publish.yml diff --git a/.github/workflows/cicd_comp_changelog-site-publish-phase.yml b/.github/workflows/cicd_comp_changelog-site-publish-phase.yml index f76193f5d933..c2b72f00a566 100644 --- a/.github/workflows/cicd_comp_changelog-site-publish-phase.yml +++ b/.github/workflows/cicd_comp_changelog-site-publish-phase.yml @@ -33,6 +33,16 @@ on: description: 'Space-separated docker image tags produced by the deployment phase' required: true type: string + force: + description: 'Override human-edit protection and update in place. Manual operator use only — the release pipeline never sets this.' + required: false + type: boolean + default: false + released_date: + description: 'Availability date (yyyy-MM-dd). Defaults to today, which is correct for a live release; a manual backfill of an older release should pass that release''s own date.' + required: false + type: string + default: '' allow_failure: description: 'If true, job failures are non-blocking (used by release pipeline). Defaults to false.' required: false @@ -109,10 +119,12 @@ jobs: DOTCMS_DEVSITE_URL: ${{ vars.DOTCMS_DEVSITE_URL }} # Read once by the tool; never echoed to logs / GITHUB_OUTPUT / step summary. DOTCMS_DEVSITE_RELEASENOTES_TOKEN: ${{ secrets.DOTCMS_DEVSITE_RELEASENOTES_TOKEN }} - # Service-account modUser id for human-edit protection (FR-011). --force is never - # passed here — override is a manual operator re-run only. + # Service-account modUser id for human-edit protection (FR-011). The release + # pipeline never sets force; only a manual operator re-run does. DOTCMS_DEVSITE_RELEASENOTES_ACCOUNT: ${{ vars.DOTCMS_DEVSITE_RELEASENOTES_ACCOUNT }} DOCKER_TAGS: ${{ inputs.docker_tags }} + FORCE: ${{ inputs.force }} + RELEASED_DATE: ${{ inputs.released_date }} run: | # Pick the sha-tagged dotcms/dotcms image (version_sha form, e.g. # dotcms/dotcms:26.07.10-01_84b0486). The `_` in the glob excludes the floating @@ -130,13 +142,17 @@ jobs: # T005 contract (0 = success/skip via ::changelog-skip:: marker; non-zero = failure). # This step itself always exits 0 so the branch steps run and the release is never # blocked (FR-008); the job is also allow_failure as belt-and-suspenders. + # ponytail: empty force/released_date are the pipeline's path — same command as before. + FORCE_FLAG="" + [ "$FORCE" = "true" ] && FORCE_FLAG="--force" + set +e OUTPUT=$(uv run changelog-publisher publish \ --version "$RELEASE_VERSION" \ --notes-file /tmp/site-release-notes.md \ --docker-image "$DOCKER_IMAGE" \ - --released-date "$(date -u +%F)" \ - --apply 2>&1) + --released-date "${RELEASED_DATE:-$(date -u +%F)}" \ + --apply $FORCE_FLAG 2>&1) RC=$? set -e echo "$OUTPUT" diff --git a/.github/workflows/cicd_manual_changelog-site-publish.yml b/.github/workflows/cicd_manual_changelog-site-publish.yml new file mode 100644 index 000000000000..9f89feb6cbc7 --- /dev/null +++ b/.github/workflows/cicd_manual_changelog-site-publish.yml @@ -0,0 +1,104 @@ +# +# Changelog Site Publish — Manual Trigger +# +# Standalone workflow for (re-)publishing a release's changelog to dev.dotcms.com. +# Calls the same reusable component used by the release pipeline, so the credentials +# (DOTCMS_DEVSITE_URL / _RELEASENOTES_TOKEN / _RELEASENOTES_ACCOUNT) stay in repo +# vars+secrets — an operator needs only the release tag. +# +# Use cases: +# - A release whose site publish never ran (deployment failed, so the phase was skipped) +# - Re-syncing the site after hand-editing a GitHub release body +# - Overriding human-edit protection (`force`) after a skip names a human editor +# +# Prerequisite: the GitHub release body must already hold the changelog. Generate it +# first with cicd_ai-release-notes-backfill.yml — this workflow only copies, never writes. +# + +name: 'Changelog Site Publish (Manual)' +run-name: "Changelog Site: ${{ inputs.release_tag }}" + +on: + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag to publish (e.g., v26.08.19-04)' + required: true + type: string + force: + description: 'Override human-edit protection — only after a dry run names a human editor' + required: false + type: boolean + default: false + +concurrency: + group: changelog-site-publish-${{ inputs.release_tag }} + cancel-in-progress: false + +jobs: + # The release pipeline gets release_version and docker_tags from upstream jobs. A manual + # run has neither, so derive both from the tag rather than making an operator hunt for a sha. + resolve: + name: Resolve Version and Image + runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} + outputs: + release_version: ${{ steps.r.outputs.release_version }} + docker_tags: ${{ steps.r.outputs.docker_tags }} + released_date: ${{ steps.r.outputs.released_date }} + steps: + - name: Resolve + id: r + env: + RELEASE_TAG: ${{ inputs.release_tag }} + GH_TOKEN: ${{ secrets.CI_MACHINE_TOKEN || github.token }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + VERSION="${RELEASE_TAG#v}" + echo "release_version=${VERSION}" >> "$GITHUB_OUTPUT" + + # Backfilling an older release must stamp that release's date, not today's. + PUBLISHED=$(gh release view "$RELEASE_TAG" --repo "$REPO" --json publishedAt --jq '.publishedAt[0:10]') + echo "released_date=${PUBLISHED}" >> "$GITHUB_OUTPUT" + + # Prefer the sha-tagged image (e.g. 26.08.19-04_a0181f9) so a manual entry matches + # what the pipeline would have written. Hub is public; no auth needed. + TAG=$(curl -fsSL \ + "https://hub.docker.com/v2/repositories/dotcms/dotcms/tags/?name=${VERSION}_&page_size=100" \ + | jq -r --arg v "$VERSION" \ + '[.results[].name | select(test("^" + $v + "_[0-9a-f]+$"))] | first // empty') + # ponytail: sha-less fallback keeps a publish possible if Hub paging ever changes shape. + echo "docker_tags=dotcms/dotcms:${TAG:-$VERSION}" >> "$GITHUB_OUTPUT" + echo "Resolved ${VERSION} -> dotcms/dotcms:${TAG:-$VERSION}" + + # This writes to the public docs site with a service-account token, and workflow_dispatch + # is open to every account with write access on this repo (45 today). Reviewer approval + # is the access control — same pattern as cicd_evergreen-tracks-promote.yml's apply gate. + # The environment must carry required_reviewers; GitHub auto-creates it UNPROTECTED on + # first use if it is missing, which would silently remove this gate. + gate: + name: Approve Site Publish + needs: resolve + runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} + environment: changelog-site-publish + steps: + - run: | + echo "Approved: publish ${{ inputs.release_tag }} to the changelog site" + echo "Image: ${{ needs.resolve.outputs.docker_tags }}" + echo "Date: ${{ needs.resolve.outputs.released_date }}" + echo "Force: ${{ inputs.force }}" + + publish: + name: Publish Site Changelog + needs: [resolve, gate] + uses: ./.github/workflows/cicd_comp_changelog-site-publish-phase.yml + with: + release_tag: ${{ inputs.release_tag }} + release_version: ${{ needs.resolve.outputs.release_version }} + docker_tags: ${{ needs.resolve.outputs.docker_tags }} + released_date: ${{ needs.resolve.outputs.released_date }} + force: ${{ inputs.force }} + secrets: + DOTCMS_DEVSITE_RELEASENOTES_TOKEN: ${{ secrets.DOTCMS_DEVSITE_RELEASENOTES_TOKEN }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + CI_MACHINE_TOKEN: ${{ secrets.CI_MACHINE_TOKEN }} From 1de1c294cd78d6809aa0cd074522932337540c64 Mon Sep 17 00:00:00 2001 From: Stephen Freudenthaler Date: Thu, 20 Aug 2026 14:23:43 -0400 Subject: [PATCH 2/2] fix(ci): reject a release_tag without the v prefix The phase gates its publish job on startsWith(release_tag, 'v'). A tag typed without the prefix resolved fine, passed the approval gate, and was then silently skipped -- the run reported success while nothing was published. Fails fast in resolve instead, before anyone is asked to approve. Found in review of #37141. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EbgDBJuoBrpJxh5qLMPorL --- .github/workflows/cicd_manual_changelog-site-publish.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/cicd_manual_changelog-site-publish.yml b/.github/workflows/cicd_manual_changelog-site-publish.yml index 9f89feb6cbc7..c035c733f025 100644 --- a/.github/workflows/cicd_manual_changelog-site-publish.yml +++ b/.github/workflows/cicd_manual_changelog-site-publish.yml @@ -54,6 +54,14 @@ jobs: REPO: ${{ github.repository }} run: | set -euo pipefail + # The phase gates its publish job on startsWith(release_tag, 'v'). Without this + # check a tag typed without the prefix resolves fine, gets approved at the gate, + # and is then silently skipped -- a green run that published nothing. + case "$RELEASE_TAG" in + v*) ;; + *) echo "Error: release_tag must start with 'v' (got '$RELEASE_TAG')"; exit 1;; + esac + VERSION="${RELEASE_TAG#v}" echo "release_version=${VERSION}" >> "$GITHUB_OUTPUT"