Canary releases run on an hourly schedule via the Release workflow:
- Runs every hour via cron, skipping if no relevant files in
crates/orpackages/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, approved, and queued to merge the release branch back into
mainafter required checks pass.
-
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.
- The release PR is automatically approved and queued to merge after required checks pass.
- 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/ or packages/ 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 @turbo/releaser version command:
- 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 SemVer identifier. The tag-override input changes only the npm dist-tag.
Once the version is calculated, the @turbo/releaser prepare-stage command updates every published package by running pnpm version. It also updates the Turborepo skill version and versioned schema URLs.
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: packages/turbo-releaser/src/stage.ts 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:
- Checkout repository at the specified SHA (defaults to latest commit on
main) - Setup Node.js environment
- Run
turboreleaser versionto calculate the new version - Execute
turboreleaser prepare-stage, which:- Verifies
version.txtwas updated - Verifies the release tag and staging branch do not already exist
- Updates all published packages with the new version
- Updates the Turborepo skill version and schema URLs
- Creates staging branch:
staging-$(VERSION)(e.g.,staging-2.6.2)
- Verifies
- Create the staging commit through
scripts/create-github-api-commit.mjs
Output: stage-branch (e.g., staging-2.6.2)
Safety Checks: The workflow verifies that the selected SHA belongs to protected main. The releaser verifies that version.txt was updated and the target release tag and staging branch are 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: packages/turbo-releaser/src/stage.ts and scripts/create-github-api-commit.mjs
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 → release-artifacts/dist-darwin-arm64/turbo rust-artifacts/turbo-x86_64-apple-darwin → release-artifacts/dist-darwin-x64/turbo rust-artifacts/turbo-aarch64-unknown-linux-musl → release-artifacts/dist-linux-arm64/turbo rust-artifacts/turbo-x86_64-unknown-linux-musl → release-artifacts/dist-linux-x64/turbo rust-artifacts/turbo-x86_64-pc-windows-msvc → release-artifacts/dist-windows-x64/turbo.exe
The @turbo/releaser tool runs turbo run build copy-schema with filters for all released JavaScript/TypeScript packages. This builds all TypeScript packages and copies the JSON schema to the appropriate locations.
See: packages/turbo-releaser/src/publish.ts
The workflow invokes the @turbo/releaser tool directly.
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
release-artifacts/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)
The @turbo/releaser tool:
- Packs all packages to tarballs using
pnpm pack - Publishes in fixed order to npm with the appropriate dist-tag (if not
--skip-publish)
See: packages/turbo-releaser/src/publish.ts
Why fixed order?
- Prevents race conditions where dependent packages are published before their dependencies
- Ensures packages are published consistently in the configured order.
Dry Run: If the workflow was triggered with dry_run: true or the releaser 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 stable, custom, and canary releases, the release workflow uses its ephemeral GITHUB_TOKEN to create a PR as github-actions[bot] with the title chore: Release Turborepo <version>. It dispatches the release review gate on the pinned staging head. The gate validates the exact generated manifest, skill, and version changes and supplies the required semantic and test summary checks. The release workflow then revalidates the head SHA and squash-merges the PR.
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 |
|---|---|---|
turboreleaser version |
packages/turbo-releaser/ |
Calculate new version and update version.txt |
turboreleaser prepare-stage |
packages/turbo-releaser/ |
Update package versions and create staging branch |
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 |
turboreleaser publish |
packages/turbo-releaser/ |
Build, pack, and publish all npm packages |
turboreleaser tag |
packages/turbo-releaser/ |
Create and push the release tag |
pnpm version <version> --allow-same-version |
package.json | Update package version |
The workflow builds and invokes the private release tool directly:
pnpm --filter @turbo/releaser build
packages/turbo-releaser/dist/index.js version --version-path version.txt --increment prerelease
packages/turbo-releaser/dist/index.js prepare-stage --repo-root . --version-path version.txt
packages/turbo-releaser/dist/index.js publish --repo-root . --artifacts-dir release-artifacts --version-path version.txt
packages/turbo-releaser/dist/index.js tag --repo-root . --version-path version.txt| Variable | Purpose | Example |
|---|---|---|
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 and workflow-owned PR operations | ${{ secrets.GITHUB_TOKEN }} |
The main branch ruleset exempts exact generated release paths from the native team-review requirement and requires the Release Semantic Validation check. Non-release changes to those paths still require a write-authorized human approval through the review gate. Validation reads files through the GitHub API using immutable base and head SHAs.
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 turbo@2.6.2 # 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.
The workflow preserves the staging branch and any release tag once npm publishing starts. If publishing or a downstream job fails, re-run the failed jobs. The releaser checks each package version against the public npm registry: missing packages are published, while existing packages are skipped only when tarball integrity and the requested dist-tag match and a provenance attestation is present. Packages with different contents stop the release because npm versions are immutable. The primary turbo package is published last so it does not expose an incomplete release through the main package entry point.
After npm publish reports an error, the releaser polls npm before retrying. If the registry accepted the package despite the CLI error, matching integrity confirms the publish and the release continues.
The GitHub release step waits for the pushed tag to become visible through the GitHub API before creating the release. If that wait times out, verify that the tag points to the staging commit before re-running the failed jobs.
If a canary release fails after some packages were published but before others:
-
Re-run the failed jobs: The preserved staging branch and registry integrity checks allow the publisher to skip packages that completed and resume with missing packages.
-
If recovery reports different contents, 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
-
Deprecate and re-release: If a published package has different integrity, that version cannot be resumed safely. Deprecate the partial release and trigger a new canary:
# Deprecate the partial release npm deprecate turbo@2.6.1-canary.5 "Partial release, use 2.6.1-canary.6" npm deprecate @turbo/darwin-64@2.6.1-canary.5 "Partial release, use 2.6.1-canary.6" # ... repeat for each published package # Merge any PR to main to trigger a new canary release
-
Manual completion: If automated recovery is unavailable, publish the missing packages from the preserved staging branch:
# 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 semantic validation: Inspect the
Release review gateworkflow for unexpected files or manifest fields. -
Check branch protection: Ensure
Release Semantic Validation,Test Summary, andJS Test Summaryare passing. -
Check the PR head: A new commit invalidates the dispatched head SHA and requires validation to restart.
-
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 the release PR was opened by
github-actions[bot]with the titlechore: 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 turbo@2.6.1-canary.5 "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 turbo@2.6.1-canary.5
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 turbo@X.Y.Z - 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.