Skip to content

ci(release): support release/* branches with version-comparison gating#3530

Draft
ericallam wants to merge 4 commits intomainfrom
chore/release-branch-flow
Draft

ci(release): support release/* branches with version-comparison gating#3530
ericallam wants to merge 4 commits intomainfrom
chore/release-branch-flow

Conversation

@ericallam
Copy link
Copy Markdown
Member

@ericallam ericallam commented May 6, 2026

Adds support for shipping a patch release from a release/<major>.<minor>.x branch alongside the existing main-only flow. Use this when main has unreleased changes you can't include in the next release — a customer hotfix on the current line, an emergency patch, etc.

When to use this

You're on 4.4.5. Main has merged work queued for 4.5.0 (a feature, a refactor — anything not ready to ship). A customer hits a bug. Normally you'd be stuck: shipping from main bundles the unreleased work; reverting on main is messy.

With this PR you can cut release/4.4.x, ship 4.4.6, and main stays untouched. The patch goes through the normal release pipeline — npm publish, Docker image, Helm chart, GitHub release, marketing-site changelog — and customers running npm install @trigger.dev/sdk get it.

If you don't need this, don't use it. Normal main releases work exactly as before.

How to ship a patch from a release branch

The flow has five logical steps. Two of them are automated by the existing release pipeline and you don't do anything for them — they're called out here so you understand what's happening.

1. Create the release branch from the last release tag

A release branch is a parallel timeline that starts from a known-released commit and accumulates patches independently of main.

You always cut from a tag (v4.4.5), not from main HEAD. The tag is immutable and points at exactly the code customers are running. Cutting from main HEAD would pull in any commits or pending changesets that have landed since the tag — defeating the purpose.

The branch name follows the pattern release/<major>.<minor>.x — one branch per minor line. release/4.4.x accumulates 4.4.6, 4.4.7, etc. — never a 4.5.x release.

git fetch --tags origin
git switch -c release/4.4.x v4.4.5
git push -u origin release/4.4.x

2. Apply the fix

The release branch represents v4.4.5 + the fix, and nothing else from main. Two ways to get the fix onto the branch:

  • Fix already merged to main: cherry-pick the fix commit. Use git cherry-pick <sha> for a single commit, git cherry-pick A..B for a range.
  • Fix doesn't exist yet: write it directly on the release branch. You'll port it to main later.

Either way, the result is the same: this branch's tip is v4.4.5 + the fix.

3. Add a changeset (and optionally a .server-changes/ entry)

Changesets is the system that tells the release pipeline what to bump. Without a changeset, the pipeline has no work to do — there's no version to publish. Each changeset is a markdown file in .changeset/ declaring which packages bump and by how much.

For a patch release, you typically want patch on the package(s) you fixed. The changesets fixed config will cascade the bump across the linked packages automatically, so you don't need to list every package.

If the fix is server-only (webapp behavior, not a published package), use .server-changes/ instead. Both formats are picked up by the pipeline; only .changeset/ ones bump versions.

pnpm run changeset:add
git add .changeset/*.md .server-changes/*.md
git commit -m "fix: <description>"
git push origin release/4.4.x

4. (Automated) Version PR opens

Pushing to a release/* branch triggers changesets-pr.yml, the same workflow that opens release PRs from main. It runs changeset version, applies the bumps, stamps Chart.yaml, consumes any .server-changes/, and opens a pull request titled chore: release v4.4.6 (the title comes from the post-process script — that's also where the version is computed and shown).

The PR's body now starts with a "Release prep" header containing:

  • the proposed version
  • the source branch
  • the current latest on npm
  • whether this release will take the latest floating tags ("yes" if the proposed version is higher than current latest; "no" otherwise — see "What gets published" below for what that means)

That header is the at-a-glance check that the release pipeline understood the situation correctly. Review the rest of the PR like any other release PR.

5. Merge the version PR → release ships

Merging triggers release.yml. From the release pipeline's point of view, a release-branch merge looks identical to a main merge — same publish steps, same Docker tags, same Helm chart push, same marketing-site dispatch. The only thing that changes is which floating tags the publish takes (see below).

6. Port the fix back to main

After the release ships, the fix exists on the release branch but not on main. If you don't bring it back, main's next release won't include the fix and the same bug will reappear.

You want to cherry-pick the fix commit only — not the version-bump commit. The version-bump commit is the changeset PR's auto-generated bump (it edits package.json, Chart.yaml, etc.) and cherry-picking it onto main would conflict with main's existing versions and delete the consumed changeset.

git switch main
git cherry-pick <fix-sha>           # NOT the version-bump commit
git push

Main's next release naturally rolls up the fix at whatever version main is on.

What gets published

A successful release-branch run publishes:

  • npm: @trigger.dev/{sdk,core,cli-v3,...}@4.4.6
  • Docker: ghcr.io/triggerdotdev/trigger.dev:v4.4.6 and ghcr.io/triggerdotdev/trigger.dev:release-4.4
  • Helm: chart at version 4.4.6
  • GitHub release: v4.4.6
  • Marketing-site changelog: dispatched with the new version

Whether it also takes the floating tags (npm latest, Docker :v4-beta, GitHub "Latest" badge, headline placement on the changelog) depends on whether 4.4.6 is higher than the current latest on npm:

Situation Floating tags
Main hasn't shipped past 4.4.5 (queued work unreleased) → 4.4.6 > 4.4.5 ✅ moves to 4.4.6
Main has shipped 4.6.04.4.6 < 4.6.0 (lagged hotfix) ❌ stays on 4.6.0

In the lagged case, 4.4.6 still publishes — but customers running npm install (no version pin) keep getting 4.6.0. Customers who want the patch on the 4.4.x line install with npm install @trigger.dev/sdk@release-4.4 (or pin Docker to :release-4.4).

The version PR's "Release prep" header shows which case you're in before you merge.

Common pitfalls

  • Always cut from the tag, not main HEAD. If you cut from main, you accidentally pull in unmerged-but-pending changesets.
  • Cherry-pick the fix back to main, not the version bump. Picking the version-bump commit causes package.json conflicts and deletes the consumed changeset on main.
  • Two release branches can be active at once. release/4.4.x and release/4.5.x work concurrently. The repo-wide concurrency.group on release.yml serializes them so they don't collide on tags.
  • Stale workflow files on old release branches. A release/4.0.x cut six months ago has the .github/workflows/ from then. If you need a hotfix on a very old line, expect to update the workflows on that branch first (cherry-pick relevant CI changes from main).

Files changed

CI / scripts only. No package or app code changes.

  • release.yml — version-comparison gating, release/** triggers, propagates is_latest
  • publish.yml, publish-webapp.yml, publish-worker-v4.ymlis_latest input, gate :v4-beta, add :release-<M.m>
  • changesets-pr.yml — trigger on release/**, dynamic source-branch handling
  • scripts/enhance-release-pr.mjs — release-branch context header on the version PR

Validation

End-to-end tested in a sandbox repo with the same pipeline shape (3 npm packages, Docker image, Helm chart, cross-repo dispatch). Real npm publishes, real GHCR builds. Confirmed:

  • Lagged hotfix doesn't clobber any floating tags
  • Fresh hotfix while main has unreleased work correctly takes all floating tags
  • Two parallel release-branch hotfixes serialize without tag collisions
  • Cross-repo dispatch payload carries the right is_latest value

Lets us ship a patch (e.g. 4.4.6) from a release/4.4.x branch without
including unreleased work merged into main, and without the patch
clobbering floating tags incorrectly.

The release-pipeline pieces this touches and how each behaves now:

  npm dist-tag        latest if version > current latest, else release-<M.m>
  Docker :v4-beta     same gate (highest version only)
  Docker :release-X.Y new per-line floating tag, always set on a semver build
  GitHub release      --latest=true|false set explicitly (no auto-detect)

How the gate is computed:
  release.yml's 'Compare new version to current latest' step queries
  npm view @trigger.dev/sdk dist-tags.latest, compares via sort -V,
  sets is_latest=true|false. Drives every floating tag.

Triggers / refs:
  - pull_request:branches[main, release/**]
  - if-conditions allow head.ref starting with 'changeset-release/'
  - workflow_dispatch ref must be reachable from main OR a release/* branch
  - changesets-pr.yml fires on push to release/** too; PR-enhance step
    discovers source branch dynamically (no more hardcoded changeset-release/main)

Other changes:
  - gh release create: drop --target main (tag carries right commit)
  - dispatch-changelog payload includes is_latest so the marketing site
    can render lagged-line releases differently
  - enhance-release-pr.mjs prepends a Release prep header on release/*
    branches showing version, current latest, and whether the PR will
    take the latest dist-tag

release-helm.yml unchanged — already creates as draft+prerelease so it
can't claim Latest. publish-worker.yml (coordinator/provider) unchanged
since those don't have a :v4-beta-equivalent floating tag.

Validated end-to-end in ericallam/pkgring-sandbox across both scenarios:
  Scenario A (lagged hotfix): latest stays put, only release-X.Y moves
  Scenario B (main has unreleased work, hotfix is highest): latest moves
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 6, 2026

⚠️ No Changeset found

Latest commit: aa4a9a8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Caution

Review failed

Failed to post review comments

Walkthrough

Workflows and a PR enhancer were updated to support release/** branches and to propagate a computed is_latest flag through the release pipeline. changesets-pr now derives SOURCE_BRANCH from the PR ref, fetches the corresponding changeset-release branch, reads the package version from that branch, and passes SOURCE_BRANCH to the enhancer. release.yml compares the new version to npm dist-tags.latest, emits is_latest and dist_tag outputs, and forwards them to downstream steps. publish workflows accept is_latest and perform semver-aware Docker tagging (adding release-MAJOR.MINOR and optionally v4-beta). scripts/enhance-release-pr.mjs obtains release context and renders a “Release prep” section in PR bodies.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Detailed summary

  • .github/workflows/changesets-pr.yml

    • Added release/** to push triggers.
    • PR handling refactored to derive SOURCE_BRANCH from GITHUB_REF_NAME, compute PR_BRANCH as changeset-release/${SOURCE_BRANCH}, fetch that branch from origin, read version from origin/${PR_BRANCH}:packages/cli-v3/package.json, and update the PR title with that version.
    • Passes SOURCE_BRANCH into the PR body enhancer via environment.
  • .github/workflows/release.yml

    • PR trigger list expanded to include release/**.
    • show-release-summary and release job conditions relaxed to accept heads starting with changeset-release/*.
    • Added outputs to expose is_latest from a new version comparison step.
    • Inserted "Compare new version to current latest" step that queries npm dist-tags.latest, compares semver, sets is_latest and dist_tag, and writes outputs for downstream steps.
    • Publish step updated to optionally publish with the computed dist_tag.
    • create-unified-release and publish-docker steps receive is_latest and use it (create-unified-release uses --latest when appropriate).
    • Dispatch changelog payload now includes is_latest.
  • .github/workflows/publish.yml

    • Added is_latest boolean input to workflow_call.inputs.
    • Threaded is_latest into publish-webapp and publish-worker-v4 calls.
  • .github/workflows/publish-webapp.yml

    • Added is_latest input to workflow_call.inputs and exposed it as INPUTS_IS_LATEST in the step environment.
    • Replaced simple tagging with semver-aware tagging: when the image tag is semver (leading v allowed), derive MAJOR.MINOR and append release-MAJOR.MINOR; if INPUTS_IS_LATEST is true, also append v4-beta.
    • Logs and emits final image_tags.
  • .github/workflows/publish-worker-v4.yml

    • Added is_latest input and passed it into steps via INPUTS_IS_LATEST.
    • Extended tagging logic to derive and append release-MAJOR.MINOR for semver tags and conditionally append v4-beta when is_latest is true.
  • scripts/enhance-release-pr.mjs

    • Added async getReleaseContext() which reads SOURCE_BRANCH, queries npm dist-tags.latest, compares semantic versions to determine willBeLatest, extracts branch/line metadata, and returns a releaseContext object.
    • Updated formatPrBody signature to accept releaseContext: formatPrBody({ version, packageEntries, serverEntries, rawBody, releaseContext }).
    • When releaseContext is present, formatPrBody renders a “Release prep” section showing version, source branch, current latest, willBeLatest, and emits hotfix guidance when the source branch matches release/*.
    • main flow updated to obtain releaseContext and pass it into formatPrBody.

No public API or exported symbol declarations were changed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the primary change: adding support for release/* branches with version-comparison logic to gate which floating tags are applied.
Description check ✅ Passed The PR description comprehensively covers the feature, includes a detailed how-to guide, explains the publishing behavior and floating tag logic, validates with testing, and identifies all changed files; however, it does not follow the repository's template structure with explicit checklist and testing sections.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/release-branch-flow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant