Skip to content

fix(sdk): enforce single source of truth for SDK package versioning#35110

Merged
fmontes merged 11 commits intomainfrom
35109-sdk-single-version-source
Apr 17, 2026
Merged

fix(sdk): enforce single source of truth for SDK package versioning#35110
fmontes merged 11 commits intomainfrom
35109-sdk-single-version-source

Conversation

@fmontes
Copy link
Copy Markdown
Member

@fmontes fmontes commented Mar 24, 2026

What

Replaces the SDK publish action's version detection logic with a single `VERSION` file at `core-web/libs/sdk/VERSION`.

Why

The old system scanned NPM dist-tags across all packages at publish time and picked the highest version globally. This broke in two ways:

  1. Package drift — `@dotcms/react` had `next=1.2.6-next.3` while all others were at `next=1.2.5-next.7`. The base version mismatch caused the suffix counter to reset to `.1`, colliding with an already-published version → 403 on every trunk run.
  2. New packages — adding `create-app` with `version=1.0.0` in its `package.json` triggered the same collision because it had never been published and the action couldn't reconcile its version with the rest.

How it works

Publishing @next (automatic)

Every merge to main triggers a trunk run that publishes all SDK packages to the `@next` NPM tag. The version is `{VERSION}-next.{run_number}` (e.g. `1.2.6-next.42`). No manual steps, no branch creation needed.

Publishing @latest (manual release)

Run the Manual SDK Release workflow from GitHub Actions UI:

  1. Choose a version increment: `patch`, `minor`, `major`, or `custom`.
  2. The workflow automatically:
    • Creates a release branch `sdk/release-X.Y.Z` from main
    • Bumps `VERSION` to the new version on that branch
    • Publishes all SDK packages to `@latest` on NPM
    • Opens a PR to main bumping `VERSION` to the next patch (e.g. `1.2.7`) so future `@next` builds stay semver-ahead of the just-released `@latest`

You do not create the branch manually — the workflow handles it. You only trigger the workflow.

Changes

  • `core-web/libs/sdk/VERSION` — new file, set to `1.2.6` (current highest published stable). Single source of truth for the base version.
  • `deploy-javascript-sdk/action.yml` — pure publisher. Reads `VERSION` from the checked-out ref, publishes `@next` always and `@latest` when `publish-latest: true`. No version increment logic, no git writes.
  • `cicd_manual-release-sdks.yml` — orchestrates the full `@latest` release: creates release branch, bumps `VERSION`, publishes, opens post-release PR to main.

Result

  • No direct pushes to main — merge queue is never disrupted.
  • `@next` is always semver-ahead of `@latest` after each release.
  • Adding a new SDK package works automatically — no manual version syncing.
  • Version history is visible in git on release branches.

Closes #35109

This PR fixes: #35109

…35109)

- Add core-web/libs/sdk/VERSION as the single version source
- Replace ~150 lines of fragile NPM-scanning version detection with a
  simple read from VERSION file
- Use GITHUB_RUN_NUMBER as next tag suffix — guaranteed unique, no collision
- Manual release workflow now bumps VERSION file and commits it to git
- New packages added to libs/sdk/ work automatically without manual sync

Fixes recurring 403 publish failures caused by packages drifting out of
version sync (e.g. react at 1.2.6-next.3 while others at 1.2.5-next.7).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fmontes fmontes requested a review from a team as a code owner March 24, 2026 20:32
@github-actions github-actions bot added Area : CI/CD PR changes GitHub Actions/workflows Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries labels Mar 24, 2026
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 24, 2026

Claude finished @fmontes's task in 3m 43s —— View job


PR Review

  • Read changed files
  • Analyze action.yml logic
  • Analyze manual release workflow
  • Post findings

Bug: @next is always published — run_number collision survives

action.yml:191-198 publishes @next unconditionally, regardless of publish-latest. The manual release workflow invokes the action with publish-latest: 'true', so it publishes both @latest and @next. fmontes' response to the Copilot comment ("only trunk publishes @next, so no collision possible") is incorrect — it is based on a claim the code does not support.

