Skip to content

Release

Release #865

# Release Pipeline
#
# This release consists of a few steps
#
# 1. Create a staging branch (acts as a lock to prevent concurrent releases)
# 2. Run some smoke tests on that branch
# 3. Build the Rust binary
# 4. Run security audits (cargo audit + pnpm audit)
# 5. Publish JS packages to npm (including turbo itself)
# 6. Create the git tag (only after npm publish succeeds)
# 7. Alias versioned docs (e.g., v2-5-4.turborepo.dev)
# 8. Create a release branch and open a PR
# 9. On failure, cleanup the staging branch and release tag automatically
#
# Canary releases run on an hourly schedule.
# Manual releases are triggered via workflow_dispatch.
#
# RECOVERY: If a release fails and cleanup doesn't work, use the
# 'clear-staging-branch' input to manually clear the stale staging branch.
name: Release
env:
CARGO_PROFILE_RELEASE_LTO: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
RELEASE_TURBO_CLI: true # TODO: do we need this?
permissions:
id-token: write # Required for npm Trusted Publishing using OIDC
contents: write # Allow workflow to checkout code from the repository
pull-requests: write # Allows the PR for post-release to be created
checks: write # Allows posting check statuses for release PRs
on:
schedule:
- cron: "0 * * * *"
workflow_dispatch:
inputs:
increment:
description: "SemVer Increment (prerelease = bump canary)"
required: true
default: "prerelease"
type: choice
options:
# Bump the canary version of the existing semver release
- prerelease
# Bump to the next patch version, creating its first canary release
- prepatch
# Bump to the next minor version, creating its first canary release
- preminor
# Bump to the next major version, creating its first canary release
- premajor
# Bump to the next patch version
- patch
# Bump to the next minor version
- minor
# Bump to the next major version
- major
dry_run:
description: "Do a dry run, skipping the final publish step."
type: boolean
tag-override:
description: "Override default npm dist-tag for the release. Should only be used for backporting"
required: false
type: string
ci-tag-override:
description: "Override default npm dist-tag to use for running tests. Should only be used when the most recent release was faulty"
required: false
type: string
default: ""
sha:
description: "Override the SHA to use for the release. Should rarely be used, usually only for debugging."
required: false
type: string
default: ""
clear-staging-branch:
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ⚠️ DANGER ZONE - READ CAREFULLY BEFORE USING │
# ├─────────────────────────────────────────────────────────────────────────────┤
# │ │
# │ This option deletes the staging branch for the version being released, │
# │ allowing the release to proceed when a previous release attempt failed. │
# │ │
# │ ❌ DO NOT USE IF: │
# │ • A release workflow is currently running (check the Actions tab!) │
# │ • You're unsure why the staging branch exists │
# │ • The npm package for this version was already published │
# │ │
# │ ✅ USE ONLY IF: │
# │ • A previous release workflow failed or was cancelled │
# │ • No release workflow is currently running for this version │
# │ • You've verified the npm package was NOT published (check npm) │
# │ │
# │ HOW TO VERIFY IT'S SAFE: │
# │ 1. Check Actions tab - no running release workflows │
# │ 2. Run: npm view turbo@<version> - should return "not found" │
# │ 3. Check git tags: git ls-remote --tags origin | grep <version> │
# │ - If tag exists, version was released successfully │
# │ │
# └─────────────────────────────────────────────────────────────────────────────┘
description: "⚠️ DANGER: Delete stale staging branch from a failed release. Only use if previous release failed AND no release is in progress. See workflow file for details."
type: boolean
default: false
concurrency:
group: turborepo-release
cancel-in-progress: false
jobs:
check-skip:
name: "Check Skip Conditions"
runs-on: ubuntu-latest
if: ${{ github.event_name == 'schedule' }}
outputs:
should_skip: ${{ steps.check.outputs.should_skip }}
steps:
- name: Check if should skip
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Find the commit that last updated version.txt (the release PR merge).
# If no relevant files changed since then, there's nothing new to release.
# Uses the GitHub API instead of a full clone to avoid fetching entire repo history.
LAST_VERSION_COMMIT=$(gh api "repos/${{ github.repository }}/commits?path=version.txt&per_page=1" --jq '.[0].sha')
CHANGES=$(gh api "repos/${{ github.repository }}/compare/${LAST_VERSION_COMMIT}...${{ github.sha }}" \
--jq '[.files[].filename | select(startswith("crates/") or startswith("packages/") or startswith("cli/"))] | length')
if [ "$CHANGES" = "0" ]; then
echo "Skipping: No relevant changes since last release (${LAST_VERSION_COMMIT:0:12})"
echo "should_skip=true" >> $GITHUB_OUTPUT
else
echo "should_skip=false" >> $GITHUB_OUTPUT
fi
stage:
needs: [check-skip]
if: ${{ always() && (github.event_name == 'workflow_dispatch' || needs.check-skip.outputs.should_skip != 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
stage-branch: ${{ steps.stage.outputs.stage-branch }}
base-sha: ${{ steps.base-sha.outputs.sha }}
version: ${{ steps.version.outputs.version }}
previous-tag: ${{ steps.previous-tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha || github.sha }}
- uses: ./.github/actions/setup-node
with:
enable-corepack: false
- name: Configure git
run: |
git config --global user.name 'Turbobot'
git config --global user.email 'turbobot@vercel.com'
- name: Get base SHA
id: base-sha
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Get previous tag
id: previous-tag
run: |
# Use ls-remote to query tags without fetching objects
PREV_TAG=$(git ls-remote --tags origin 'refs/tags/v*-canary.*' \
| awk '{print $2}' | sed 's|refs/tags/||' | grep -v '\^{}$' \
| sort -V | tail -n 1)
if [ -z "$PREV_TAG" ]; then
PREV_TAG=$(git ls-remote --tags origin 'refs/tags/v*' \
| awk '{print $2}' | sed 's|refs/tags/||' | grep -v '\^{}$' \
| grep -v canary | sort -V | tail -n 1)
fi
echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT
echo "Previous tag: $PREV_TAG"
- name: Clear stale staging branch (if requested)
if: ${{ inputs.clear-staging-branch }}
env:
INCREMENT: ${{ github.event_name == 'schedule' && 'prerelease' || inputs.increment }}
TAG_OVERRIDE: ${{ inputs.tag-override }}
run: |
echo "::warning::clear-staging-branch was enabled. This should only be used to recover from a failed release."
echo ""
# Calculate what version we're about to release so we know which staging branch to delete
./scripts/version.js "$INCREMENT" "$TAG_OVERRIDE"
VERSION=$(head -n 1 version.txt)
echo "Checking for stale staging branch: staging-${VERSION}"
if git ls-remote --exit-code --heads origin "staging-${VERSION}" >/dev/null 2>&1; then
echo "::warning::Deleting staging branch staging-${VERSION}..."
git push origin --delete "staging-${VERSION}"
echo "Deleted staging branch staging-${VERSION}"
else
echo "No staging branch found for staging-${VERSION}"
fi
# Reset version.txt so the Version step can run cleanly
git checkout version.txt
- name: Version
id: version
env:
# For scheduled runs (canary), always use prerelease. For workflow_dispatch, use the input.
INCREMENT: ${{ github.event_name == 'schedule' && 'prerelease' || inputs.increment }}
TAG_OVERRIDE: ${{ inputs.tag-override }}
run: |
if [[ -n "$TAG_OVERRIDE" && ! "$TAG_OVERRIDE" =~ ^[a-zA-Z0-9-]+$ ]]; then
echo "::error::Invalid tag-override format. Must be alphanumeric with hyphens only."
exit 1
fi
./scripts/version.js "$INCREMENT" "$TAG_OVERRIDE"
VERSION=$(head -n 1 version.txt)
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Invalid version format produced: $VERSION"
exit 1
fi
# If this is a canary, check whether its stable version was already
# published to npm. This catches the window between a stable publish
# and the release PR merging back into main (which bumps version.txt).
if [[ "$VERSION" == *"-canary."* ]]; then
STABLE_VERSION="${VERSION%%-canary.*}"
if npm view "turbo@${STABLE_VERSION}" version >/dev/null 2>&1; then
echo "::error::turbo@${STABLE_VERSION} already exists on npm. Skipping stale canary ${VERSION}."
echo "::error::The release PR that bumps version.txt likely hasn't merged yet."
exit 1
fi
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "New version: $VERSION"
cat version.txt
- name: Stage Commit
id: stage
run: |
cd cli && make stage-release
echo "stage-branch=$(git branch --show-current)" >> $GITHUB_OUTPUT
rust-smoke-test:
name: Rust Unit Tests
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [stage]
if: ${{ always() && needs.stage.result == 'success' }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.stage.outputs.stage-branch }}
- name: Setup Environment
uses: ./.github/actions/setup-environment
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
node: "false"
- name: Install cargo-nextest
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532
with:
tool: nextest
- name: Run tests
timeout-minutes: 30
run: cargo nextest run --workspace
js-smoke-test:
name: JS Package Tests
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [stage]
if: ${{ always() && needs.stage.result == 'success' }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.stage.outputs.stage-branch }}
- name: Setup Environment
uses: ./.github/actions/setup-environment
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
rust: "false"
capnproto: "false"
node-extra-flags: "--no-frozen-lockfile"
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install Global Turbo
uses: ./.github/actions/install-global-turbo
with:
turbo-version: ${{ inputs.ci-tag-override || '' }}
- name: Run JS Package Tests
run: turbo run check-types test --filter="./packages/*" --color
build-rust:
name: "Build Rust"
needs: [stage]
if: ${{ always() && needs.stage.result == 'success' }}
strategy:
fail-fast: false
matrix:
settings:
- host: macos-latest
target: "x86_64-apple-darwin"
- host: macos-latest
target: "aarch64-apple-darwin"
- host: ubuntu-latest
target: "x86_64-unknown-linux-musl"
setup: "sudo apt-get update && sudo apt-get install -y build-essential clang lldb llvm libclang-dev curl musl-tools sudo unzip"
- host: ubuntu-latest
target: "aarch64-unknown-linux-musl"
rust-build-env: 'CC_aarch64_unknown_linux_musl=clang AR_aarch64_unknown_linux_musl=llvm-ar RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld"'
setup: "sudo apt-get update && sudo apt-get install -y build-essential musl-tools clang llvm gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu"
- host: windows-latest
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.settings.host }}
timeout-minutes: 30
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
ref: ${{ needs.stage.outputs.stage-branch }}
- name: Setup Protoc
uses: ./.github/actions/setup-protoc
with:
version: "26.x"
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup capnproto
uses: ./.github/actions/setup-capnproto
- name: Rust Setup
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.settings.target }}
# needed to not make it override the defaults
rustflags: ""
# we want more specific settings
cache: false
- name: Build Setup
if: ${{ matrix.settings.setup }}
run: ${{ matrix.settings.setup }}
- name: Build
run: ${{ matrix.settings.rust-build-env }} cargo build --profile release-turborepo -p turbo --target ${{ matrix.settings.target }}
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: turbo-${{ matrix.settings.target }}
path: target/${{ matrix.settings.target }}/release-turborepo/turbo*
security-scan:
name: "Security Audit (Non-blocking)"
runs-on: ubuntu-latest
timeout-minutes: 15
continue-on-error: true
needs: [stage]
if: ${{ always() && needs.stage.result == 'success' }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.stage.outputs.stage-branch }}
- name: Setup Environment
uses: ./.github/actions/setup-environment
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
node-extra-flags: "--no-frozen-lockfile"
- name: Rust dependency audit
run: |
cargo install cargo-audit --locked --quiet
cargo audit --deny unsound --deny yanked
- name: JS dependency audit
run: pnpm audit --prod --audit-level low
build-gen:
name: "Build @turbo/gen Binaries"
needs: [stage]
if: ${{ always() && needs.stage.result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.stage.outputs.stage-branch }}
- uses: ./.github/actions/setup-node
with:
enable-corepack: false
extra-flags: "--no-frozen-lockfile"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install Global Turbo
uses: ./.github/actions/install-global-turbo
with:
turbo-version: ${{ inputs.ci-tag-override || '' }}
- name: Build @turbo/gen (dependencies + embedded templates)
run: turbo run build --filter=@turbo/gen
- name: Cross-compile @turbo/gen for all platforms
run: pnpm --filter @turbo/gen run build:all
- name: Upload gen binaries
uses: actions/upload-artifact@v4
with:
name: turbo-gen-binaries
path: packages/turbo-gen/dist/turbo-gen-*
npm-publish:
name: "Publish To NPM"
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [stage, build-rust, build-gen, rust-smoke-test, js-smoke-test]
if: ${{ always() && needs.stage.result == 'success' && needs.build-rust.result == 'success' && needs.build-gen.result == 'success' && needs.rust-smoke-test.result == 'success' && needs.js-smoke-test.result == 'success' }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.stage.outputs.stage-branch }}
- uses: ./.github/actions/setup-node
with:
enable-corepack: false
extra-flags: "--no-frozen-lockfile"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install Global Turbo
uses: ./.github/actions/install-global-turbo
with:
turbo-version: ${{ inputs.ci-tag-override || '' }}
- name: Configure git
run: |
git config --global user.name 'Turbobot'
git config --global user.email 'turbobot@vercel.com'
- name: Download Rust artifacts
uses: actions/download-artifact@v4
with:
path: rust-artifacts
- name: Download @turbo/gen binaries
uses: actions/download-artifact@v4
with:
name: turbo-gen-binaries
path: gen-binaries
- name: Move Rust artifacts into place
run: |
mv rust-artifacts/turbo-aarch64-apple-darwin cli/dist-darwin-arm64
mv rust-artifacts/turbo-aarch64-unknown-linux-musl cli/dist-linux-arm64
cp -r rust-artifacts/turbo-x86_64-pc-windows-msvc cli/dist-windows-arm64
mv rust-artifacts/turbo-x86_64-unknown-linux-musl cli/dist-linux-x64
mv rust-artifacts/turbo-x86_64-apple-darwin cli/dist-darwin-x64
mv rust-artifacts/turbo-x86_64-pc-windows-msvc cli/dist-windows-x64
- name: Move @turbo/gen binaries into place
run: |
mkdir -p cli/dist-gen-darwin-arm64 cli/dist-gen-darwin-x64 cli/dist-gen-linux-x64 cli/dist-gen-linux-arm64 cli/dist-gen-windows-x64
cp gen-binaries/turbo-gen-darwin-arm64 cli/dist-gen-darwin-arm64/turbo-gen
cp gen-binaries/turbo-gen-darwin-x64 cli/dist-gen-darwin-x64/turbo-gen
cp gen-binaries/turbo-gen-linux-x64 cli/dist-gen-linux-x64/turbo-gen
cp gen-binaries/turbo-gen-linux-arm64 cli/dist-gen-linux-arm64/turbo-gen
cp gen-binaries/turbo-gen-windows-x64.exe cli/dist-gen-windows-x64/turbo-gen.exe
- name: Ensure npm version
run: npm install -g npm@11.5.1
- name: Perform Release
run: |
SKIP_FLAG=""
if [ "${{ inputs.dry_run }}" = "true" ]; then
SKIP_FLAG="--skip-publish"
fi
cd cli && make publish-turbo SKIP_PUBLISH=$SKIP_FLAG
- name: Publish @turbo/gen platform packages
run: |
SKIP_FLAG=""
if [ "${{ inputs.dry_run }}" = "true" ]; then
SKIP_FLAG="--skip-publish"
fi
cd cli && pnpm exec turboreleaser gen --version-path ../version.txt $SKIP_FLAG
# Upload published artifacts in case they are needed for debugging later
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: turbo-combined
path: cli/dist
create-release-tag:
name: "Create Release Tag"
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [stage, npm-publish]
if: ${{ always() && !inputs.dry_run && needs.npm-publish.result == 'success' }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.stage.outputs.stage-branch }}
- name: Configure git
run: |
git config --global user.name 'Turbobot'
git config --global user.email 'turbobot@vercel.com'
- name: Create and push release tag
run: cd cli && make create-release-tag
alias-versioned-docs:
name: "Alias Versioned Docs"
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [stage, npm-publish]
if: ${{ always() && !inputs.dry_run && needs.npm-publish.result == 'success' }}
outputs:
success: ${{ steps.alias.outcome == 'success' }}
subdomain: ${{ steps.version.outputs.subdomain }}
version: ${{ steps.version.outputs.version }}
docs_url: ${{ steps.alias.outputs.docs_url }}
steps:
- name: Checkout staging branch
uses: actions/checkout@v4
with:
ref: ${{ needs.stage.outputs.stage-branch }}
- name: Get version and compute subdomain
id: version
run: |
VERSION=$(head -n 1 version.txt)
# Transform version to valid subdomain (replace dots with dashes, prepend v)
SUBDOMAIN=$(echo "v${VERSION}" | tr '.' '-')
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "subdomain=${SUBDOMAIN}" >> $GITHUB_OUTPUT
- name: Install Vercel CLI
run: npm install -g vercel@latest
- name: Find Vercel deployment for SHA
id: find-deployment
env:
VERCEL_TOKEN: ${{ secrets.TURBO_TOKEN }}
run: |
SHA="${{ needs.stage.outputs.base-sha }}"
DEPLOYMENT_URL=$(vercel list turbo-site --scope=vercel -m githubCommitSha="${SHA}" --status=READY --token="${VERCEL_TOKEN}" 2>&1 | tee /dev/stderr | grep -E '^\S+\.vercel\.(app|sh)' | head -n 1 | awk '{print $1}')
if [ -z "$DEPLOYMENT_URL" ]; then
echo "::error::No deployment found for SHA ${SHA}."
exit 1
fi
echo "deployment_url=${DEPLOYMENT_URL}" >> $GITHUB_OUTPUT
- name: Assign subdomain alias
id: alias
env:
VERCEL_TOKEN: ${{ secrets.TURBO_TOKEN }}
run: |
ALIAS="${{ steps.version.outputs.subdomain }}.turborepo.dev"
DEPLOYMENT_URL="${{ steps.find-deployment.outputs.deployment_url }}"
vercel alias set "${DEPLOYMENT_URL}" "${ALIAS}" --token="${VERCEL_TOKEN}" --scope=vercel
echo "docs_url=https://${ALIAS}" >> $GITHUB_OUTPUT
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1.23.0
with:
payload: |
{
"text": "Versioned docs aliasing failed for v${{ steps.version.outputs.version }}",
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "Versioned Docs Aliasing Failed" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Version:*\nv${{ steps.version.outputs.version }}" },
{ "type": "mrkdwn", "text": "*Subdomain:*\n${{ steps.version.outputs.subdomain }}.turborepo.dev" }
]
},
{
"type": "section",
"text": { "type": "mrkdwn", "text": "*Workflow:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Logs>" }
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.DOCS_ALIAS_FAILURE_SLACK_WEBHOOK_URL }}
create-release-pr:
name: "Create Release PR"
needs: [stage, npm-publish, create-release-tag, alias-versioned-docs]
if: ${{ always() && needs.npm-publish.result == 'success' && needs.create-release-tag.result == 'success' && !inputs.dry_run }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.stage.outputs.stage-branch }}
- uses: ./.github/actions/setup-node
with:
enable-corepack: false
extra-flags: "--no-frozen-lockfile"
- name: Configure git
run: |
git config --global user.name 'Turbobot'
git config --global user.email 'turbobot@vercel.com'
- name: Commit lockfile if updated by install
run: |
if git diff --quiet pnpm-lock.yaml; then
echo "Lockfile already up to date."
else
git add pnpm-lock.yaml
git commit -m "Update lockfile for release"
git push origin "${{ needs.stage.outputs.stage-branch }}"
fi
- name: Bump to next canary for stable releases
run: |
TAG=$(sed -n '2p' version.txt)
if [ "$TAG" = "latest" ]; then
VERSION="${{ needs.stage.outputs.version }}"
echo "Stable release detected (${VERSION}). Bumping to next prepatch canary..."
./scripts/version.js prepatch
cat version.txt
git commit -anm "bump to next canary after ${VERSION}"
git push origin "${{ needs.stage.outputs.stage-branch }}"
else
echo "Pre-release ($TAG), skipping canary bump."
fi
- name: Build PR body
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.stage.outputs.version }}"
PREVIOUS_TAG="${{ needs.stage.outputs.previous-tag }}"
DOCS_URL="${{ needs.alias-versioned-docs.outputs.docs_url }}"
echo "## Release v${VERSION}" > pr-body.md
echo "" >> pr-body.md
# Docs link (or warning if failed)
if [ "${{ needs.alias-versioned-docs.result }}" != "success" ]; then
echo "> [!CAUTION]" >> pr-body.md
echo "> Versioned docs aliasing FAILED. [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> pr-body.md
elif [ -n "$DOCS_URL" ]; then
echo "Versioned docs: ${DOCS_URL}" >> pr-body.md
fi
echo "" >> pr-body.md
# Changelog (via GitHub API to avoid needing full git history)
echo "### Changes" >> pr-body.md
echo "" >> pr-body.md
if [ -n "$PREVIOUS_TAG" ]; then
gh api "repos/${{ github.repository }}/compare/${PREVIOUS_TAG}...main" \
--jq '.commits[] | "- \(.commit.message | split("\n") | .[0]) (`\(.sha[:7])`)"' >> pr-body.md
else
echo "First release - no previous tag." >> pr-body.md
fi
- name: Create PR with auto-merge
id: create-pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.stage.outputs.version }}"
STAGE_BRANCH="${{ needs.stage.outputs.stage-branch }}"
PR_URL=$(gh pr create \
--title "release(turborepo): ${VERSION}" \
--body-file pr-body.md \
--head "${STAGE_BRANCH}" \
--base main)
echo "url=$PR_URL" >> $GITHUB_OUTPUT
PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "number=$PR_NUM" >> $GITHUB_OUTPUT
gh pr merge "$PR_NUM" --auto --squash
- name: Post required check statuses
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUM="${{ steps.create-pr.outputs.number }}"
SHA=$(gh pr view "$PR_NUM" --json headRefOid --jq '.headRefOid')
for check in "Test Summary" "JS Test Summary"; do
gh api "repos/${{ github.repository }}/check-runs" \
--method POST \
-f name="$check" \
-f head_sha="$SHA" \
-f status="completed" \
-f conclusion="success" \
-f "output[title]=Skipped for release PR" \
-f "output[summary]=Release PRs skip CI - code was already tested on main before release."
done
cleanup-on-failure:
name: "Cleanup Failed Release"
runs-on: ubuntu-latest
timeout-minutes: 10
needs:
[
stage,
build-rust,
build-gen,
rust-smoke-test,
js-smoke-test,
npm-publish,
create-release-tag,
create-release-pr
]
if: >-
${{
always()
&& needs.stage.result == 'success'
&& (
needs.build-rust.result == 'failure'
|| needs.build-gen.result == 'failure'
|| needs.rust-smoke-test.result == 'failure'
|| needs.js-smoke-test.result == 'failure'
|| needs.npm-publish.result == 'failure'
|| needs.create-release-tag.result == 'failure'
|| needs.create-release-pr.result == 'failure'
)
}}
steps:
- name: Delete staging branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
STAGE_BRANCH="${{ needs.stage.outputs.stage-branch }}"
echo "::warning::Release failed. Cleaning up staging branch ${STAGE_BRANCH}..."
gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/${STAGE_BRANCH}" || echo "Branch may already be deleted or not exist"
- name: Delete release tag if it exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.stage.outputs.version }}"
echo "::warning::Cleaning up release tag v${VERSION} if it exists..."
HTTP_STATUS=$(gh api -X DELETE "repos/${{ github.repository }}/git/refs/tags/v${VERSION}" 2>&1) && echo "Tag deleted." || {
if echo "$HTTP_STATUS" | grep -q "Reference does not exist"; then
echo "Tag does not exist, nothing to clean up."
else
echo "::error::Failed to delete tag v${VERSION}: ${HTTP_STATUS}"
exit 1
fi
}
echo "Cleanup complete. You can retry the release without using clear-staging-branch."