Skip to content

feat(ci): manual changelog site publish + fix release-notes auto-detect - #37137

Closed
sfreudenthaler wants to merge 2 commits into
mainfrom
issue-37136-manual-changelog-publish
Closed

feat(ci): manual changelog site publish + fix release-notes auto-detect#37137
sfreudenthaler wants to merge 2 commits into
mainfrom
issue-37136-manual-changelog-publish

Conversation

@sfreudenthaler

@sfreudenthaler sfreudenthaler commented Aug 20, 2026

Copy link
Copy Markdown
Member

Closes: #37136
Closes: #37138

Two related changes: a manual publish path, and the auto-detect bug that made one necessary.

1. Manual trigger for changelog site publish (#37136)

Adds cicd_manual_changelog-site-publish.yml, a workflow_dispatch wrapper around the existing phase — the pattern cicd_ai-release-notes-backfill.yml already uses.

gh workflow run cicd_manual_changelog-site-publish.yml --repo dotCMS/core --ref main \
  -f release_tag=v26.08.19-04

Why: the phase is workflow_call: only and gated on success(), so any upstream failure or cancellation skips it with no CI path to recover. Today that means a local checkout plus three values that otherwise never leave GitHub — DOTCMS_DEVSITE_URL, DOTCMS_DEVSITE_RELEASENOTES_TOKEN, DOTCMS_DEVSITE_RELEASENOTES_ACCOUNT. The phase's comments also describe --force as "a manual operator re-run only", but no manual re-run path existed, so the flag was unreachable.

The wrapper derives what the pipeline would have passed:

Input Derived from
release_version the tag, minus the v
docker_tags Docker Hub public API, preferring the version_sha tag so a manual entry matches a pipeline-written one
released_date the release's own publishedAt, so backfilling an older release does not stamp today's date

The phase gains force and released_date, both defaulting to current behavior. The release pipeline path is unchangedcicd_6-release.yml sets neither input.

Access control

workflow_dispatch is limited to accounts with write access — never forks, never the public — but that is 45 accounts today, and this writes to the public docs site with a service-account token. So the publish job sits behind a no-op approval gate on the changelog-site-publish environment (required_reviewers: dotDevelopers), matching cicd_evergreen-tracks-promote.yml's apply gate.

The environment has been created with that protection rule. It must keep it: GitHub auto-creates a missing environment unprotected on first use, which would silently remove the gate.

2. Auto-detect picks an undocumented tag (#37138)

findPreviousTag() returned tags[idx + 1] unconditionally. On a multi-attempt day that is another attempt at the same release, whose pipeline died before writing notes — so the changelog covers one attempt instead of the release.

This is what produced the 26.08.19-04 changelog: auto-detect resolved its predecessor to v26.08.19-03, and the release notes described 1 of 19 commits, publishing "contains internal maintenance only" for a release carrying Accessibility Studio, the Experiments portlet, three roles endpoints and nine fixes.

Three fixes in the same two functions:

  • listStandardReleaseTags() returns { tag, hasNotes } and skips drafts. listReleases returns drafts first regardless of date, corrupting the newest-first ordering the docstring promises — 6 drafts currently match the standard pattern, and one duplicates v26.04.11-02.
  • findPreviousTag() walks back to the first release with notes, so an undocumented attempt's commits stay inside the range instead of being stranded behind its tag.
  • findIndex on tag equality replaces indexOf, which returned the first match for a duplicated tag.

A same-day attempt that did publish notes is still a valid boundary — the skip is conditioned on missing notes, not on the date, and there is a test for exactly that (26.08.12-02 → 26.08.12-01).

Verification

  • npx tsc --noEmit clean; npx jest 35/35 passing, including three new findPreviousTag cases: skips undocumented predecessors, keeps a documented same-day attempt, returns undefined when every predecessor is undocumented.
  • Both workflows parse as valid YAML.
  • The resolve logic was run against a real tag and reproduces the manual publish exactly: docker tag → 26.08.19-04_a0181f9, released date → 2026-08-20, both matching the entry now live on the site.
  • Draft counts above were measured against the live API, not assumed.

Not exercised end-to-end against the live site: the only release currently needing a publish was already published by hand, and re-running would trip human-edit protection. A run on the next release that needs it is the real test.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EbgDBJuoBrpJxh5qLMPorL

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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbgDBJuoBrpJxh5qLMPorL
@claude

claude Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Claude finished @sfreudenthaler's task in 3m 2s —— View job


Code Review — feat(ci): manual trigger for changelog site publish

Reviewed the full diff (workflow wrapper, phase changes, and the gather-release-data TS changes).

New Issues

  • 🟡 Medium: .github/scripts/gather-release-data/src/index.ts:96 — The error emitted when findPreviousTag returns undefined now reads "{toTag} is the earliest standard release — no previous tag to compare against." After this PR, findPreviousTag also returns undefined when predecessors exist but are all undocumented (hasNotes: false) — the exact multi-attempt-day scenario the PR targets. In that case the message is misleading (there are earlier tags, they just lack notes) and the suggested remedy is wrong. Consider distinguishing "no predecessor at all" from "no documented predecessor". Fix this →

  • 🟡 Medium: .github/workflows/cicd_manual_changelog-site-publish.yml:79 (gate job) — The only access control on this write-to-prod workflow is reviewer approval via the changelog-site-publish environment. As the in-file comment itself notes, GitHub auto-creates the environment UNPROTECTED on first use if it doesn't already exist, which silently removes the gate.

    • Assumption: the changelog-site-publish environment does not yet exist / may not carry required_reviewers.
    • What to verify: confirm the environment exists in repo settings with required_reviewers configured before first run; otherwise the first dispatch runs ungated with the service-account token. This is operational rather than a code defect, but it's the load-bearing control for a public-site write path.

Notes (non-blocking)

  • The PR description lists the findPreviousTag() auto-detect fix as out of scope / filed separately, but the diff actually implements it (ReleaseRef.hasNotes filtering in github.ts:75, plus regression tests in github.test.ts). Not a defect — just a description/scope mismatch worth reconciling so the linked follow-up issue isn't left dangling.
  • cicd_manual_changelog-site-publish.yml:70 — the Docker-tag fallback (${TAG:-$VERSION}) only covers the case where jq finds no matching tag. Because the step runs under set -euo pipefail, a curl failure (network / Hub 5xx) aborts the step before the fallback, so the sha-less path is only reachable on empty-but-successful responses. That's a reasonable fail-loud choice; flagging only so it's intentional.

Verified clean

  • Release-pipeline path unchanged: cicd_6-release.yml sets neither force nor released_date; defaults (false / '') reproduce the prior command exactly — FORCE_FLAG stays empty and --released-date falls back to $(date -u +%F).
  • --apply $FORCE_FLAG is intentionally unquoted so an empty value passes no arg (quoting would inject an empty positional). Correct.
  • [ "$FORCE" = "true" ] && FORCE_FLAG="--force" is safe under set -e (test is not the final command of the && list, so a false test doesn't abort the step).
  • ✅ Concurrency: the reusable phase's own changelog-site-publish-${release_version} group serializes a manual run against a pipeline run, closing the double-create window.
  • ✅ Token/secret handling: DOTCMS_DEVSITE_RELEASENOTES_TOKEN stays in secrets and is not echoed; no hardcoded credentials introduced.
  • ✅ Test coverage: findPreviousTag behavior change is covered by three new regression cases (skips ghosts, keeps documented same-day attempt, all-undocumented → undefined).

No blocking issues. The two 🟡 items are worth addressing but don't block merge.

· branch claude/pr-37137-20260820-1553

…tag (#37138)

findPreviousTag returned tags[idx + 1] unconditionally. On a day with more than
one release attempt that preceding tag is another attempt at the same release,
whose pipeline died before writing notes -- so the generated changelog covered
one attempt instead of the whole release. On 26.08.19 that published "internal
maintenance only" for a release carrying 19 commits.

Three fixes in the same two functions:

- listStandardReleaseTags now returns {tag, hasNotes} and skips drafts.
  listReleases returns drafts first regardless of date, corrupting the
  newest-first ordering the docstring promises; 6 drafts currently match the
  standard pattern and one duplicates v26.04.11-02.
- findPreviousTag walks back to the first release with notes, so an
  undocumented attempt's commits stay inside the range instead of being
  stranded behind its tag.
- findIndex on tag equality replaces indexOf, which returned the first match
  for a duplicated tag.

A same-day attempt that DID publish notes is still a valid boundary -- the skip
is conditioned on missing notes, not on the date.

Also gates the manual publish workflow behind the changelog-site-publish
environment (required_reviewers: dotDevelopers), matching the evergreen
promote apply gate. workflow_dispatch is open to all 45 accounts with write
access, and this writes to the public docs site with a service-account token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbgDBJuoBrpJxh5qLMPorL
@sfreudenthaler sfreudenthaler changed the title feat(ci): add a manual trigger for changelog site publish feat(ci): manual changelog site publish + fix release-notes auto-detect Aug 20, 2026
@sfreudenthaler

Copy link
Copy Markdown
Member Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : CI/CD PR changes GitHub Actions/workflows

Projects

Status: Done

1 participant