The collision window: after a release is cut but before the post-release PR to main is merged, VERSION on both main and the release branch is the same (e.g. 1.2.6). If the trunk run number and the manual-release run number are the same, both workflows produce 1.2.6-next.{N} → 403 on NPM. run_number is unique per workflow, not per repo. This was correctly identified, fixed in fcbbd58, then the fix was reversed in the refactor without the reasoning being updated.

Fix: use ${{ github.run_id }}-${{ github.run_attempt }} as the suffix instead of run_number. run_id is globally unique across all workflows in the repo. Fix this →


Bug: published-latest output is true even when @latest was never attempted

action.yml:172 initializes PUBLISH_LATEST_SUCCESS=true. When publish-latest=false (every trunk run), the flag is never set to false, so actual_published_latest is written as true at line 205, and published-latest at line 229 reflects it. Any caller checking published-latest to confirm a stable release was shipped will get a false positive on every trunk run. No current caller breaks (deployment phase only reads published), but the output contract is wrong.

Fix: initialize PUBLISH_LATEST_SUCCESS=false and set it true only on a successful @latest publish (currently inside the if [ "$PUBLISH_LATEST" = "true" ] block at line 181). Fix this →


Outputs lost when any publish fails

action.yml:219 has if: success(). If any @next publish fails (the script calls exit 1 at line 213), the deployment_status step never runs, and published-latest, published-next, and npm-package-version are never written. Downstream steps or callers get empty strings. The Display results step defensively adds || 'unknown', but the composite action's published outputs — which callers actually use — are silently empty. The fix is to remove if: success() and let the step always run, checking steps.publish_packages.outcome instead. Fix this →


Minor: validate inputs step missing set -euo pipefail

cicd_manual-release-sdks.yml:39 — the Validate inputs step is the only shell step in either file without set -euo pipefail. Inconsistent with all other steps. Low impact since exit 1 still works, but the pattern is inconsistent.


Design note: @next is published with the release version during manual releases

When the manual release runs (e.g., releasing 1.2.6), the action publishes @next as 1.2.6-next.{N} from the release branch. Semver-wise, 1.2.6-next.N < 1.2.6, so @next momentarily points to a version lower than @latest. This is transient — the next trunk merge (after the post-release PR is merged and VERSION becomes 1.2.7) will push @next to 1.2.7-next.M, which is ahead. But there's a window where @next is semantically lower than @latest. Not a regression given the old system, but worth knowing.


The architecture (release branch pattern, merge queue preservation, post-release PR) is solid. The three bugs above are the ones worth fixing before merge.

@fmontes fmontes marked this pull request as draft March 24, 2026 20:36
@spbolton
Copy link
Copy Markdown
Contributor

Architectural concern: VERSION file on main vs release branch pattern

The dotCMS release model keeps main versionless and deterministic:

  • Main/trunk always builds as 1.0.0-SNAPSHOT.mvn/maven.config with a version is actively prevented on main
  • Build output is determined by commit hash — same hash, same output
  • Release branches commit the version to .mvn/maven.config on a tagged release branch
  • The merge queue is the single gateway for changes to main — out-of-band pushes to main force all running merge queue builds to restart, wasting up to 30-90 minutes of compute per interruption

This PR introduces core-web/libs/sdk/VERSION on main. A few concerns:

  1. Determinism — a VERSION bump commit changes no code but alters build output. Two commits with identical code but different VERSION values produce different artifacts, diverging from the commit-hash-determines-output invariant.

  2. Merge queue disruption — the manual release workflow pushes directly to main, bypassing the merge queue. Any push to main outside the merge queue invalidates and restarts all in-flight merge queue builds. Maintaining the merge queue as the only source of changes to main avoids this disruption.

  3. next tag semver ordering — after publishing 1.2.6 as stable, trunk runs publish 1.2.6-next.X. In semver, prereleases are lower than their release (1.2.6-next.X < 1.2.6), so the @next tag would point to versions semantically older than @latest. VERSION would need to be bumped to 1.2.7 immediately after a release to keep next ahead of latest — adding another push to main.

