Release #2376
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. Sign and notarize macOS binaries | |
| # 5. Publish JS packages to npm | |
| # 6. Create the git tag (only after npm publish succeeds) | |
| # 7. Publish the VS Code extension from the release tag | |
| # 8. Alias versioned docs (e.g., v2-5-4.turborepo.dev) | |
| # 9. Create a release branch and open a PR, even if VS Code publishing fails | |
| # 10. 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 | |
| TURBO_TELEMETRY_MESSAGE_DISABLED: "1" | |
| HUSKY: "0" | |
| RELEASE_TURBO_CLI: true # TODO: do we need this? | |
| permissions: | |
| contents: read | |
| 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-24.04 | |
| 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/"))] | 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-24.04 | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: write | |
| 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: | |
| - name: Ensure release runs from default branch | |
| if: ${{ github.ref_name != github.event.repository.default_branch }} | |
| run: | | |
| echo "::error::Release workflow must run from the default branch." | |
| exit 1 | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ inputs.sha || github.sha }} | |
| filter: blob:none | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Validate SHA override | |
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.sha != '' }} | |
| run: | | |
| git fetch --filter=blob:none origin main | |
| if ! git merge-base --is-ancestor HEAD origin/main; then | |
| echo "::error::sha override must be reachable from protected main." | |
| exit 1 | |
| fi | |
| - uses: ./.github/actions/setup-node | |
| with: | |
| enable-corepack: false | |
| - name: Build release tool | |
| run: pnpm --filter @turbo/releaser build | |
| - name: Get base SHA | |
| id: base-sha | |
| run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT | |
| - name: Get previous tag | |
| id: previous-tag | |
| env: | |
| INCREMENT: ${{ github.event_name == 'schedule' && 'prerelease' || inputs.increment }} | |
| run: | | |
| # For minor/major releases, compare against the previous minor/major | |
| # release (vX.Y.0) so notes include all patches since then. | |
| # For patch releases, compare against the latest stable tag. | |
| # For canary releases, compare against the latest canary tag. | |
| if [[ "$INCREMENT" == "minor" || "$INCREMENT" == "major" ]]; then | |
| PREV_TAG=$(git ls-remote --tags origin 'refs/tags/v*' \ | |
| | awk '{print $2}' | sed 's|refs/tags/||' | grep -v '\^{}$' \ | |
| | grep -v canary | grep -E 'v[0-9]+\.[0-9]+\.0$' | sort -V | tail -n 1) | |
| elif [[ "$INCREMENT" == "patch" ]]; 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) | |
| else | |
| 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 | |
| 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: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 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 | |
| VERSION_ARGS=(--version-path version.txt --increment "$INCREMENT") | |
| if [ -n "$TAG_OVERRIDE" ]; then | |
| VERSION_ARGS+=(--tag-override "$TAG_OVERRIDE") | |
| fi | |
| packages/turbo-releaser/dist/index.js version "${VERSION_ARGS[@]}" | |
| 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 | |
| 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}..." | |
| gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/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 | |
| VERSION_ARGS=(--version-path version.txt --increment "$INCREMENT") | |
| if [ -n "$TAG_OVERRIDE" ]; then | |
| VERSION_ARGS+=(--tag-override "$TAG_OVERRIDE") | |
| fi | |
| packages/turbo-releaser/dist/index.js version "${VERSION_ARGS[@]}" | |
| 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: Prepare stage branch | |
| run: packages/turbo-releaser/dist/index.js prepare-stage --repo-root . --version-path version.txt | |
| - name: Commit stage branch | |
| id: stage | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION=$(head -n 1 version.txt) | |
| node scripts/create-github-api-commit.mjs \ | |
| --branch "staging-${VERSION}" \ | |
| --message "publish ${VERSION} to registry" \ | |
| --all-tracked | |
| echo "stage-branch=$(git branch --show-current)" >> $GITHUB_OUTPUT | |
| # TODO: Re-enable Rust unit tests once flakiness is resolved. | |
| # 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@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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-24.04 | |
| timeout-minutes: 30 | |
| needs: [stage] | |
| if: ${{ always() && needs.stage.result == 'success' }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ needs.stage.outputs.stage-branch }} | |
| filter: blob:none | |
| persist-credentials: false | |
| - 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@0c5077e51419868618aeaa5fe8019c62421857d6 # 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/*" --filter="!@turbo/repository" --color | |
| build-rust: | |
| name: "Build Rust" | |
| needs: [stage] | |
| if: ${{ always() && needs.stage.result == 'success' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| settings: | |
| - host: macos-15-xlarge | |
| target: "x86_64-apple-darwin" | |
| - host: macos-15-xlarge | |
| target: "aarch64-apple-darwin" | |
| - host: ubuntu-24.04 | |
| 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-24.04 | |
| target: "aarch64-unknown-linux-musl" | |
| # clang sees glibc 2.39 headers when compiling aws-lc-sys C code, | |
| # but the final static link uses musl, which only exports the base symbols. | |
| rust-build-env: 'CC_aarch64_unknown_linux_musl=clang AR_aarch64_unknown_linux_musl=llvm-ar RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld -Clink-arg=--defsym=__isoc23_sscanf=sscanf -Clink-arg=--defsym=__isoc23_strtol=strtol"' | |
| 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-2022 | |
| target: x86_64-pc-windows-msvc | |
| runs-on: ${{ matrix.settings.host }} | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ needs.stage.outputs.stage-branch }} | |
| filter: blob:none | |
| persist-credentials: false | |
| - 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@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1 | |
| with: | |
| target: ${{ matrix.settings.target }} | |
| # needed to not make it override the defaults | |
| rustflags: "" | |
| # we want more specific settings | |
| cache: false | |
| - name: Setup Zig | |
| uses: ./.github/actions/setup-zig | |
| - 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: Install rcodesign | |
| if: ${{ contains(matrix.settings.target, 'apple-darwin') }} | |
| run: cargo install apple-codesign --version 0.29.0 --locked | |
| - name: Write Apple signing certificate | |
| if: ${{ contains(matrix.settings.target, 'apple-darwin') }} | |
| shell: bash | |
| env: | |
| APPLE_CERT_DATA: ${{ secrets.APPLE_CERT_DATA }} | |
| APPLE_CERT_PATH: ${{ runner.temp }}/apple-certificate.p12 | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| if [[ -z "$APPLE_CERT_DATA" ]]; then | |
| echo "::error::APPLE_CERT_DATA secret is empty or unavailable" | |
| exit 1 | |
| fi | |
| printf '%s' "$APPLE_CERT_DATA" | base64 --decode > "$APPLE_CERT_PATH" | |
| chmod 600 "$APPLE_CERT_PATH" | |
| if [[ ! -s "$APPLE_CERT_PATH" ]]; then | |
| echo "::error::Decoded Apple certificate is empty" | |
| exit 1 | |
| fi | |
| echo "APPLE_CERT_PATH=$APPLE_CERT_PATH" >> "$GITHUB_ENV" | |
| - name: Write Apple notarization API key | |
| if: ${{ contains(matrix.settings.target, 'apple-darwin') }} | |
| shell: bash | |
| env: | |
| APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} | |
| APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.json | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| if [[ -z "$APPLE_API_KEY" ]]; then | |
| echo "::error::APPLE_API_KEY secret is empty or unavailable" | |
| exit 1 | |
| fi | |
| printf '%s' "$APPLE_API_KEY" | base64 --decode > "$APPLE_API_KEY_PATH" | |
| chmod 600 "$APPLE_API_KEY_PATH" | |
| if [[ ! -s "$APPLE_API_KEY_PATH" ]]; then | |
| echo "::error::Decoded Apple API key is empty" | |
| exit 1 | |
| fi | |
| echo "APPLE_API_KEY_PATH=$APPLE_API_KEY_PATH" >> "$GITHUB_ENV" | |
| - name: Sign macOS binary | |
| if: ${{ contains(matrix.settings.target, 'apple-darwin') }} | |
| shell: bash | |
| env: | |
| APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} | |
| TURBO_APPLE_SIGNING_IDENTIFIER: com.vercel.turbo | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "$APPLE_CERT_PASSWORD" ]]; then | |
| echo "::error::APPLE_CERT_PASSWORD secret is empty or unavailable" | |
| exit 1 | |
| fi | |
| bin="target/${{ matrix.settings.target }}/release-turborepo/turbo" | |
| rcodesign sign \ | |
| --for-notarization \ | |
| --binary-identifier "$TURBO_APPLE_SIGNING_IDENTIFIER" \ | |
| --p12-file "$APPLE_CERT_PATH" \ | |
| --p12-password "$APPLE_CERT_PASSWORD" \ | |
| "$bin" | |
| codesign --verify --strict --verbose=2 "$bin" | |
| requirement=$(codesign -dr - "$bin" 2>&1) | |
| printf '%s\n' "$requirement" | |
| expected_identifier="identifier \"$TURBO_APPLE_SIGNING_IDENTIFIER\"" | |
| if [[ "$requirement" != *"$expected_identifier"* ]]; then | |
| echo "::error::Expected designated requirement to contain $expected_identifier" | |
| exit 1 | |
| fi | |
| - name: Notarize macOS binary | |
| if: ${{ contains(matrix.settings.target, 'apple-darwin') }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bin="target/${{ matrix.settings.target }}/release-turborepo/turbo" | |
| zip_path="$RUNNER_TEMP/turbo-${{ matrix.settings.target }}.zip" | |
| ditto -c -k --keepParent "$bin" "$zip_path" | |
| rcodesign notary-submit \ | |
| --api-key-file "$APPLE_API_KEY_PATH" \ | |
| --wait \ | |
| "$zip_path" | |
| - name: Remove Apple signing files | |
| if: ${{ always() && contains(matrix.settings.target, 'apple-darwin') }} | |
| shell: bash | |
| run: rm -f "${APPLE_CERT_PATH:-}" "${APPLE_API_KEY_PATH:-}" | |
| - name: Upload Artifacts | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: turbo-${{ matrix.settings.target }} | |
| path: target/${{ matrix.settings.target }}/release-turborepo/turbo* | |
| npm-publish: | |
| name: "Publish To NPM" | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| id-token: write # Required for npm Trusted Publishing using OIDC | |
| env: | |
| NPM_CONFIG_PROVENANCE: "true" | |
| # TODO: Add rust-smoke-test back to needs and if-condition when re-enabled. | |
| needs: [stage, build-rust, js-smoke-test] | |
| if: ${{ always() && needs.stage.result == 'success' && needs.build-rust.result == 'success' && needs.js-smoke-test.result == 'success' }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ needs.stage.outputs.stage-branch }} | |
| fetch-depth: 2 | |
| filter: blob:none | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-node | |
| with: | |
| enable-corepack: false | |
| extra-flags: "--no-frozen-lockfile" | |
| - name: Install Global Turbo | |
| uses: ./.github/actions/install-global-turbo | |
| with: | |
| turbo-version: ${{ inputs.ci-tag-override || '' }} | |
| - name: Download Rust artifacts | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| path: rust-artifacts | |
| - name: Move Rust artifacts into place | |
| run: | | |
| mkdir release-artifacts | |
| mv rust-artifacts/turbo-aarch64-apple-darwin release-artifacts/dist-darwin-arm64 | |
| mv rust-artifacts/turbo-aarch64-unknown-linux-musl release-artifacts/dist-linux-arm64 | |
| cp -r rust-artifacts/turbo-x86_64-pc-windows-msvc release-artifacts/dist-windows-arm64 | |
| mv rust-artifacts/turbo-x86_64-unknown-linux-musl release-artifacts/dist-linux-x64 | |
| mv rust-artifacts/turbo-x86_64-apple-darwin release-artifacts/dist-darwin-x64 | |
| mv rust-artifacts/turbo-x86_64-pc-windows-msvc release-artifacts/dist-windows-x64 | |
| - name: Ensure npm version | |
| run: npm install -g npm@11.5.1 | |
| - name: Build release tool | |
| run: turbo run build --filter=@turbo/releaser | |
| - name: Perform Release | |
| run: | | |
| RELEASE_ARGS=() | |
| if [ "${{ inputs.dry_run }}" = "true" ]; then | |
| RELEASE_ARGS+=("--skip-publish") | |
| fi | |
| packages/turbo-releaser/dist/index.js publish \ | |
| --repo-root . \ | |
| --artifacts-dir release-artifacts \ | |
| --version-path version.txt \ | |
| "${RELEASE_ARGS[@]}" | |
| # Upload published artifacts in case they are needed for debugging later | |
| - name: Upload Artifacts | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: turbo-combined | |
| path: release-artifacts/dist | |
| create-release-tag: | |
| name: "Create Release Tag" | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| needs: [stage, npm-publish] | |
| if: ${{ always() && !inputs.dry_run && needs.npm-publish.result == 'success' }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ needs.stage.outputs.stage-branch }} | |
| filter: blob:none | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-node | |
| with: | |
| enable-corepack: false | |
| extra-flags: "--no-frozen-lockfile" | |
| - name: Build release tool | |
| run: pnpm --filter @turbo/releaser build | |
| - name: Create and push release tag | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh auth setup-git | |
| packages/turbo-releaser/dist/index.js tag --repo-root . --version-path version.txt | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.stage.outputs.version }} | |
| PREVIOUS_TAG: ${{ needs.stage.outputs.previous-tag }} | |
| run: | | |
| PRERELEASE_FLAG="" | |
| if [[ "$VERSION" == *"-canary."* ]]; then | |
| PRERELEASE_FLAG=("--prerelease") | |
| else | |
| PRERELEASE_FLAG=() | |
| fi | |
| NOTES_START_TAG_FLAG=() | |
| if [ -n "$PREVIOUS_TAG" ]; then | |
| NOTES_START_TAG_FLAG=("--notes-start-tag" "$PREVIOUS_TAG") | |
| fi | |
| gh release create "v${VERSION}" \ | |
| --title "Turborepo v${VERSION}" \ | |
| --generate-notes \ | |
| "${NOTES_START_TAG_FLAG[@]}" \ | |
| "${PRERELEASE_FLAG[@]}" | |
| publish-vscode-extension: | |
| name: "Publish VS Code Extension" | |
| needs: [stage, js-smoke-test, create-release-tag] | |
| if: ${{ always() && needs.stage.result == 'success' && needs.js-smoke-test.result == 'success' && needs.create-release-tag.result == 'success' && !inputs.dry_run }} | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/lsp.yml | |
| with: | |
| publish: true | |
| dry_run: false | |
| ref: v${{ needs.stage.outputs.version }} | |
| secrets: inherit | |
| alias-versioned-docs: | |
| name: "Alias Versioned Docs" | |
| runs-on: ubuntu-24.04 | |
| 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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ needs.stage.outputs.stage-branch }} | |
| filter: blob:none | |
| persist-credentials: false | |
| - 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@53.1.0 | |
| - name: Find Vercel deployment for SHA | |
| id: find-deployment | |
| env: | |
| BASE_SHA: ${{ needs.stage.outputs.base-sha }} | |
| VERCEL_TOKEN: ${{ secrets.TURBO_TOKEN }} | |
| run: | | |
| SHA="${BASE_SHA}" | |
| VERCEL_LIST_STDERR="${RUNNER_TEMP}/vercel-list.stderr" | |
| if ! VERCEL_LIST_OUTPUT=$(vercel list turbo-site --scope=vercel -m githubCommitSha="${SHA}" --status=READY --token="${VERCEL_TOKEN}" 2>"${VERCEL_LIST_STDERR}"); then | |
| echo "::error::Failed to list Vercel deployments for SHA ${SHA}. Vercel CLI stderr was captured without printing to logs." | |
| exit 1 | |
| fi | |
| DEPLOYMENT_URL=$(printf '%s\n' "${VERCEL_LIST_OUTPUT}" | grep -E '^\S+\.vercel\.(app|sh)' | head -n 1 | awk '{print $1}' || true) | |
| 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@007b2c3c751a190b6f0f040e47ed024deaa72844 # 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 }} | |
| update-examples: | |
| name: "Update Examples" | |
| needs: [stage, create-release-tag] | |
| if: ${{ always() && needs.create-release-tag.result == 'success' && !contains(needs.stage.outputs.version, '-canary.') }} | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| uses: ./.github/workflows/update-examples-on-release.yml | |
| 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-24.04 | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: ${{ needs.stage.outputs.stage-branch }} | |
| filter: blob:none | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-node | |
| with: | |
| enable-corepack: false | |
| extra-flags: "--no-frozen-lockfile" | |
| - name: Build release tool | |
| run: pnpm --filter @turbo/releaser build | |
| - name: Commit lockfile if updated by install | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| if git diff --quiet pnpm-lock.yaml; then | |
| echo "Lockfile already up to date." | |
| else | |
| node scripts/create-github-api-commit.mjs \ | |
| --branch "${{ needs.stage.outputs.stage-branch }}" \ | |
| --message "Update lockfile for release" \ | |
| --path pnpm-lock.yaml \ | |
| --if-exists update | |
| fi | |
| - name: Bump to next canary for stable releases | |
| id: next-canary | |
| 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..." | |
| packages/turbo-releaser/dist/index.js version \ | |
| --version-path version.txt \ | |
| --increment prepatch | |
| cat version.txt | |
| echo "updated=true" >> $GITHUB_OUTPUT | |
| echo "version=${VERSION}" >> $GITHUB_OUTPUT | |
| else | |
| echo "Pre-release ($TAG), skipping canary bump." | |
| echo "updated=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Commit next canary bump | |
| if: ${{ steps.next-canary.outputs.updated == 'true' }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| node scripts/create-github-api-commit.mjs \ | |
| --branch "${{ needs.stage.outputs.stage-branch }}" \ | |
| --message "bump to next canary after ${{ steps.next-canary.outputs.version }}" \ | |
| --all-tracked \ | |
| --if-exists update | |
| - 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 | |
| cleanup-on-failure: | |
| name: "Cleanup Failed Release" | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| # TODO: Add rust-smoke-test back to needs and if-condition when re-enabled. | |
| needs: | |
| [ | |
| stage, | |
| build-rust, | |
| js-smoke-test, | |
| npm-publish, | |
| create-release-tag, | |
| create-release-pr | |
| ] | |
| if: >- | |
| ${{ | |
| always() | |
| && needs.stage.result == 'success' | |
| && ( | |
| needs.build-rust.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 }}" | |
| if [[ ! "$STAGE_BRANCH" =~ ^staging-[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then | |
| echo "::error::Refusing to delete unexpected staging branch: $STAGE_BRANCH" | |
| exit 1 | |
| fi | |
| 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 }}" | |
| if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then | |
| echo "::error::Refusing to delete unexpected release tag version: $VERSION" | |
| exit 1 | |
| fi | |
| 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." |