Canary releases run on an hourly schedule via the Release workflow:
- Runs every hour via cron, skipping if no relevant files (
crates/,packages/,cli/) changed since the last canary tag - Skips if the latest commit is a release PR merge (to avoid releasing the version bump itself)
- Publishes to npm with the
canarytag - Opens a PR with auto-merge enabled to merge the version bump back to
main
No manual intervention required for canary releases.
-
Create a release by triggering the Turborepo Release workflow
- For stable releases, use
patch,minor, ormajor - For custom pre-releases, use
prepatch,preminor, orpremajor - Check the "Dry Run" box to test the workflow without publishing
- For stable releases, use
-
A PR is automatically opened to merge the release branch back into
main- Merge this promptly to avoid conflicts
-
Run
bump-version.shto update the versions of the packages. Merge in the changes tomain. -
Create a release by triggering the Turborepo Library Release workflow.
- Check the "Dry Run" box to run the full release workflow without publishing any packages.
- GitHub Release Notes are published automatically using the config from
turborepo-release.yml, triggered by theturbo-orchestratorbot.
This section provides comprehensive documentation on how the Turborepo CLI is released, including the architecture, workflows, and detailed step-by-step processes.
- Architecture Overview
- Automated Canary Releases
- Version Management
- Release Workflow Stages
- Packages Released
- Platform-Specific Binaries
- Technical Reference
- Best Practices
The Turborepo release process is a multi-stage pipeline that:
- Manages versions centrally via
version.txtat the repository root - Builds Rust binaries for 6 different platforms (macOS, Linux, Windows on x64 and ARM64)
- Packages native binaries as separate npm packages (e.g.,
@turbo/darwin-64,@turbo/linux-arm64) - Publishes JavaScript packages (main
turbopackage,create-turbo, codemods, ESLint plugins, etc.) - Aliases versioned documentation to subdomains (e.g.,
v2-5-4.turborepo.dev) - Creates a release branch with version bumps and automatically opens a PR to merge back to
main
The process is orchestrated through one GitHub Actions workflow:
.github/workflows/turborepo-release.yml- Handles both scheduled canary releases and manual releases
The canary release system runs on an hourly cron schedule, publishing a new canary version if relevant files have changed since the last release.
┌─────────────────────────────────────────────────────────────┐
│ Hourly cron fires │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ check-skip job │
│ - Skips if no relevant files changed since last release │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ turborepo-release.yml continues │
│ - Stages version bump │
│ - Runs smoke tests │
│ - Builds binaries │
│ - Publishes to npm │
│ - Aliases versioned docs │
│ - Creates PR with auto-merge │
└─────────────────────────────────────────────────────────────┘
The check-skip job finds the commit that last modified version.txt (which is always the release PR merge) and diffs from there to HEAD. If no files in crates/, packages/, or cli/ changed since that commit, there's nothing new to release and the run is skipped.
All releases (scheduled and manual) share a single concurrency group:
concurrency:
group: turborepo-release
cancel-in-progress: falseThis ensures only one release runs at a time. If a manual release is triggered while a scheduled run is in progress, it waits for the current run to finish.
The single source of truth for the Turborepo version is version.txt at the repository root. This file contains two lines:
- Line 1: Version number (e.g., 2.6.1)
- Line 2: NPM dist-tag (e.g., latest, canary)
See: version.txt
When a release is triggered, the scripts/version.js script:
- Reads the current version from
version.txt - Applies the specified SemVer increment (using the
semvernpm package) - Determines the npm dist-tag based on whether it's a pre-release
- Writes the new version and tag back to
version.txt
Increment Types:
| Increment Type | Description | Example | NPM Tag |
|---|---|---|---|
prerelease |
Bump canary of existing version | 2.6.1-canary.0 → 2.6.1-canary.1 |
canary |
prepatch |
Create first canary of next patch | 2.6.1 → 2.6.2-canary.0 |
canary |
preminor |
Create first canary of next minor | 2.6.1 → 2.7.0-canary.0 |
canary |
premajor |
Create first canary of next major | 2.6.1 → 3.0.0-canary.0 |
canary |
patch |
Stable patch release | 2.6.1 → 2.6.2 |
latest |
minor |
Stable minor release | 2.6.1 → 2.7.0 |
latest |
major |
Stable major release | 2.6.1 → 3.0.0 |
latest |
Note: Pre-release versions always use canary as the identifier unless overridden with the tag-override input.
Once the version is calculated, the cli/Makefile (target: stage-release) updates all package.json files by running pnpm version for each package to match TURBO_VERSION.
Additionally, the packages/turbo/bump-version.js postversion hook updates the optionalDependencies in packages/turbo/package.json to reference the correct versions of platform-specific packages.
See: cli/Makefile (stage-release target) and packages/turbo/bump-version.js
The release workflow consists of 7 sequential and parallel stages:
┌─────────────────────────────────────────────────────────────┐
│ Stage 1: Version & Stage Commit │
│ - Calculate new version │
│ - Create staging branch (staging-X.Y.Z) │
│ - Update all package.json files │
│ - Commit and tag (vX.Y.Z) │
│ - Push staging branch │
└──────────────────────┬──────────────────────────────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ Stage 2: │ │ Stage 3: │
│ Rust Smoke Test │ │ JS Smoke Test │
│ - cargo groups test │ │ - turbo run test │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
└────────────┬────────────┘
│
▼
┌────────────────────────┐
│ Stage 4: Build Rust │
│ (5 parallel targets) │
│ - macOS x64 & ARM64 │
│ - Linux x64 & ARM64 │
│ - Windows x64 │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ Stage 5: NPM Publish │
│ - Pack native packages │
│ - Publish native pkgs │
│ - Publish JS packages │
└───────────┬────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ Stage 6: │ │ Stage 7: │
│ Alias Versioned Docs │ │ Release PR │
│ - Find deployment │ │ - Create PR to main │
│ - Create subdomain │ │ - Include docs link │
│ alias │ │ or warning │
└──────────────────────┘ └──────────────────────┘
Job: stage (runs on ubuntu-latest)
Steps:
- Checkout repository at the specified SHA (defaults to latest commit on
main) - Setup Node.js environment
- Configure git with Turbobot credentials
- Run
node scripts/version.js <increment>to calculate the new version - Create staging branch:
staging-$(VERSION)(e.g.,staging-2.6.2) - Execute
make -C cli stage-releasewhich:- Verifies no unpushed commits exist
- Verifies
version.txtwas updated - Updates all
package.jsonfiles with the new version - Commits with message:
"publish $(VERSION) to registry"
- Create git tag:
v$(VERSION)(e.g.,v2.6.2) - Force push staging branch with tags to origin
Output: stage-branch (e.g., staging-2.6.2)
Safety Checks: The Makefile includes safety checks to verify no unpushed commits exist and that version.txt was properly updated before proceeding.
See: cli/Makefile (stage-release target)
Job: rust-smoke-test (depends on stage)
Steps:
- Checkout the staging branch
- Setup Turborepo environment (Rust toolchain only, skips Node.js setup)
- Install
cargo-nextestfor running tests - Run:
cargo nextest run --workspace
This runs all Rust unit tests to ensure the code builds and tests pass before publishing.
Job: js-smoke-test (depends on stage)
Steps:
- Checkout the staging branch
- Setup Node.js and install project dependencies
- Install global Turbo from npm (using
ci-tag-overrideif provided) - Run:
turbo run check-types test --filter="./packages/*" --color
This runs TypeScript type checking and all Jest/Vitest tests for JavaScript packages.
Note: The ci-tag-override parameter is useful when a recent release was faulty and you need to test against a specific npm tag.
Job: build-rust (parallel matrix across 5 target platforms)
Build Targets:
| Platform | Target Triple | Runner | Binary Name |
|---|---|---|---|
| macOS x64 | x86_64-apple-darwin |
macos-13 |
turbo |
| macOS ARM64 | aarch64-apple-darwin |
macos-latest (ARM64) |
turbo |
| Linux x64 | x86_64-unknown-linux-musl |
ubuntu-latest |
turbo |
| Linux ARM64 | aarch64-unknown-linux-musl |
ubuntu-latest |
turbo |
| Windows x64 | x86_64-pc-windows-msvc |
windows-latest |
turbo.exe |
Note: Windows ARM64 (aarch64-pc-windows-msvc) is not currently built but the wrapper supports it for future compatibility.
Build Configuration:
The Rust binaries are built using the release-turborepo profile (inherits from release profile with stripping enabled) and Link-time optimization (LTO) enabled via the CARGO_PROFILE_RELEASE_LTO=true environment variable.
See: Cargo.toml (release-turborepo profile) and .github/workflows/turborepo-release.yml
Build Steps:
- Install system dependencies (clang, musl-tools for Linux cross-compilation)
- Install Protoc (v26.x) and Cap'n Proto (code generation tools)
- Run:
cargo build --profile release-turborepo -p turbo --target <target> - Upload binary artifact from
target/<target>/release-turborepo/turbo*
Job: npm-publish (depends on all previous stages)
This is the most complex stage with multiple sub-steps:
- Download all platform-specific binary artifacts from Stage 4
- Move binaries to platform-specific directories:
rust-artifacts/turbo-aarch64-apple-darwin → cli/dist-darwin-arm64/turbo rust-artifacts/turbo-x86_64-apple-darwin → cli/dist-darwin-x64/turbo rust-artifacts/turbo-aarch64-unknown-linux-musl → cli/dist-linux-arm64/turbo rust-artifacts/turbo-x86_64-unknown-linux-musl → cli/dist-linux-x64/turbo rust-artifacts/turbo-x86_64-pc-windows-msvc → cli/dist-windows-x64/turbo.exe
Execute make -C cli build which runs turbo build copy-schema with filters for all JavaScript/TypeScript packages. This builds all TypeScript packages and copies the JSON schema to the appropriate locations.
See: cli/Makefile (build target)
Execute turbo release-native which invokes the @turbo/releaser tool.
The @turbo/releaser tool (packages/turbo-releaser/):
- Reads version and tag from
version.txt - For each platform (6 total):
- Generates a native package structure with platform-specific metadata (name, os, cpu, etc.)
- Copies
LICENSEandREADME.mdfrom template - For Windows: includes a
bin/turboNode.js wrapper script (to work around npm.exestripping) - Copies the prebuilt binary from
cli/dist-<os>-<arch>/ - Makes the binary executable (
chmod +xon Unix) - Creates a
.tar.gzarchive - Publishes to npm:
npm publish @turbo/<os>-<arch>.tar.gz --tag <npm-tag> --access public
See: packages/turbo-releaser/ for native package generation logic
Published native packages:
turbo-darwin-64turbo-darwin-arm64turbo-linux-64turbo-linux-arm64turbo-windows-64turbo-windows-arm64(package structure only, binary not yet built)
Execute make -C cli publish-turbo which:
- Packs all packages to tarballs using
pnpm pack - Publishes in fixed order to npm with the appropriate dist-tag (if not
--skip-publish)
See: cli/Makefile (publish-turbo target)
Why fixed order?
- Prevents race conditions where dependent packages are published before their dependencies
- Ensures
turbois published last so the platform specific binaries that it depends on are available.
Dry Run: If the workflow was triggered with dry_run: true or the Makefile is called with --skip-publish, the publish commands are skipped, allowing you to test the entire pipeline without publishing.
Job: alias-versioned-docs (depends on stage, npm-publish)
This stage creates a versioned subdomain alias for the documentation site, making docs for each release accessible at a version-specific URL (e.g., v2-5-4.turborepo.dev).
Steps:
- Checkout the staging branch with full git history and tags
- Read version from
version.txtand transform to subdomain format:2.5.4→v2-5-42.7.5-canary.0→v2-7-5-canary-0
- Get the SHA for the version tag using
git rev-list - Query Vercel API to find the deployment for that SHA
- Use Vercel CLI to assign the subdomain alias
Failure Handling:
- If aliasing fails, a Slack notification is sent to
#team-turborepo - The release PR will include a prominent warning banner
- The release itself is not blocked - the PR will still be created
Skipped during dry runs: This stage only runs when dry_run is false.
Required Secrets:
| Secret | Purpose |
|---|---|
TURBO_TOKEN |
Vercel API authentication |
VERCEL_ORG_ID |
Vercel team ID |
VERCEL_PROJECT_ID |
Vercel project ID for turbo-site |
Example URLs:
| Version | Subdomain URL |
|---|---|
2.5.4 |
https://v2-5-4.turborepo.dev |
2.7.5-canary.0 |
https://v2-7-5-canary-0.turborepo.dev |
3.0.0 |
https://v3-0-0.turborepo.dev |
For manual releases: A PR is automatically created using the thomaseizinger/create-pull-request action. Merge it as soon as possible after publishing.
For canary releases: The canary workflow creates a PR with auto-merge enabled. The PR includes:
- A list of commits/PRs included since the last canary
- A link to versioned docs (if aliasing succeeded)
The PR body will include:
- On success: A link to the versioned docs (e.g.,
https://v2-5-4.turborepo.dev) - On failure: A warning banner indicating the docs aliasing failed and needs manual intervention
The Turborepo release publishes 15 npm packages (6 native + 9 JavaScript):
| Package | Description | OS | Arch |
|---|---|---|---|
@turbo/darwin-64 |
macOS Intel binary | darwin |
x64 |
@turbo/darwin-arm64 |
macOS Apple Silicon binary | darwin |
arm64 |
@turbo/linux-64 |
Linux x64 binary (musl) | linux |
x64 |
@turbo/linux-arm64 |
Linux ARM64 binary (musl) | linux |
arm64 |
@turbo/windows-64 |
Windows x64 binary | win32 |
x64 |
@turbo/windows-arm64 |
Windows ARM64 binary | win32 |
arm64 |
Note: Native packages use musl for Linux to ensure maximum compatibility across distributions.
| Package | Description | Location |
|---|---|---|
turbo |
Main CLI package (platform detection and loader) | packages/turbo/ |
create-turbo |
Scaffold new Turborepo projects | packages/create-turbo/ |
@turbo/codemod |
Codemods for version upgrades | packages/turbo-codemod/ |
turbo-ignore |
CI/CD ignore utility (determines if deployment is needed) | packages/turbo-ignore/ |
@turbo/workspaces |
Workspace management tools | packages/turbo-workspaces/ |
@turbo/gen |
Generator for extending Turborepo | packages/turbo-gen/ |
eslint-plugin-turbo |
ESLint plugin for Turborepo | packages/eslint-plugin-turbo/ |
eslint-config-turbo |
Shared ESLint configuration | packages/eslint-config-turbo/ |
@turbo/types |
TypeScript types and JSON schema | packages/turbo-types/ |
The turbo package is unique:
-
Doesn't contain the binary - it's a JavaScript wrapper that:
- Detects the current platform and architecture
- Requires the appropriate platform-specific package
- Falls back to x64 on ARM64 for macOS/Windows (Rosetta/emulation support)
- Provides just-in-time installation if the platform package is missing
-
Declares platform packages as optional dependencies - all six platform-specific packages are listed as
optionalDependenciesin the package.json, allowing npm to install only the relevant one for the current platform. -
Entry point:
packages/turbo/bin/turbo(Node.js script)
See: packages/turbo/package.json and packages/turbo/bin/turbo
When a user runs turbo, the packages/turbo/bin/turbo script:
- Checks
TURBO_BINARY_PATHenvironment variable (for local development) - Detects platform:
process.platformandprocess.arch - Maps to package name using a platform-to-package mapping
- Attempts to require the correct platform package
- Falls back to x64 on ARM64 for macOS and Windows (Rosetta 2 / emulation support)
- Just-in-time install: If the package is missing, attempts
npm installfor that specific platform - Errors with diagnostics if all attempts fail
See: packages/turbo/bin/turbo for the complete platform detection logic
Windows has special considerations:
- Binary name:
turbo.exe - npm
.exestripping issue: npm strips.exefiles from thebin/directory - Solution: Native Windows packages include a
bin/turboNode.js wrapper script that spawnsturbo.exeand forwards all arguments and stdio
See: packages/turbo-releaser/ for the Windows wrapper generation
| Script/Command | Location | Purpose |
|---|---|---|
node scripts/version.js <increment> |
scripts/version.js |
Calculate new version and update version.txt |
make -C cli stage-release |
cli/Makefile |
Update all package.json versions and commit |
cargo build --profile release-turborepo -p turbo |
Cargo.toml |
Build Rust binary for release |
turbo release-native |
cli/turbo.json |
Pack and publish native packages |
make -C cli build |
cli/Makefile |
Build all JavaScript packages |
make -C cli publish-turbo |
cli/Makefile |
Pack and publish all packages |
pnpm version <version> --allow-same-version |
package.json | Update package version |
turboreleaser --version-path ../version.txt |
packages/turbo-releaser/ |
Pack native packages |
| Variable | Purpose | Example |
|---|---|---|
TURBO_VERSION |
Version to release (read from version.txt) | 2.6.2 |
TURBO_TAG |
npm dist-tag (read from version.txt) | latest or canary |
CARGO_PROFILE_RELEASE_LTO |
Enable link-time optimization for Rust | true |
TURBO_BINARY_PATH |
Override binary path (development only) | /path/to/turbo |
The release-turborepo profile inherits from the release profile with debug symbol stripping enabled. Link-time optimization is enabled via the CARGO_PROFILE_RELEASE_LTO=true environment variable during the build.
See: Cargo.toml (release-turborepo profile)
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
increment |
choice | Yes | prerelease |
SemVer increment type: prerelease, prepatch, preminor, premajor, patch, minor, major |
dry_run |
boolean | No | false |
Skip npm publish and PR creation (test mode) |
tag-override |
string | No | - | Override npm dist-tag (e.g., for backports) |
ci-tag-override |
string | No | - | Override npm tag for running tests (when recent release was faulty) |
sha |
string | No | - | Override SHA to release from (rarely used, mainly for debugging) |
| Tag | Usage | Example Version |
|---|---|---|
latest |
Stable releases | 2.6.2 |
canary |
Pre-release versions | 2.6.3-canary.0 |
next |
Beta releases (manual override) | 3.0.0-beta.1 |
backport |
Backported fixes (manual override) | 2.5.2-backport.0 |
Users can install specific tags:
npm install turbo@latest # Stable
npm install turbo@canary # Pre-release
npm install [email protected] # Specific versionImportant Note: Rust crate versions in Cargo.toml are not updated during releases. The Rust crates remain at version 0.1.0 in the manifest.
The version management is handled entirely through:
version.txtfor the release pipeline- npm package versions for distribution
This is because the Rust binary is never published to crates.io; it's only published to npm as platform-specific packages.
-
Let canary releases happen automatically: The hourly cron handles canary releases. No need to manually trigger
prereleasefor normal development. -
Use manual releases for stable versions: When ready to promote to stable, manually trigger the release workflow with
patch,minor, ormajor. -
Use dry run for testing: When in doubt, use
dry_run: trueto test the entire pipeline without publishing. -
Monitor canary PRs: Canary release PRs have auto-merge enabled, but check that they're merging successfully. If a canary PR fails to merge, investigate promptly.
-
Check npm after publishing: Verify that all packages were published correctly:
npm view turbo@<version> npm view @turbo/darwin-64@<version> npm view create-turbo@<version> # ... etc
-
Handle failed releases carefully: If a release fails mid-publish (some packages published, others not), document which packages were published and manually publish the rest if needed.
-
Backporting: Use
tag-overridewhen backporting fixes to older major versions. For example, releasing2.5.3whenmainis on3.0.0.
This section covers common failure scenarios and how to recover from them.
If a canary release fails after some packages were published but before others:
-
Identify what was published:
VERSION="2.6.1-canary.5" # The failed version for pkg in turbo @turbo/darwin-64 @turbo/darwin-arm64 @turbo/linux-64 @turbo/linux-arm64 @turbo/windows-64 @turbo/windows-arm64 create-turbo @turbo/codemod turbo-ignore @turbo/workspaces @turbo/gen eslint-plugin-turbo eslint-config-turbo @turbo/types; do npm view "$pkg@$VERSION" version 2>/dev/null && echo "✓ $pkg published" || echo "✗ $pkg NOT published" done
-
Option A - Deprecate and re-release: If few packages were published, deprecate them and trigger a new canary:
# Deprecate the partial release npm deprecate [email protected] "Partial release, use 2.6.1-canary.6" npm deprecate @turbo/[email protected] "Partial release, use 2.6.1-canary.6" # ... repeat for each published package # Merge any PR to main to trigger a new canary release
-
Option B - Manual completion: If most packages were published, manually publish the rest:
cd cli # Ensure you're on the staging branch git checkout staging-2.6.1-canary.5 # Publish missing packages manually npm publish ./path/to/package --tag canary
If a canary release PR is created but fails to auto-merge:
-
Check branch protection: Ensure required status checks are passing
-
Check for conflicts: The staging branch may have diverged from main
-
Manual merge: If checks pass, manually merge the PR via GitHub UI
-
Cleanup if abandoned: If you need to abandon the release:
# Delete the staging branch git push origin --delete staging-2.6.1-canary.5 # Close the PR via GitHub UI
If canary releases keep firing when they shouldn't:
-
Disable the workflow temporarily:
- Go to Actions → Release → "..." menu → Disable workflow
-
Investigate the cause:
- Check if the skip detection is working: the
check-skipjob should skip when the latest commit is a release PR merge or when no relevant files changed since the last canary tag - Verify that release PR commit messages match the expected format:
release(turborepo): <version>
- Check if the skip detection is working: the
-
Fix and re-enable:
- Ensure the release PR title follows the expected format
- Re-enable the workflow once the issue is resolved
If a canary release contains a critical bug:
-
Deprecate immediately (does NOT remove the package, just warns users):
npm deprecate [email protected] "Critical bug in task scheduling, use 2.6.1-canary.6 or later"
-
Cut a fix release: Merge the fix to main; the next hourly canary run will pick it up automatically
-
Unpublish (last resort, time-limited):
- npm allows unpublish within 72 hours for packages with few downloads
- Generally NOT recommended; deprecation is preferred
# Only if absolutely necessary and within 72 hours npm unpublish [email protected]
Staging branches (staging-X.Y.Z) are normally deleted when the PR merges. If orphaned branches accumulate:
# List orphaned staging branches
git fetch --prune
git branch -r | grep 'origin/staging-' | while read branch; do
echo "Orphaned: $branch"
done
# Delete a specific orphaned branch
git push origin --delete staging-2.6.1-canary.5If two releases attempted to use the same version:
- The second publish will fail with "cannot publish over existing version"
- Check which release succeeded:
npm view [email protected] - If needed, manually bump
version.txtand re-trigger
If the versioned docs subdomain wasn't created:
-
Check the workflow logs for the specific error
-
Manually create the alias:
# Find the deployment URL for the release commit vercel list turbo-site --scope=vercel -m githubCommitSha="<commit-sha>" # Create the alias vercel alias set <deployment-url> v2-6-1-canary-5.turborepo.dev --scope=vercel
-
A Slack notification is sent to
#team-turborepowhen this fails
The release pipeline handles sensitive operations (npm publishing, git tagging). Keep these security practices in mind:
-
Commit messages are trusted input: The skip detection reads the latest commit message via
git log. This is safe because commits tomainrequire PR approval, but never copy this pattern for workflows triggered by fork PRs. -
Version format is validated: The pipeline validates that version strings match expected semver patterns before using them in shell commands.
-
Secrets scope: The canary workflow inherits secrets to the release workflow. Only maintainers with write access can trigger releases.
-
OIDC publishing: npm packages are published using GitHub's OIDC trusted publishing, which provides cryptographic provenance without storing long-lived tokens.