RELEASE.md
Canary releases run on an hourly schedule via the Release workflow:
crates/, packages/, cli/) changed since the last canary tagcanary tagmainNo manual intervention required for canary releases.
Create a release by triggering the Turborepo Release workflow
patch, minor, or majorprepatch, preminor, or premajorA PR is automatically opened to merge the release branch back into main
@turbo/repositoryRun bump-version.sh to update the versions of the packages. Merge in the changes to main.
Create a release by triggering the Turborepo Library Release workflow.
turborepo-release.yml,
triggered by the turbo-orchestrator bot.This section provides comprehensive documentation on how the Turborepo CLI is released, including the architecture, workflows, and detailed step-by-step processes.
The Turborepo release process is a multi-stage pipeline that:
version.txt at the repository root@turbo/darwin-64, @turbo/linux-arm64)turbo package, create-turbo, codemods, ESLint plugins, etc.)v2-5-4.turborepo.dev)mainThe process is orchestrated through one GitHub Actions workflow:
.github/workflows/turborepo-release.yml - Handles both scheduled canary releases and manual releasesThe 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: false
This 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:
See: version.txt
When a release is triggered, the scripts/version.js script:
version.txtsemver npm package)version.txtIncrement 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: prepare-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 (prepare-stage-release and commit-stage-release targets) 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 staging branch through GitHub API │
└──────────────────────┬──────────────────────────────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 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:
main)node scripts/version.js <increment> to calculate the new versionmake -C cli prepare-stage-release which:
version.txt was updatedpackage.json files with the new versionstaging-$(VERSION) (e.g., staging-2.6.2)make -C cli commit-stage-release which creates the staging commit through scripts/create-github-api-commit.mjsOutput: stage-branch (e.g., staging-2.6.2)
Safety Checks: The Makefile includes safety checks to verify no unpushed commits exist, version.txt was properly updated, and the target staging branch is not already present before proceeding. The commit helper creates the remote branch from local HEAD, uploads selected file changes through GitHub's createCommitOnBranch API, waits for GitHub commit verification, and then updates the local branch ref.
If the API commit is created but verification does not complete, the helper leaves the remote branch in place and exits with the commit SHA in the logs. Use the logged SHA and branch name to inspect recovery state before retrying or clearing the staging branch.
See: cli/Makefile (prepare-stage-release and commit-stage-release targets)
Job: rust-smoke-test (depends on stage)
Steps:
cargo-nextest for running testscargo nextest run --workspaceThis runs all Rust unit tests to ensure the code builds and tests pass before publishing.
Job: js-smoke-test (depends on stage)
Steps:
ci-tag-override if provided)turbo run check-types test --filter="./packages/*" --colorThis 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:
cargo build --profile release-turborepo -p turbo --target <target>target/<target>/release-turborepo/turbo*Job: npm-publish (depends on all previous stages)
This is the most complex stage with multiple sub-steps:
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/):
version.txtLICENSE and README.md from templatebin/turbo Node.js wrapper script (to work around npm .exe stripping)cli/dist-<os>-<arch>/chmod +x on Unix).tar.gz archivenpm publish @turbo/<os>-<arch>.tar.gz --tag <npm-tag> --access publicSee: 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:
pnpm pack--skip-publish)See: cli/Makefile (publish-turbo target)
Why fixed order?
turbo is 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:
version.txt and transform to subdomain format:
2.5.4 → v2-5-42.7.5-canary.0 → v2-7-5-canary-0git rev-listFailure Handling:
#team-turborepoSkipped 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:
The PR body will include:
https://v2-5-4.turborepo.dev)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/ |
turboThe turbo package is unique:
Doesn't contain the binary - it's a JavaScript wrapper that:
Declares platform packages as optional dependencies - all six platform-specific packages are listed as optionalDependencies in 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:
TURBO_BINARY_PATH environment variable (for local development)process.platform and process.archnpm install for that specific platformSee: packages/turbo/bin/turbo for the complete platform detection logic
Windows has special considerations:
turbo.exe.exe stripping issue: npm strips .exe files from the bin/ directorybin/turbo Node.js wrapper script that spawns turbo.exe and forwards all arguments and stdioSee: 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 prepare-stage-release | cli/Makefile | Update package versions and create staging branch |
make -C cli commit-stage-release | cli/Makefile | Commit staging changes through GitHub API |
node scripts/create-github-api-commit.mjs | scripts/ | Create a GitHub-verified API commit on a branch |
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 |
GH_TOKEN | GitHub API token for commit/PR steps only | ${{ secrets.GITHUB_TOKEN }} |
scripts/create-github-api-commit.mjs replaces local release git commit and branch pushes for generated release commits. It requires GITHUB_REPOSITORY and GH_TOKEN or GITHUB_TOKEN, creates a missing branch from local HEAD, then calls GitHub's createCommitOnBranch mutation with selected file changes.
Use explicit --path values when possible. --all-tracked includes all tracked changes compared to HEAD, and --include-untracked also includes unignored untracked files. The helper refuses sensitive-looking files, unsupported non-regular files, and oversized payloads before creating or updating the remote branch.
By default, an existing remote branch must point at local HEAD; otherwise the helper fails to preserve release lock semantics. Use --if-exists update only for idempotent workflows, such as the examples update PR, where reruns should commit onto an existing branch.
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 version
Important 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.txt for the release pipelineThis 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 prerelease for normal development.
Use manual releases for stable versions: When ready to promote to stable, manually trigger the release workflow with patch, minor, or major.
Use dry run for testing: When in doubt, use dry_run: true to 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-override when backporting fixes to older major versions. For example, releasing 2.5.3 when main is on 3.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:
Investigate the cause:
check-skip job should skip when the latest commit is a release PR merge or when no relevant files changed since the last canary tagrelease(turborepo): <version>Fix and re-enable:
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):
# 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.5
If two releases attempted to use the same version:
npm view [email protected]version.txt and re-triggerIf 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-turborepo when 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 to main require 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.