Files
ccs/docs/release-process.md
Kai (Tam Nhu) TranandGitHub 4e9c14b64a fix(release)!: decouple npm @latest from Docker rc.1 soak (REV11) (#1277)
Reverts the loop-1 prerelease channel that was publishing npm as
vX.Y.Z-rc.N to the `rc` dist-tag instead of `latest`.

`main` is now a stable semantic-release channel again: every merge
publishes vX.Y.Z immediately to npm @latest. The rc.1 soak window
that guards Docker mutable tags is moved entirely into the Docker
publish workflow:

- `.releaserc.cjs`: remove `prerelease: 'rc'` from productionConfig,
  restore `branches: ['main']` (stable). Restore full successComment
  and `released` label. Keep loop-1 releaseNotesGenerator additions
  (revert section, breaking change comment).

- `docker-release.yml`: every `release: published` event publishes
  only the immutable `:<ver>` Docker tag. `promote-mutable-tags` job
  now gates exclusively on `workflow_dispatch` with
  `promote_to_latest=true` — no longer triggered automatically by
  non-prerelease release events.

- `promote-release.yml`: rewritten as a dispatch wrapper that validates
  the stable tag exists and is not a prerelease, verifies the immutable
  Docker image is in the registry, then dispatches docker-release.yml
  with `promote_to_latest=true`. Removes the `gh release edit
  --prerelease=false` approach that required the rc soak to be wired
  through GitHub release state.

- `docs/release-process.md`: updated to reflect the decoupled model —
  npm @latest is immediate; Docker :latest requires manual promote after
  soak. Documents the `why` split between npm and Docker soak windows.
2026-05-17 05:46:48 -04:00

4.2 KiB

CCS Release Process

CCS uses a decoupled release model: every merge to main immediately publishes a stable npm @latest release and an immutable Docker :<ver> tag. Docker mutable tags (:latest, :<MAJOR>, :<MINOR>) require a separate manual promote step after an operator-verified soak window. This decouples the npm ecosystem from the Docker stability gate.

Phase 1 — Automatic stable release (on every merge to main)

  1. A PR is merged into main with a conventional commit (feat:, fix:, etc.).
  2. release.yml triggers semantic-release, which reads .releaserc.cjs.
  3. Because main is a stable channel, semantic-release cuts a GitHub release tagged vX.Y.Z and publishes the npm package to the @latest dist-tag immediately. No rc channel, no soak delay on npm.
  4. docker-release.yml triggers on the release: published event and:
    • Validates the tag as stable semver (vX.Y.Z).
    • Builds the integrated image for linux/amd64 and linux/arm64.
    • Pushes only the immutable ghcr.io/kaitranntt/ccs:X.Y.Z tag.
    • Signs the image with cosign (keyless OIDC).
    • Runs smoke tests (smoke-test job).
    • Mutable tags (:latest, :<MAJOR>, :<MINOR>) are not added at this stage — promote-mutable-tags only runs on explicit workflow_dispatch with promote_to_latest=true.

Phase 2 — Manual promotion to Docker mutable tags (rc.1 soak window)

After the immutable :<ver> Docker image has soaked (typically 24 h with no reported issues), the operator promotes mutable tags:

  1. Verify the immutable image is healthy:

    docker pull ghcr.io/kaitranntt/ccs:X.Y.Z
    docker run --rm -p 3000:3000 -p 8317:8317 ghcr.io/kaitranntt/ccs:X.Y.Z
    # check http://localhost:3000 and http://localhost:8317
    
  2. Optionally verify the cosign signature:

    cosign verify \
      --certificate-identity-regexp "https://github.com/kaitranntt/ccs/.github/workflows/docker-release.yml" \
      --certificate-oidc-issuer https://token.actions.githubusercontent.com \
      ghcr.io/kaitranntt/ccs:X.Y.Z
    
  3. Run the promote-release workflow via GitHub Actions UI or CLI:

    gh workflow run promote-release.yml \
      --field tag=vX.Y.Z
    

    This dispatches docker-release.yml with promote_to_latest=true, which triggers the promote-mutable-tags job to add :latest, :<MAJOR>, and :<MINOR> via docker buildx imagetools create.

    Alternatively, dispatch docker-release.yml directly:

    gh workflow run "Publish Docker Image" \
      --field tag=vX.Y.Z \
      --field promote_to_latest=true
    

Why npm and Docker have different soak windows

  • npm @latest: Published immediately on every main merge. npm users who pin a version are unaffected; users who run npm install -g @kaitranntt/ccs get the latest immediately. Rollback is npm install -g @kaitranntt/ccs@X.Y.Z.
  • Docker :latest: Promoted only after operator confirmation. Users who pull :latest or run docker pull without a pinned tag are shielded from a bad image. The immutable :<ver> tag is always available for pinned usage from the moment of release.

Verifying the promotion

# Confirm :latest points to the promoted digest
docker buildx imagetools inspect ghcr.io/kaitranntt/ccs:latest

# Confirm npm @latest updated (happens automatically at Phase 1)
npm view @kaitranntt/ccs dist-tags

Rollback

If a promoted release is found to be bad:

# Repoint :latest to the previous known-good immutable tag
docker buildx imagetools create \
  --tag ghcr.io/kaitranntt/ccs:latest \
  ghcr.io/kaitranntt/ccs:PREVIOUS.VERSION

# For npm, publish a fix as a new patch release (do not unpublish)
# Unpublishing npm packages causes downstream breakage for pinned consumers.

Branch / tag taxonomy

Branch Semantic-release channel npm dist-tag Docker tag (on release event) Docker mutable (on promote)
main stable @latest :<ver> (immutable, immediate) :latest, :<MAJOR>, :<MINOR> (after soak)
dev dev prerelease @dev not published not published