feat(ci): support release candidates via changesets pre mode (#3628)
## Summary Enables shipping `X.Y.Z-rc.N` prereleases of `@trigger.dev/*` via changesets pre mode. RCs publish under the `rc` npm dist-tag, never claim `latest`, and don't trigger marketing-site changelog PRs. The plumbing is hyphen-in-version detection in `release.yml` — no separate workflow, no opt-in flag at publish time. Validated end-to-end against a sandbox repo (real npm publishes, Docker builds, Helm chart pushes, GitHub releases) before porting back. Full RC lifecycle tested: pre enter → rc.0 → iterate to rc.1 → pre exit → stable. Plus interaction with the existing release-branch hotfix flow. ## What changes ### `release.yml` - New `is_prerelease` output (hyphen-in-version) - GitHub release adds `--prerelease` flag for RC publishes (Pre-release badge, not Latest) - `dispatch-changelog` job gated on `is_prerelease != 'true'` — no marketing-site PR per RC ### Docker workflows - Removes the `:v4-beta` floating tag entirely from `publish-webapp.yml` and `publish-worker-v4.yml`. v4 is GA; the tag is a misnomer and is already inconsistent with the npm side (npm `v4-beta` dist-tag was frozen at 4.0.4 months ago while Docker `:v4-beta` kept bumping). Self-hosters should pin to a versioned tag going forward — the last value of `:v4-beta` stays frozen wherever it currently points. ### CLI version-check fix (`packages/cli-v3/src/utilities/initialBanner.ts`) Switches the "new version available" comparison from JavaScript `localeCompare` to `semver.lt`. The old comparison handled `X.Y.Z-rc.N` vs `X.Y.Z` incorrectly — a user on `4.5.0-rc.0` would never be prompted to upgrade once `4.5.0` stable shipped (lex order put the prerelease ahead of the bare version). Real semver gets this right. Stable users were never affected: the check queries the `@latest` dist-tag, which by convention never points at a prerelease. ## How an RC actually publishes after this 1. `pnpm exec changeset pre enter rc` on main, push the `pre.json` 2. Bot regenerates the release PR as `chore: release v<X.Y.Z>-rc.0` 3. Merge → `release.yml` runs `changeset publish` which reads `pre.json.tag` and publishes under `--tag rc`. GitHub release marked Pre-release. No marketing-site dispatch. 4. Iterate by adding changesets normally; bot bumps to `rc.1`, `rc.2`, … 5. When ready: `pnpm exec changeset pre exit`, push, merge regenerated PR → stable ships under `latest` and the marketing-site dispatch fires.
This commit is contained in:
@@ -53,12 +53,6 @@ jobs:
|
||||
ref_without_tag=ghcr.io/triggerdotdev/trigger.dev
|
||||
image_tags=$ref_without_tag:${STEPS_GET_TAG_OUTPUTS_TAG}
|
||||
|
||||
# if tag is a semver, also tag it as v4
|
||||
if [[ "${STEPS_GET_TAG_OUTPUTS_IS_SEMVER}" == true ]]; then
|
||||
# TODO: switch to v4 tag on GA
|
||||
image_tags=$image_tags,$ref_without_tag:v4-beta
|
||||
fi
|
||||
|
||||
# when pushing the mutable main tag, also push an immutable-by-convention
|
||||
# full-commit-sha tag so a commit can be resolved to a specific digest
|
||||
if [[ "${STEPS_GET_TAG_OUTPUTS_TAG}" == "main" ]]; then
|
||||
|
||||
@@ -68,12 +68,6 @@ jobs:
|
||||
ref_without_tag=ghcr.io/triggerdotdev/${STEPS_GET_REPOSITORY_OUTPUTS_REPO}
|
||||
image_tags=$ref_without_tag:${STEPS_GET_TAG_OUTPUTS_TAG}
|
||||
|
||||
# if tag is a semver, also tag it as v4
|
||||
if [[ "${STEPS_GET_TAG_OUTPUTS_IS_SEMVER}" == true ]]; then
|
||||
# TODO: switch to v4 tag on GA
|
||||
image_tags=$image_tags,$ref_without_tag:v4-beta
|
||||
fi
|
||||
|
||||
echo "image_tags=${image_tags}" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
STEPS_GET_REPOSITORY_OUTPUTS_REPO: ${{ steps.get_repository.outputs.repo }}
|
||||
|
||||
@@ -64,6 +64,7 @@ jobs:
|
||||
published: ${{ steps.changesets.outputs.published }}
|
||||
published_packages: ${{ steps.changesets.outputs.publishedPackages }}
|
||||
published_package_version: ${{ steps.get_version.outputs.package_version }}
|
||||
is_prerelease: ${{ steps.get_version.outputs.is_prerelease }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
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
|
||||
@@ -124,6 +125,12 @@ jobs:
|
||||
run: |
|
||||
package_version=$(echo "${STEPS_CHANGESETS_OUTPUTS_PUBLISHEDPACKAGES}" | jq -r '.[0].version')
|
||||
echo "package_version=${package_version}" >> "$GITHUB_OUTPUT"
|
||||
# Any semver with a hyphen is a prerelease (e.g. 4.5.0-rc.0, 0.0.0-snapshot-...)
|
||||
if [[ "${package_version}" == *-* ]]; then
|
||||
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
env:
|
||||
STEPS_CHANGESETS_OUTPUTS_PUBLISHEDPACKAGES: ${{ steps.changesets.outputs.publishedPackages }}
|
||||
|
||||
@@ -133,13 +140,19 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_PR_BODY: ${{ github.event.pull_request.body }}
|
||||
STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION: ${{ steps.get_version.outputs.package_version }}
|
||||
STEPS_GET_VERSION_OUTPUTS_IS_PRERELEASE: ${{ steps.get_version.outputs.is_prerelease }}
|
||||
run: |
|
||||
VERSION="${STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION}"
|
||||
node scripts/generate-github-release.mjs "$VERSION" > /tmp/release-body.md
|
||||
PRERELEASE_FLAG=""
|
||||
if [ "${STEPS_GET_VERSION_OUTPUTS_IS_PRERELEASE}" = "true" ]; then
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
fi
|
||||
gh release create "v${VERSION}" \
|
||||
--title "trigger.dev v${VERSION}" \
|
||||
--notes-file /tmp/release-body.md \
|
||||
--target main
|
||||
--target main \
|
||||
$PRERELEASE_FLAG
|
||||
|
||||
- name: Create and push Docker tag
|
||||
if: steps.changesets.outputs.published == 'true'
|
||||
@@ -239,7 +252,7 @@ jobs:
|
||||
dispatch-changelog:
|
||||
name: 📝 Dispatch changelog PR
|
||||
needs: [release, update-release]
|
||||
if: needs.release.outputs.published == 'true'
|
||||
if: needs.release.outputs.published == 'true' && needs.release.outputs.is_prerelease != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
steps:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import chalk from "chalk";
|
||||
import { getLatestVersion } from "fast-npm-meta";
|
||||
import * as semver from "semver";
|
||||
import { VERSION } from "../version.js";
|
||||
import { chalkGrey, chalkRun, chalkTask, chalkWorker, logo } from "./cliOutput.js";
|
||||
import { logger } from "./logger.js";
|
||||
@@ -105,18 +106,16 @@ async function doUpdateCheck(): Promise<string | undefined> {
|
||||
return;
|
||||
}
|
||||
|
||||
const compareVersions = (a: string, b: string) =>
|
||||
a.localeCompare(b, "en-US", { numeric: true });
|
||||
|
||||
const comparison = compareVersions(VERSION, meta.version);
|
||||
|
||||
if (comparison === -1) {
|
||||
// Use real semver comparison (loose) so prereleases sort correctly against
|
||||
// their stable counterpart — e.g. a user on `4.5.0-rc.0` sees `4.5.0` as
|
||||
// newer. String/locale comparison gets this wrong for `X.Y.Z-rc.N` vs `X.Y.Z`.
|
||||
if (semver.lt(VERSION, meta.version, true)) {
|
||||
return meta.version;
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (err) {
|
||||
// ignore error
|
||||
// ignore error (covers both network failures and any version-parse oddities)
|
||||
logger.debug(err);
|
||||
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user