Alternative (aligned with existing model):

  • Trunk runs: publish next tag with ${GITHUB_RUN_NUMBER} suffix (as this PR does — good). Base version derived from the current NPM @latest tag or a fixed value.
  • Manual SDK releases: create a release branch, commit the VERSION file there, publish from that branch. Same pattern as Maven releases with .mvn/maven.config.
  • Main stays versionless: no VERSION file on main. Trunk builds derive what they need without tracked version state.

This is a design question, not a blocker — there may be good reasons the SDK model needs to differ from Maven. But worth a deliberate decision rather than drifting into a different pattern.

Also from the automated review:

  • Double-increment bug: the manual workflow bumps VERSION then passes the same version-type to the action, which increments again. Git ends up with 1.2.7, NPM with 1.2.8. Needs fixing regardless of architectural direction.
  • Missing permissions: contents: write: the git push step will fail without write permissions if the repo default is read-only.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enforces a single source of truth for dotCMS JavaScript SDK versioning by introducing a core-web/libs/sdk/VERSION file and simplifying the SDK publish action to derive versions from it (instead of scanning NPM dist-tags).

Changes:

  • Added core-web/libs/sdk/VERSION and set it to 1.2.6 as the base SDK version.
  • Updated the deploy-javascript-sdk composite action to read the base version from VERSION and generate next versions using a CI-derived suffix.
  • Updated the manual SDK release workflow to bump/commit the VERSION file before publishing.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
core-web/libs/sdk/VERSION Introduces the canonical SDK base version file.
.github/workflows/cicd_manual-release-sdks.yml Bumps and commits VERSION before running the publish action.
.github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml Replaces NPM-scanning version logic with VERSION-based version selection and simplified publishing.

Comment thread .github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml Outdated
Comment thread .github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml Outdated
Comment thread .github/workflows/cicd_manual-release-sdks.yml Outdated
Comment thread .github/workflows/cicd_manual-release-sdks.yml Outdated
- Add VERSION file existence check with clear error message
- Fix run_number collision: use run_id-run_attempt (globally unique)
- Fix double-bump bug: remove pre-bump steps from manual workflow;
  action now owns the single VERSION increment and git commit/push
- Add custom-version same-as-current guard to prevent no-op commits
- Add permissions: contents: write to manual release job
- Fix for sdk in $(ls) fragility: use find -type d instead

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fmontes
Copy link
Copy Markdown
Member Author

fmontes commented Apr 16, 2026

All concrete issues from the automated review and inline comments have been addressed in fcbbd58:

  • Double-increment bug — fixed. Removed the pre-bump steps from the manual workflow. The action is now the single owner of the VERSION increment and the git commit/push. The manual workflow passes version-type straight through, so the bump happens exactly once.
  • Missing permissions: contents: write — fixed. Added to the release-sdks job.
  • VERSION file existence check — fixed. Added guard with clear ::error:: message.
  • run_number collision — fixed. Changed suffix to run_id-run_attempt, which is globally unique across all workflows.
  • custom-version same-as-current guard — fixed. Added check that exits with a clear error before reaching git commit.
  • for sdk in $(ls) fragility — already fixed in a prior commit with find -type d.

Regarding the architectural concerns (@spbolton):

  • Determinism / VERSION on main — acknowledged trade-off. The SDK versioning model intentionally differs from Maven: SDK consumers depend on a stable semver @latest tag rather than a commit hash, so a tracked VERSION file is the right anchor. The VERSION commit is a deliberate, minimal signal that "a release happened here."
  • Merge queue disruption — the VERSION bump commit goes through the action's git push, which does bypass the merge queue. This is an accepted trade-off for now given the low frequency of SDK releases. If disruption becomes a real issue, the release-branch pattern you described is the right next step.
  • @next semver ordering — acknowledged. After a release the VERSION file should be bumped to the next dev version (e.g. 1.2.7) so trunk next builds stay ahead of @latest. This is a process step that needs to be documented and ideally automated as a follow-up.

