diff --git a/.github/workflows/turborepo-canary.yml b/.github/workflows/turborepo-canary.yml deleted file mode 100644 index 6df75ca8b5416..0000000000000 --- a/.github/workflows/turborepo-canary.yml +++ /dev/null @@ -1,168 +0,0 @@ -# Canary Release Pipeline -# -# Automatically publishes a canary release when PRs merge to main. -# -# Key behaviors: -# - Triggers on push to main (when relevant paths change) -# - Skips if the push is from a release PR merge (bot actor + release commit message) -# - Uses GitHub's concurrency to queue/squash multiple rapid merges -# - Calls the release workflow, then opens an auto-merging PR - -name: Canary Release - -permissions: - id-token: write - contents: write - pull-requests: write - -on: - push: - branches: [main] - paths: - - "crates/**" - - "packages/**" - - "cli/**" - - ".github/workflows/**" - - ".github/actions/**" - -concurrency: - group: canary-release - cancel-in-progress: false - -jobs: - check-skip: - name: "Check Skip Conditions" - runs-on: ubuntu-latest - outputs: - should_skip: ${{ steps.check.outputs.should_skip }} - steps: - - name: Check if release PR merge - id: check - env: - ACTOR: ${{ github.actor }} - COMMIT_MESSAGE: ${{ github.event.head_commit.message }} - run: | - if [[ "$ACTOR" == "github-actions[bot]" && "$COMMIT_MESSAGE" =~ ^release\(turborepo\): ]]; then - echo "Skipping: This is a release PR merge" - echo "should_skip=true" >> $GITHUB_OUTPUT - else - echo "should_skip=false" >> $GITHUB_OUTPUT - fi - - release: - needs: [check-skip] - if: ${{ needs.check-skip.outputs.should_skip != 'true' }} - uses: ./.github/workflows/turborepo-release.yml - with: - increment: prerelease - is-canary: true - secrets: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - DOCS_ALIAS_FAILURE_SLACK_WEBHOOK_URL: ${{ secrets.DOCS_ALIAS_FAILURE_SLACK_WEBHOOK_URL }} - - create-canary-pr: - name: "Open Canary Release PR" - needs: [check-skip, release] - if: ${{ needs.check-skip.outputs.should_skip != 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.release.outputs.stage-branch }} - fetch-depth: 0 - - - name: Fetch main and tags - run: git fetch origin main --tags - - - name: Validate version format - env: - VERSION: ${{ needs.release.outputs.version }} - run: | - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then - echo "::error::Invalid version format: $VERSION" - exit 1 - fi - - - name: Build PR Body - env: - PREVIOUS_TAG: ${{ needs.release.outputs.previous-tag }} - VERSION: ${{ needs.release.outputs.version }} - DOCS_URL: ${{ needs.release.outputs.docs-url }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - echo "## Canary Release" > pr-body.md - echo "" >> pr-body.md - - if [ -n "$DOCS_URL" ]; then - echo "Versioned docs: ${DOCS_URL}" >> pr-body.md - echo "" >> pr-body.md - fi - - echo "### Included Changes" >> pr-body.md - echo "" >> pr-body.md - - if [ -n "$PREVIOUS_TAG" ]; then - git log ${PREVIOUS_TAG}..origin/main --pretty=format:"%H %s" | while read -r line; do - SHA=$(echo "$line" | cut -d' ' -f1) - SHORT_SHA=$(echo "$SHA" | cut -c1-7) - MESSAGE=$(echo "$line" | cut -d' ' -f2-) - PR_NUM=$(gh api "/repos/${{ github.repository }}/commits/${SHA}/pulls" --jq '.[0].number' 2>/dev/null || echo "") - - if [ -n "$PR_NUM" ]; then - echo "- ${SHORT_SHA} - ${MESSAGE} (#${PR_NUM})" >> pr-body.md - else - echo "- ${SHORT_SHA} - ${MESSAGE}" >> pr-body.md - fi - done - else - echo "No previous tag found. This is the first canary release." >> pr-body.md - fi - - echo "" >> pr-body.md - echo "---" >> pr-body.md - echo "Release PR for turborepo v${VERSION}" >> pr-body.md - - - name: Create pull request with auto-merge - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ needs.release.outputs.version }} - STAGE_BRANCH: ${{ needs.release.outputs.stage-branch }} - run: | - MAX_RETRIES=3 - RETRY_DELAY=10 - - for i in $(seq 1 $MAX_RETRIES); do - if PR_URL=$(gh pr create \ - --title "release(turborepo): ${VERSION}" \ - --body-file pr-body.md \ - --head "${STAGE_BRANCH}" \ - --base main 2>&1); then - break - fi - echo "PR creation attempt $i failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - done - - if [ -z "$PR_URL" ] || [[ ! "$PR_URL" =~ ^https://github.com/.*/pull/[0-9]+$ ]]; then - echo "::error::Failed to create PR after $MAX_RETRIES attempts. Output: $PR_URL" - exit 1 - fi - - echo "Created PR: $PR_URL" - PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$') - - MERGE_SUCCESS=false - for i in $(seq 1 $MAX_RETRIES); do - if gh pr merge "$PR_NUM" --auto --squash; then - MERGE_SUCCESS=true - break - fi - echo "Auto-merge attempt $i failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - done - - if [ "$MERGE_SUCCESS" != "true" ]; then - echo "::warning::Failed to enable auto-merge after $MAX_RETRIES attempts. PR created but requires manual merge: $PR_URL" - fi diff --git a/.github/workflows/turborepo-release.yml b/.github/workflows/turborepo-release.yml index 3c713fcfee390..f5a7304ab8a59 100644 --- a/.github/workflows/turborepo-release.yml +++ b/.github/workflows/turborepo-release.yml @@ -8,8 +8,9 @@ # 4. Publish JS packages npm (including turbo itself) # 5. Alias versioned docs (e.g., v2-5-4.turborepo.dev) # 6. Create a release branch and open a PR. - -# You can opt into a dry run, which will skip publishing to npm and opening the release branch +# +# Canary releases are triggered automatically on push to main. +# Manual releases are triggered via workflow_dispatch. name: Release @@ -24,6 +25,14 @@ permissions: pull-requests: write # Allows the PR for post-release to be created on: + push: + branches: [main] + paths: + - "crates/**" + - "packages/**" + - "cli/**" + - ".github/workflows/**" + - ".github/actions/**" workflow_dispatch: inputs: increment: @@ -64,57 +73,36 @@ on: type: string default: "" - workflow_call: - inputs: - increment: - required: true - type: string - dry_run: - required: false - type: boolean - default: false - tag-override: - required: false - type: string - default: "" - ci-tag-override: - required: false - type: string - default: "" - sha: - required: false - type: string - default: "" - is-canary: - required: false - type: boolean - default: false - secrets: - NPM_TOKEN: - required: true - TURBO_TOKEN: - required: true - DOCS_ALIAS_FAILURE_SLACK_WEBHOOK_URL: - required: true - outputs: - stage-branch: - description: "The staging branch name" - value: ${{ jobs.stage.outputs.stage-branch }} - version: - description: "The released version" - value: ${{ jobs.stage.outputs.version }} - previous-tag: - description: "The previous release tag" - value: ${{ jobs.stage.outputs.previous-tag }} - docs-url: - description: "The versioned docs URL" - value: ${{ jobs.alias-versioned-docs.outputs.docs_url }} - docs-success: - description: "Whether docs aliasing succeeded" - value: ${{ jobs.alias-versioned-docs.outputs.success }} +# Canary releases queue up (rapid merges to main get batched). +# Manual releases each get their own concurrency group. +concurrency: + group: ${{ github.event_name == 'push' && 'canary-release' || format('release-{0}', github.run_id) }} + cancel-in-progress: false jobs: + check-skip: + name: "Check Skip Conditions" + runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' }} + outputs: + should_skip: ${{ steps.check.outputs.should_skip }} + steps: + - name: Check if release PR merge + id: check + env: + ACTOR: ${{ github.actor }} + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + run: | + if [[ "$ACTOR" == "github-actions[bot]" && "$COMMIT_MESSAGE" =~ ^release\(turborepo\): ]]; then + echo "Skipping: This is a release PR merge" + 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: @@ -132,10 +120,6 @@ jobs: with: enable-corepack: false - - name: Pull latest main (for canary releases) - if: ${{ inputs.is-canary }} - run: git pull origin main - - name: Configure git run: | git config --global user.name 'Turbobot' @@ -158,7 +142,8 @@ jobs: - name: Version id: version env: - INCREMENT: ${{ inputs.increment }} + # For push events (canary), always use prerelease. For workflow_dispatch, use the input. + INCREMENT: ${{ github.event_name == 'push' && 'prerelease' || inputs.increment }} TAG_OVERRIDE: ${{ inputs.tag-override }} run: | if [[ -n "$TAG_OVERRIDE" && ! "$TAG_OVERRIDE" =~ ^[a-zA-Z0-9-]+$ ]]; then @@ -436,7 +421,7 @@ jobs: create-release-pr: name: "Open Release Branch PR" needs: [stage, npm-publish, alias-versioned-docs] - if: ${{ always() && needs.npm-publish.result == 'success' && !inputs.is-canary }} + if: ${{ always() && needs.npm-publish.result == 'success' && github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -470,3 +455,109 @@ jobs: base: main title: "release(turborepo): ${{ steps.getVersion.outputs.version }}" body-path: pr-body.md + + create-canary-pr: + name: "Open Canary Release PR" + needs: [stage, npm-publish, alias-versioned-docs] + if: ${{ always() && needs.npm-publish.result == 'success' && github.event_name == 'push' }} + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.stage.outputs.stage-branch }} + fetch-depth: 0 + + - name: Fetch main and tags + run: git fetch origin main --tags + + - name: Validate version format + run: | + VERSION="${{ needs.stage.outputs.version }}" + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "::error::Invalid version format: $VERSION" + exit 1 + fi + + - name: Build PR Body + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PREVIOUS_TAG="${{ needs.stage.outputs.previous-tag }}" + VERSION="${{ needs.stage.outputs.version }}" + DOCS_URL="${{ needs.alias-versioned-docs.outputs.docs_url }}" + + echo "## Canary Release" > pr-body.md + echo "" >> pr-body.md + + if [ -n "$DOCS_URL" ]; then + echo "Versioned docs: ${DOCS_URL}" >> pr-body.md + echo "" >> pr-body.md + fi + + echo "### Included Changes" >> pr-body.md + echo "" >> pr-body.md + + if [ -n "$PREVIOUS_TAG" ]; then + git log ${PREVIOUS_TAG}..origin/main --pretty=format:"%H %s" | while read -r line; do + SHA=$(echo "$line" | cut -d' ' -f1) + SHORT_SHA=$(echo "$SHA" | cut -c1-7) + MESSAGE=$(echo "$line" | cut -d' ' -f2-) + PR_NUM=$(gh api "/repos/${{ github.repository }}/commits/${SHA}/pulls" --jq '.[0].number' 2>/dev/null || echo "") + + if [ -n "$PR_NUM" ]; then + echo "- ${SHORT_SHA} - ${MESSAGE} (#${PR_NUM})" >> pr-body.md + else + echo "- ${SHORT_SHA} - ${MESSAGE}" >> pr-body.md + fi + done + else + echo "No previous tag found. This is the first canary release." >> pr-body.md + fi + + echo "" >> pr-body.md + echo "---" >> pr-body.md + echo "Release PR for turborepo v${VERSION}" >> pr-body.md + + - name: Create pull request with auto-merge + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ needs.stage.outputs.version }}" + STAGE_BRANCH="${{ needs.stage.outputs.stage-branch }}" + MAX_RETRIES=3 + RETRY_DELAY=10 + + for i in $(seq 1 $MAX_RETRIES); do + if PR_URL=$(gh pr create \ + --title "release(turborepo): ${VERSION}" \ + --body-file pr-body.md \ + --head "${STAGE_BRANCH}" \ + --base main 2>&1); then + break + fi + echo "PR creation attempt $i failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + done + + if [ -z "$PR_URL" ] || [[ ! "$PR_URL" =~ ^https://github.com/.*/pull/[0-9]+$ ]]; then + echo "::error::Failed to create PR after $MAX_RETRIES attempts. Output: $PR_URL" + exit 1 + fi + + echo "Created PR: $PR_URL" + PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$') + + MERGE_SUCCESS=false + for i in $(seq 1 $MAX_RETRIES); do + if gh pr merge "$PR_NUM" --auto --squash; then + MERGE_SUCCESS=true + break + fi + echo "Auto-merge attempt $i failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + done + + if [ "$MERGE_SUCCESS" != "true" ]; then + echo "::warning::Failed to enable auto-merge after $MAX_RETRIES attempts. PR created but requires manual merge: $PR_URL" + fi