Skip to content

Commit 49c5362

Browse files
committed
ci(release): support release/* branches with version-comparison gating
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
1 parent a8966a4 commit 49c5362

6 files changed

Lines changed: 209 additions & 22 deletions

File tree

.github/workflows/changesets-pr.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
branches:
66
- main
7+
- "release/**"
78
paths:
89
- "packages/**"
910
- ".changeset/**"
@@ -56,16 +57,21 @@ jobs:
5657
env:
5758
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5859
run: |
59-
PR_NUMBER=$(gh pr list --head changeset-release/main --json number --jq '.[0].number')
60+
# On main: PR branch is `changeset-release/main`.
61+
# On release/X.Y.x: PR branch is `changeset-release/release/X.Y.x`.
62+
# Same source-branch lookup either way.
63+
SOURCE_BRANCH="${GITHUB_REF_NAME}"
64+
PR_BRANCH="changeset-release/${SOURCE_BRANCH}"
65+
PR_NUMBER=$(gh pr list --head "$PR_BRANCH" --json number --jq '.[0].number')
6066
if [ -n "$PR_NUMBER" ]; then
61-
git fetch origin changeset-release/main
67+
git fetch origin "+${PR_BRANCH}:refs/remotes/origin/${PR_BRANCH}"
6268
# we arbitrarily reference the version of the cli package here; it is the same for all package releases
63-
VERSION=$(git show origin/changeset-release/main:packages/cli-v3/package.json | jq -r '.version')
69+
VERSION=$(git show "origin/${PR_BRANCH}:packages/cli-v3/package.json" | jq -r '.version')
6470
gh pr edit "$PR_NUMBER" --title "chore: release v$VERSION"
6571
6672
# Enhance the PR body with a clean, deduplicated summary
6773
RAW_BODY=$(gh pr view "$PR_NUMBER" --json body --jq '.body')
68-
ENHANCED_BODY=$(CHANGESET_PR_BODY="$RAW_BODY" node scripts/enhance-release-pr.mjs "$VERSION")
74+
ENHANCED_BODY=$(CHANGESET_PR_BODY="$RAW_BODY" SOURCE_BRANCH="$SOURCE_BRANCH" node scripts/enhance-release-pr.mjs "$VERSION")
6975
if [ -n "$ENHANCED_BODY" ]; then
7076
gh api repos/triggerdotdev/trigger.dev/pulls/"$PR_NUMBER" \
7177
-X PATCH \

.github/workflows/publish-webapp.yml

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ on:
1313
type: string
1414
required: false
1515
default: ""
16+
is_latest:
17+
description: "Whether this version should also tag :v4-beta. release.yml computes via version comparison."
18+
type: boolean
19+
required: false
20+
default: false
1621
secrets:
1722
SENTRY_AUTH_TOKEN:
1823
required: false
@@ -52,16 +57,31 @@ jobs:
5257
ref_without_tag=ghcr.io/triggerdotdev/trigger.dev
5358
image_tags=$ref_without_tag:${STEPS_GET_TAG_OUTPUTS_TAG}
5459
55-
# if tag is a semver, also tag it as v4
5660
if [[ "${STEPS_GET_TAG_OUTPUTS_IS_SEMVER}" == true ]]; then
57-
# TODO: switch to v4 tag on GA
58-
image_tags=$image_tags,$ref_without_tag:v4-beta
61+
# Strip leading 'v' to get bare version (vX.Y.Z -> X.Y.Z)
62+
VERSION="${STEPS_GET_TAG_OUTPUTS_TAG#v}"
63+
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
64+
65+
# Per-line floating tag — analog of npm "release-X.Y" dist-tag.
66+
# Always set on a semver build so line-pinned consumers always get
67+
# the latest patch on their line, regardless of whether it's the
68+
# global :v4-beta / :latest.
69+
image_tags=$image_tags,$ref_without_tag:release-${MAJOR_MINOR}
70+
71+
# Global :v4-beta updated only when release.yml decided this version
72+
# is the highest-ever. Hotfixes on lagged lines must NOT clobber it.
73+
# TODO: switch to :latest on v4 GA.
74+
if [[ "${INPUTS_IS_LATEST}" == true ]]; then
75+
image_tags=$image_tags,$ref_without_tag:v4-beta
76+
fi
5977
fi
6078
6179
echo "image_tags=${image_tags}" >> "$GITHUB_OUTPUT"
80+
echo "::notice::Docker tags: ${image_tags}"
6281
env:
6382
STEPS_GET_TAG_OUTPUTS_TAG: ${{ steps.get_tag.outputs.tag }}
6483
STEPS_GET_TAG_OUTPUTS_IS_SEMVER: ${{ steps.get_tag.outputs.is_semver }}
84+
INPUTS_IS_LATEST: ${{ inputs.is_latest }}
6585

6686
- name: 📝 Set the build info
6787
id: set_build_info

.github/workflows/publish-worker-v4.yml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ on:
88
type: string
99
required: false
1010
default: ""
11+
is_latest:
12+
description: "Whether this version should also tag :v4-beta. release.yml computes via version comparison."
13+
type: boolean
14+
required: false
15+
default: false
1116
push:
1217
tags:
1318
- "re2-test-*"
@@ -68,17 +73,28 @@ jobs:
6873
ref_without_tag=ghcr.io/triggerdotdev/${STEPS_GET_REPOSITORY_OUTPUTS_REPO}
6974
image_tags=$ref_without_tag:${STEPS_GET_TAG_OUTPUTS_TAG}
7075
71-
# if tag is a semver, also tag it as v4
7276
if [[ "${STEPS_GET_TAG_OUTPUTS_IS_SEMVER}" == true ]]; then
73-
# TODO: switch to v4 tag on GA
74-
image_tags=$image_tags,$ref_without_tag:v4-beta
77+
VERSION="${STEPS_GET_TAG_OUTPUTS_TAG#v}"
78+
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
79+
80+
# Per-line floating tag — line-pinned consumers always get the latest
81+
# patch on their line, regardless of whether it's the global :v4-beta.
82+
image_tags=$image_tags,$ref_without_tag:release-${MAJOR_MINOR}
83+
84+
# Global :v4-beta updated only when this is the highest-ever version.
85+
# TODO: switch to :latest on v4 GA.
86+
if [[ "${INPUTS_IS_LATEST}" == true ]]; then
87+
image_tags=$image_tags,$ref_without_tag:v4-beta
88+
fi
7589
fi
7690
7791
echo "image_tags=${image_tags}" >> "$GITHUB_OUTPUT"
92+
echo "::notice::Docker tags: ${image_tags}"
7893
env:
7994
STEPS_GET_REPOSITORY_OUTPUTS_REPO: ${{ steps.get_repository.outputs.repo }}
8095
STEPS_GET_TAG_OUTPUTS_TAG: ${{ steps.get_tag.outputs.tag }}
8196
STEPS_GET_TAG_OUTPUTS_IS_SEMVER: ${{ steps.get_tag.outputs.is_semver }}
97+
INPUTS_IS_LATEST: ${{ inputs.is_latest }}
8298

8399
- name: 🐙 Login to GitHub Container Registry
84100
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0

.github/workflows/publish.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ on:
88
description: The image tag to publish
99
required: true
1010
type: string
11+
is_latest:
12+
description: "Whether this version is the highest released version. Drives :v4-beta / :latest tag updates. release.yml computes this via version comparison."
13+
required: false
14+
type: boolean
15+
default: false
1116
secrets:
1217
DOCKERHUB_USERNAME:
1318
required: false
@@ -73,6 +78,7 @@ jobs:
7378
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
7479
with:
7580
image_tag: ${{ inputs.image_tag }}
81+
is_latest: ${{ inputs.is_latest }}
7682

7783
publish-worker:
7884
needs: [typecheck]
@@ -95,3 +101,4 @@ jobs:
95101
uses: ./.github/workflows/publish-worker-v4.yml
96102
with:
97103
image_tag: ${{ inputs.image_tag }}
104+
is_latest: ${{ inputs.is_latest }}

.github/workflows/release.yml

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
types: [closed]
66
branches:
77
- main
8+
- "release/**"
89
workflow_dispatch:
910
inputs:
1011
type:
@@ -38,7 +39,7 @@ jobs:
3839
github.repository == 'triggerdotdev/trigger.dev' &&
3940
github.event_name == 'pull_request' &&
4041
github.event.pull_request.merged == true &&
41-
github.event.pull_request.head.ref == 'changeset-release/main'
42+
startsWith(github.event.pull_request.head.ref, 'changeset-release/')
4243
steps:
4344
- name: Show release summary
4445
env:
@@ -58,28 +59,39 @@ jobs:
5859
github.repository == 'triggerdotdev/trigger.dev' &&
5960
(
6061
(github.event_name == 'workflow_dispatch' && github.event.inputs.type == 'release') ||
61-
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'changeset-release/main')
62+
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'changeset-release/'))
6263
)
6364
outputs:
6465
published: ${{ steps.changesets.outputs.published }}
6566
published_packages: ${{ steps.changesets.outputs.publishedPackages }}
6667
published_package_version: ${{ steps.get_version.outputs.package_version }}
68+
is_latest: ${{ steps.compare.outputs.is_latest }}
6769
steps:
6870
- name: Checkout repo
6971
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # zizmor: ignore[artipacked] needs persisted git creds for tag push; no artifact upload here so no leak path
7072
with:
7173
fetch-depth: 0
7274
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ref || github.sha }}
7375

74-
- name: Verify ref is on main
76+
- name: Verify ref is on main or a release branch
7577
if: github.event_name == 'workflow_dispatch'
76-
run: |
77-
if ! git merge-base --is-ancestor "${GITHUB_EVENT_INPUTS_REF}" origin/main; then
78-
echo "Error: ref must be an ancestor of main (i.e., already merged)"
79-
exit 1
80-
fi
8178
env:
8279
GITHUB_EVENT_INPUTS_REF: ${{ github.event.inputs.ref }}
80+
run: |
81+
set -e
82+
if git merge-base --is-ancestor "${GITHUB_EVENT_INPUTS_REF}" origin/main; then
83+
echo "Ref is reachable from main."
84+
exit 0
85+
fi
86+
# Look for any origin/release/* branch that contains this ref.
87+
for branch in $(git branch -r --list 'origin/release/*' --format '%(refname:short)'); do
88+
if git merge-base --is-ancestor "${GITHUB_EVENT_INPUTS_REF}" "${branch}"; then
89+
echo "Ref is reachable from ${branch}."
90+
exit 0
91+
fi
92+
done
93+
echo "Error: ref must be reachable from main or a release/* branch." >&2
94+
exit 1
8395
8496
- name: Setup pnpm
8597
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
@@ -109,11 +121,60 @@ jobs:
109121
- name: Type check
110122
run: pnpm run typecheck --filter "@trigger.dev/*" --filter "trigger.dev"
111123

124+
# Decide whether this publish should become the new "latest" everywhere
125+
# (npm dist-tag, Docker `:v4-beta` / `:latest`, GitHub release "Latest"
126+
# badge, marketing-site changelog).
127+
#
128+
# Right rule: compare the new version to the current `latest`. NEW > CURRENT
129+
# => becomes latest. Otherwise => goes to a per-line dist-tag (release-X.Y).
130+
#
131+
# Handles two scenarios with one rule:
132+
# - Lagged hotfix (main shipped 4.6.0, hotfix 4.4.7 from release/4.4.x):
133+
# 4.4.7 < 4.6.0 -> NOT latest. dist-tag release-4.4.
134+
# - Fresh hotfix while main has unreleased work (main released 4.4.5,
135+
# PR #3173 merged but unreleased; hotfix 4.4.6 from release/4.4.x):
136+
# 4.4.6 > 4.4.5 -> IS latest. Customers running `npm install` get it.
137+
#
138+
# Source of truth: npm's `latest` dist-tag for the canonical package
139+
# (`@trigger.dev/sdk`). All public packages are version-locked via the
140+
# `fixed` config, so any one is canonical.
141+
- name: Compare new version to current latest
142+
id: compare
143+
run: |
144+
set -euo pipefail
145+
NEW=$(jq -r '.version' packages/cli-v3/package.json)
146+
CURRENT=$(npm view @trigger.dev/sdk dist-tags.latest 2>/dev/null || true)
147+
if [ -z "$CURRENT" ] || [ "$CURRENT" = "undefined" ]; then
148+
CURRENT="0.0.0"
149+
fi
150+
151+
# sort -V is semver-aware. NEW strictly greater than CURRENT => becomes latest.
152+
HIGHER=$(printf '%s\n%s\n' "$NEW" "$CURRENT" | sort -V | tail -1)
153+
if [ "$HIGHER" = "$NEW" ] && [ "$NEW" != "$CURRENT" ]; then
154+
IS_LATEST=true
155+
DIST_TAG=""
156+
else
157+
IS_LATEST=false
158+
DIST_TAG="release-$(echo "$NEW" | cut -d. -f1,2)"
159+
fi
160+
161+
echo "new=${NEW}" >> "$GITHUB_OUTPUT"
162+
echo "current=${CURRENT}" >> "$GITHUB_OUTPUT"
163+
echo "is_latest=${IS_LATEST}" >> "$GITHUB_OUTPUT"
164+
echo "dist_tag=${DIST_TAG}" >> "$GITHUB_OUTPUT"
165+
166+
echo "::notice::Publishing ${NEW}; current latest=${CURRENT}; is_latest=${IS_LATEST}; dist_tag=${DIST_TAG:-latest}"
167+
112168
- name: Publish
113169
id: changesets
114170
uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1.7.0
115171
with:
116-
publish: pnpm run changeset:release
172+
# When this publish lags (lower than current latest), pass --tag release-<M.m>
173+
# so npm's `latest` dist-tag is not touched. Otherwise default (= latest).
174+
# `release-` prefix because npm rejects dist-tag names that parse as a valid
175+
# semver range (`v4.4`, `4.4`, `4.4.x` would all be rejected).
176+
# Build was done above; this step only publishes.
177+
publish: ${{ steps.compare.outputs.dist_tag != '' && format('pnpm exec changeset publish --tag {0}', steps.compare.outputs.dist_tag) || 'pnpm exec changeset publish' }}
117178
createGithubReleases: false
118179
env:
119180
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -133,13 +194,19 @@ jobs:
133194
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
134195
RELEASE_PR_BODY: ${{ github.event.pull_request.body }}
135196
STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION: ${{ steps.get_version.outputs.package_version }}
197+
IS_LATEST: ${{ steps.compare.outputs.is_latest }}
136198
run: |
137199
VERSION="${STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION}"
138200
node scripts/generate-github-release.mjs "$VERSION" > /tmp/release-body.md
201+
# --target dropped: changesets created the per-package tags on the
202+
# release commit (which lives on main OR a release/* branch); the
203+
# tag itself carries the right commit, no need to pin --target.
204+
# --latest set explicitly: GitHub auto-detect uses publish date,
205+
# which would mark a lagged hotfix as "Latest" by accident.
139206
gh release create "v${VERSION}" \
140207
--title "trigger.dev v${VERSION}" \
141208
--notes-file /tmp/release-body.md \
142-
--target main
209+
--latest="${IS_LATEST}"
143210
144211
- name: Create and push Docker tag
145212
if: steps.changesets.outputs.published == 'true'
@@ -176,6 +243,7 @@ jobs:
176243
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
177244
with:
178245
image_tag: v${{ needs.release.outputs.published_package_version }}
246+
is_latest: ${{ needs.release.outputs.is_latest == 'true' }}
179247

180248
# Trigger Helm chart release directly via workflow_call (same GITHUB_TOKEN
181249
# limitation as the Docker path). Runs after Docker images are published so
@@ -247,7 +315,11 @@ jobs:
247315
token: ${{ secrets.CROSS_REPO_PAT }}
248316
repository: triggerdotdev/trigger.dev-site-v3
249317
event-type: new-release
250-
client-payload: '{"version": "${{ needs.release.outputs.published_package_version }}"}'
318+
# is_latest is included so the marketing site's changelog can
319+
# decide how to render lagged-line releases (e.g. demote to a
320+
# secondary section, or skip headline placement). Existing site
321+
# consumers ignoring the field are unaffected.
322+
client-payload: '{"version": "${{ needs.release.outputs.published_package_version }}", "is_latest": ${{ needs.release.outputs.is_latest }}}'
251323

252324
# The prerelease job needs to be on the same workflow file due to a limitation related to how npm verifies OIDC claims.
253325
prerelease:

0 commit comments

Comments
 (0)