Release #865
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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." |