Repository navigation
fix(stack): keep port registry reads open while a peer commits (#7342) #12588
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
| name: Release | |
| on: | |
| push: | |
| branches: | |
| - develop | |
| - main | |
| - next | |
| # workflow_dispatch is the manual re-cut path. Use it when: | |
| # - a previous release published stale bytes under a version that | |
| # semantic-release keeps re-computing (cut a fresh version forward), or | |
| # - a downstream step (GH release, brew, scoop) failed after npm published | |
| # and you need to rerun the whole pipeline against an explicit version. | |
| # An empty version lets semantic-release compute it from the dispatched ref. `maintenance` | |
| # releases a v<N>.x line and must be dispatched on that ref. | |
| workflow_dispatch: | |
| inputs: | |
| channel: | |
| description: Release channel | |
| required: true | |
| type: choice | |
| options: | |
| - beta | |
| - stable | |
| - next | |
| - maintenance | |
| version: | |
| description: npm package version to publish (must be unique on npm). Leave empty to let semantic-release compute it from the dispatched ref. | |
| required: false | |
| type: string | |
| default: "" | |
| # Defaults to `true` so a stray "Run workflow" click can't accidentally publish; operators | |
| # must consciously untick this when dispatching. | |
| dry_run: | |
| description: Dry run (skip actual publishing) | |
| required: false | |
| type: boolean | |
| default: true | |
| # The release pipeline mutates external registries, git tags, GitHub Releases, Homebrew, and | |
| # Scoop, so keep one release per ref active at a time to avoid duplicate push events racing. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| test-next: | |
| name: Test next | |
| if: github.event_name == 'push' && github.ref_name == 'next' | |
| permissions: | |
| contents: read | |
| actions: read | |
| uses: ./.github/workflows/test.yml | |
| with: | |
| force: true | |
| secrets: | |
| DF_FIREWALL_TOKEN: ${{ secrets.DF_FIREWALL_TOKEN }} | |
| plan: | |
| name: Plan release | |
| needs: test-next | |
| # A skipped test-next (every non-next trigger) makes the implicit success() false, so the | |
| # result is checked explicitly. | |
| if: ${{ !cancelled() && contains(fromJSON('["success","skipped"]'), needs.test-next.result) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| outputs: | |
| should_release: ${{ steps.compute.outputs.should_release }} | |
| version: ${{ steps.compute.outputs.version }} | |
| npm_tag: ${{ steps.compute.outputs.npm_tag }} | |
| prerelease: ${{ steps.compute.outputs.prerelease }} | |
| brew_name: ${{ steps.compute.outputs.brew_name }} | |
| scoop_name: ${{ steps.compute.outputs.scoop_name }} | |
| publish_brew_scoop: ${{ steps.compute.outputs.publish_brew_scoop }} | |
| dry_run: ${{ steps.compute.outputs.dry_run }} | |
| channel: ${{ steps.compute.outputs.channel }} | |
| steps: | |
| # semantic-release runs `git push --dry-run HEAD:<branch>` as part of verifyAuth even in | |
| # `dry_run: true` mode, so the token needs push access to the protected develop/main | |
| # branches — the default GITHUB_TOKEN doesn't have it. | |
| - id: app-token | |
| if: github.event_name == 'push' || inputs.version == '' | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ vars.GH_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} | |
| permission-contents: write | |
| # `persist-credentials: false` is required: otherwise checkout caches the default | |
| # GITHUB_TOKEN in git config, and its Authorization header overrides the App token | |
| # semantic-release puts in the push URL, making the dry-push get rejected by branch protection. | |
| - uses: useblacksmith/checkout@6fd481652155169ed4d2f25ebaf97464f685175f # v1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - id: semantic-release | |
| if: github.event_name == 'push' || inputs.version == '' | |
| uses: cycjimmy/semantic-release-action@b12c8f6015dc215fe37bc154d4ad456dd3833c90 # v6.0.0 | |
| with: | |
| working_directory: apps/cli | |
| dry_run: true | |
| env: | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} | |
| - id: compute | |
| env: | |
| EVENT: ${{ github.event_name }} | |
| REF_NAME: ${{ github.ref_name }} | |
| DISPATCH_CHANNEL: ${{ inputs.channel }} | |
| DISPATCH_VERSION: ${{ inputs.version }} | |
| DISPATCH_DRY_RUN: ${{ inputs.dry_run }} | |
| SR_PUBLISHED: ${{ steps.semantic-release.outputs.new_release_published }} | |
| SR_VERSION: ${{ steps.semantic-release.outputs.new_release_version }} | |
| run: | | |
| set -euo pipefail | |
| maint_re='^v([0-9]+)\.x$' | |
| version_re='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$' | |
| plain_re='^[0-9]+\.[0-9]+\.[0-9]+$' | |
| next_re='^[0-9]+\.[0-9]+\.[0-9]+-next\.[0-9]+$' | |
| beta_re='^[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$' | |
| natural="" | |
| case "$REF_NAME" in | |
| develop) natural=beta ;; | |
| main) natural=stable ;; | |
| next) natural=next ;; | |
| *) [[ "$REF_NAME" =~ $maint_re ]] && natural=maintenance ;; | |
| esac | |
| if [[ "$EVENT" == "workflow_dispatch" ]]; then | |
| channel="$DISPATCH_CHANNEL" | |
| dry_run="$DISPATCH_DRY_RUN" | |
| if [[ "$natural" == maintenance && "$channel" != maintenance ]] || [[ "$natural" != maintenance && "$channel" == maintenance ]]; then | |
| echo "::error::channel=maintenance is only valid on v<N>.x refs, and v<N>.x refs only release the maintenance channel" | |
| exit 1 | |
| fi | |
| if [[ "$channel" == next && "$REF_NAME" != next ]]; then | |
| echo "::error::channel=next must be dispatched on the next branch" | |
| exit 1 | |
| fi | |
| if [[ "$natural" == next && "$channel" != next ]]; then | |
| echo "::error::${REF_NAME} only releases the next channel" | |
| exit 1 | |
| fi | |
| if [[ "$channel" == stable && "$dry_run" != true && "$REF_NAME" != main ]]; then | |
| echo "::error::A stable release must be dispatched on main; other refs may only dry-run" | |
| exit 1 | |
| fi | |
| if [[ -n "$DISPATCH_VERSION" ]]; then | |
| if [[ ! "$DISPATCH_VERSION" =~ $version_re ]]; then | |
| echo "::error::version ${DISPATCH_VERSION} is not a semver version" | |
| exit 1 | |
| fi | |
| case "$channel" in | |
| next) channel_re="$next_re"; shape="X.Y.Z-next.N" ;; | |
| beta) channel_re="$beta_re"; shape="X.Y.Z-beta.N" ;; | |
| *) channel_re="$plain_re"; shape="X.Y.Z" ;; | |
| esac | |
| if [[ ! "$DISPATCH_VERSION" =~ $channel_re ]]; then | |
| echo "::error::version ${DISPATCH_VERSION} does not match channel ${channel} (expected ${shape})" | |
| exit 1 | |
| fi | |
| version="$DISPATCH_VERSION" | |
| else | |
| if [[ "$channel" != "$natural" ]]; then | |
| echo "::error::An empty version uses semantic-release, so channel must match ${REF_NAME} (${natural:-unconfigured})" | |
| exit 1 | |
| fi | |
| if [[ "$SR_PUBLISHED" != "true" ]]; then | |
| echo "::error::semantic-release computed no release for ${REF_NAME}" | |
| exit 1 | |
| fi | |
| version="$SR_VERSION" | |
| fi | |
| should_release=true | |
| else | |
| dry_run=false | |
| if [[ "$SR_PUBLISHED" != "true" ]]; then | |
| echo "should_release=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| version="$SR_VERSION" | |
| channel="$natural" | |
| should_release=true | |
| fi | |
| case "$channel" in | |
| beta) | |
| npm_tag=beta | |
| prerelease=true | |
| brew_name=supabase-beta | |
| scoop_name=supabase-beta | |
| publish_brew_scoop=true | |
| ;; | |
| stable) | |
| npm_tag=latest | |
| prerelease=false | |
| brew_name=supabase | |
| scoop_name=supabase | |
| publish_brew_scoop=true | |
| ;; | |
| next) | |
| npm_tag=next | |
| prerelease=true | |
| brew_name="" | |
| scoop_name="" | |
| publish_brew_scoop=false | |
| ;; | |
| maintenance) | |
| [[ "$REF_NAME" =~ $maint_re ]] | |
| major="${BASH_REMATCH[1]}" | |
| if [[ "${version%%.*}" != "$major" ]]; then | |
| echo "::error::version ${version} is not on the v${major} line" | |
| exit 1 | |
| fi | |
| npm_tag="v${major}.stable" | |
| prerelease=false | |
| brew_name="supabase@${major}" | |
| scoop_name="supabase-v${major}-stable" | |
| publish_brew_scoop=true | |
| ;; | |
| *) | |
| echo "Unknown channel: $channel" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| { | |
| echo "should_release=$should_release" | |
| echo "version=$version" | |
| echo "npm_tag=$npm_tag" | |
| echo "prerelease=$prerelease" | |
| echo "brew_name=$brew_name" | |
| echo "scoop_name=$scoop_name" | |
| echo "publish_brew_scoop=$publish_brew_scoop" | |
| echo "dry_run=$dry_run" | |
| echo "channel=$channel" | |
| } >> "$GITHUB_OUTPUT" | |
| release: | |
| name: Release | |
| needs: plan | |
| if: ${{ !cancelled() && needs.plan.result == 'success' && needs.plan.outputs.should_release == 'true' }} | |
| # pull-requests: write is required by the nested propose-release-notes workflow | |
| # (release-shared.yml -> propose-release-notes.yml): a called job's permissions can't exceed | |
| # those granted to the calling job, so this must be declared here too. | |
| permissions: | |
| actions: read | |
| contents: write | |
| id-token: write | |
| pull-requests: write | |
| uses: ./.github/workflows/release-shared.yml | |
| with: | |
| version: ${{ needs.plan.outputs.version }} | |
| npm_tag: ${{ needs.plan.outputs.npm_tag }} | |
| prerelease: ${{ needs.plan.outputs.prerelease == 'true' }} | |
| publish_brew_scoop: ${{ needs.plan.outputs.publish_brew_scoop == 'true' }} | |
| brew_name: ${{ needs.plan.outputs.brew_name }} | |
| scoop_name: ${{ needs.plan.outputs.scoop_name }} | |
| dry_run: ${{ needs.plan.outputs.dry_run == 'true' }} | |
| channel: ${{ needs.plan.outputs.channel }} | |
| secrets: | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} | |
| POSTHOG_ENDPOINT: ${{ secrets.POSTHOG_ENDPOINT }} | |
| GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| DF_FIREWALL_TOKEN: ${{ secrets.DF_FIREWALL_TOKEN }} | |
| LINEAR_CLI_STABLE_RELEASE_ACCESS_KEY: ${{ secrets.LINEAR_CLI_STABLE_RELEASE_ACCESS_KEY }} | |
| LINEAR_CLI_BETA_RELEASE_ACCESS_KEY: ${{ secrets.LINEAR_CLI_BETA_RELEASE_ACCESS_KEY }} | |
| SUPABASE_E2E_CLI_LIVE_STAGING_ACCESS_TOKEN: ${{ secrets.SUPABASE_E2E_CLI_LIVE_STAGING_ACCESS_TOKEN }} | |
| # Republishes the CLI reference and the config schemas to supabase.com. Stable only: both | |
| # document the CLI users actually install, so pre-releases and maintenance lines are skipped. | |
| # Nothing depends on this job, so a docs-site failure cannot affect the already-completed | |
| # release; a failed publish reaches Slack through notify-slack-publish-failure. | |
| docs: | |
| name: Publish to supabase.com | |
| needs: [plan, release] | |
| if: ${{ !cancelled() && needs.release.result == 'success' && needs.plan.outputs.channel == 'stable' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| continue-on-error: true | |
| outputs: | |
| failed: ${{ steps.report.outputs.failed }} | |
| steps: | |
| # Scoped to supabase/supabase: this job pushes a branch and opens a PR | |
| # there, and needs nothing beyond a plain read-only checkout here. | |
| - id: app-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ vars.GH_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} | |
| owner: ${{ github.repository_owner }} | |
| repositories: | | |
| supabase | |
| permission-contents: write | |
| permission-pull-requests: write | |
| - uses: useblacksmith/checkout@6fd481652155169ed4d2f25ebaf97464f685175f # v1 | |
| with: | |
| persist-credentials: false | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} | |
| - name: Authenticate git for the docs push | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: gh auth setup-git | |
| - name: Publish the CLI reference | |
| id: reference | |
| continue-on-error: true | |
| working-directory: apps/cli | |
| # `shell: bash` for `-o pipefail`: the default `run:` shell takes only the publisher's | |
| # exit status, so a generator that died part-way would look like a successful run. | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| VERSION: ${{ needs.plan.outputs.version }} | |
| DRY_RUN: ${{ needs.plan.outputs.dry_run }} | |
| run: | | |
| args=() | |
| if [[ "$DRY_RUN" == "true" ]]; then | |
| args+=(--dry-run) | |
| fi | |
| bun scripts/generate-docs-spec.ts "$VERSION" \ | |
| | bun scripts/publish-docs-spec.ts --version "$VERSION" "${args[@]}" | |
| - name: Publish the config schemas | |
| id: schemas | |
| continue-on-error: true | |
| working-directory: apps/cli | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| VERSION: ${{ needs.plan.outputs.version }} | |
| DRY_RUN: ${{ needs.plan.outputs.dry_run }} | |
| run: | | |
| args=() | |
| if [[ "$DRY_RUN" == "true" ]]; then | |
| args+=(--dry-run) | |
| fi | |
| bun scripts/publish-schemas.ts --version "$VERSION" "${args[@]}" | |
| - name: Report failed publishes | |
| id: report | |
| if: ${{ !cancelled() }} | |
| env: | |
| REFERENCE_OUTCOME: ${{ steps.reference.outcome }} | |
| SCHEMAS_OUTCOME: ${{ steps.schemas.outcome }} | |
| run: | | |
| failed=false | |
| report() { | |
| if [[ "$2" != "success" ]]; then | |
| failed=true | |
| echo "::error title=Publishing the $1 failed::The $1 was not published to supabase.com (step outcome: $2)." | |
| echo "- The $1 was not published to supabase.com (step outcome: $2)." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| } | |
| report "CLI reference" "$REFERENCE_OUTCOME" | |
| report "config schemas" "$SCHEMAS_OUTCOME" | |
| echo "failed=$failed" >> "$GITHUB_OUTPUT" | |
| if [[ "$failed" == "true" ]]; then | |
| exit 1 | |
| fi | |
| # Notifies real (non-dry-run) stable and maintenance cuts. Nothing depends on this job, so a | |
| # Slack failure can't affect the already-completed release. | |
| notify-slack: | |
| name: Notify Slack | |
| needs: [plan, release] | |
| if: >- | |
| ${{ !cancelled() && | |
| needs.release.result == 'success' && | |
| needs.plan.outputs.dry_run != 'true' && | |
| (needs.plan.outputs.channel == 'stable' || needs.plan.outputs.channel == 'maintenance') }} | |
| uses: ./.github/workflows/slack-notify.yml | |
| with: | |
| package: Supabase CLI | |
| status: success | |
| version: ${{ needs.plan.outputs.version }} | |
| channel: ${{ needs.plan.outputs.channel }} | |
| secrets: | |
| SLACK_RELEASE_WEBHOOK: ${{ secrets.SLACK_RELEASE_WEBHOOK }} | |
| # Pages when the release shipped but the supabase.com publish did not. The docs job's | |
| # continue-on-error hides its own failures, so its report step exports `failed`; a job that ran | |
| # without exporting `failed=false` (timeout, crash before the report step) counts as failed. | |
| notify-slack-publish-failure: | |
| name: Notify Slack (publish failure) | |
| needs: [plan, docs] | |
| if: ${{ !cancelled() && needs.plan.outputs.dry_run != 'true' && needs.docs.result != 'skipped' && needs.docs.outputs.failed != 'false' }} | |
| uses: ./.github/workflows/slack-notify.yml | |
| with: | |
| package: Supabase CLI | |
| status: publish-failure | |
| version: ${{ needs.plan.outputs.version }} | |
| channel: ${{ needs.plan.outputs.channel }} | |
| secrets: | |
| SLACK_RELEASE_WEBHOOK: ${{ secrets.SLACK_RELEASE_WEBHOOK }} | |
| # Reports a broken release on every channel. `failure()` evaluates against the full `needs` | |
| # chain, so this fires whenever `test-next`, `plan` or `release` (including the reusable | |
| # release-shared workflow) fails. Skipped jobs don't count as failures. Dry runs are excluded | |
| # via the dispatch input directly, so even a plan job that dies before recording its `dry_run` | |
| # output stays quiet. A failed `test-next` skips `plan`, so the channel falls back to the ref. | |
| notify-slack-failure: | |
| name: Notify Slack (failure) | |
| needs: [test-next, plan, release] | |
| if: ${{ failure() && !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }} | |
| uses: ./.github/workflows/slack-notify.yml | |
| with: | |
| package: Supabase CLI | |
| status: failure | |
| version: ${{ needs.plan.outputs.version }} | |
| channel: ${{ needs.plan.outputs.channel || (github.ref_name == 'next' && 'next') || '' }} | |
| secrets: | |
| SLACK_RELEASE_WEBHOOK: ${{ secrets.SLACK_RELEASE_WEBHOOK }} |