Maven was only used to install the Node/yarn toolchain. Replacing with
actions/setup-node is faster and removes the Java dependency entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fmontes fmontes marked this pull request as ready for review April 16, 2026 22:14
@fmontes fmontes requested a review from a team as a code owner April 16, 2026 22:14
@fmontes fmontes requested a review from Copilot April 16, 2026 22:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread .github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml Outdated
Comment thread .github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml Outdated
- Move VERSION commit/push to after successful publish so the repo
  version always reflects what actually landed on NPM
- Enable Corepack after setup-node to guarantee the Yarn version
  declared in packageManager is used, not the runner default
- Fail fast with a clear error on unknown version-type instead of
  silently falling through to auto-no-increment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fmontes fmontes requested a review from Copilot April 16, 2026 23:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

Comment thread .github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml Outdated
Comment thread .github/workflows/cicd_manual-release-sdks.yml
fmontes and others added 2 commits April 16, 2026 18:07
- Use node-version-file: .nvmrc instead of hardcoded Node version
- Add set -euo pipefail to bash steps that mutate package.json so any
  jq failure stops the action immediately instead of silently continuing
- Write actual_published_latest/next outputs before exit 1 so the
  VERSION commit step fires correctly when latest succeeds but next fails
- Update PR description to reflect that the action owns VERSION commits

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses architectural concern about VERSION file on main and merge
queue disruption from direct pushes.

action.yml:
- Removed version increment logic and git commit/push — action is now
  a pure publisher, reads VERSION from whatever ref is checked out
- Replaced version-type/custom-version inputs with publish-latest flag
- @next always published; @latest only when publish-latest=true

cicd_manual-release-sdks.yml:
- Creates a release branch (sdk/release-X.Y.Z) from main
- Commits VERSION bump on the release branch, never on main directly
- Publishes @latest from the release branch
- Opens a PR to bump VERSION to the next patch on main so trunk
  @next builds stay ahead of @latest semver-wise
- No merge queue bypass; main only changes via normal PR flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fmontes
Copy link
Copy Markdown
Member Author

fmontes commented Apr 17, 2026

@spbolton — all three concerns addressed in cb50376, the model now aligns with the existing dotCMS release pattern:

Determinism / VERSION on main — main still has the VERSION file, but it only changes via normal PRs through the merge queue (the post-release bump PR). No direct pushes to main from CI. Trunk reads VERSION as-is and publishes @next only — no git writes during trunk runs.

Merge queue disruption — eliminated. Manual releases now create a release branch (sdk/release-X.Y.Z), commit the VERSION bump there, and publish @latest from that branch. Same pattern as Maven releases with .mvn/maven.config on a release branch. Main is never pushed to directly.

@next semver ordering — fixed structurally. After publishing 1.2.6 as @latest from the release branch, the workflow automatically opens a PR bumping VERSION to 1.2.7 on main through the merge queue. Once merged, trunk @next builds produce 1.2.7-next.X, which is semver-ahead of 1.2.6. No manual step required.

The alternative you described (release branch pattern) is exactly what's now implemented. Thanks for the detailed write-up — it made the right direction clear.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

Comment thread .github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml Outdated
Comment thread .github/workflows/cicd_manual-release-sdks.yml
Comment thread .github/workflows/cicd_manual-release-sdks.yml
fmontes and others added 2 commits April 16, 2026 18:44
- Remove unused npm-package-tag input from action (tags are hardcoded
  to latest/next and the input was never wired up)
- Add set -euo pipefail to publish step so jq failures fail fast
- Add VERSION file existence and semver validation in manual workflow
  before arithmetic to give a clear error on malformed input
- Reset and clean workspace before git checkout main in post-release
  step so dirty package.json files from the publish action don't block
  the branch switch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fmontes fmontes added this pull request to the merge queue Apr 17, 2026
Merged via the queue into main with commit 591a7a1 Apr 17, 2026
50 checks passed
@fmontes fmontes deleted the 35109-sdk-single-version-source branch April 17, 2026 17:31
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 Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

fix(sdk): enforce single source of truth for SDK package versioning

6 participants