Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions .github/workflows/release-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ on:
required: false
type: string
default: supabase
publish_winget:
description: Whether to submit a Winget manifest PR
required: false
type: boolean
default: false
winget_package_id:
description: Winget package identifier
required: false
type: string
default: Supabase.CLI
secrets:
SENTRY_DSN:
required: false
Expand All @@ -55,6 +65,8 @@ on:
required: false
DF_FIREWALL_TOKEN:
required: false
WINGET_CREATE_GITHUB_TOKEN:
required: false
jobs:
build-blacksmith:
name: Build CLI artifacts (Blacksmith)
Expand Down Expand Up @@ -536,20 +548,44 @@ jobs:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}

publish-winget:
needs: publish
if: ${{ !inputs.dry_run && inputs.publish_winget }}
runs-on: windows-2025
env:
VERSION: ${{ inputs.version }}
WINGET_PACKAGE_ID: ${{ inputs.winget_package_id }}
WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Submit Winget manifest
shell: pwsh
run: |
if ([string]::IsNullOrWhiteSpace($env:WINGET_CREATE_GITHUB_TOKEN)) {
Write-Error "WINGET_CREATE_GITHUB_TOKEN is required to submit Winget manifests."
exit 1
fi
./apps/cli/scripts/update-winget.ps1 -Version $env:VERSION -PackageId $env:WINGET_PACKAGE_ID -Submit

# Post-publish smoke test for the `supabase/setup-cli` GitHub Action against
# the just-released CLI. Runs last and intentionally does not gate
# publish-homebrew / publish-scoop — by the time the smoke runs, the npm
# package and GitHub release are already live and the brew/scoop pushes
# publish-homebrew / publish-scoop / publish-winget — by the time the smoke
# runs, the npm package and GitHub release are already live and downstream pushes
# have either succeeded or skipped, so a setup-cli regression surfaces as
# a red post-release signal without holding back the rest of the channel.
#
# Depends on the publish jobs only via `needs` for ordering; the `if`
# uses `always() && needs.publish.result == 'success'` so the smoke still
# runs when publish-homebrew / publish-scoop are skipped (alpha) or fail.
# runs when publish-homebrew / publish-scoop / publish-winget are skipped
# (alpha) or fail.
# The reusable workflow can also be dispatched manually against any
# already-published version when debugging setup-cli regressions.
setup-cli-smoke:
needs: [publish, publish-homebrew, publish-scoop]
needs: [publish, publish-homebrew, publish-scoop, publish-winget]
if: ${{ always() && !inputs.dry_run && needs.publish.result == 'success' }}
uses: ./.github/workflows/setup-cli-smoke-test.yml
with:
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ jobs:
brew_name: ${{ steps.compute.outputs.brew_name }}
scoop_name: ${{ steps.compute.outputs.scoop_name }}
publish_brew_scoop: ${{ steps.compute.outputs.publish_brew_scoop }}
publish_winget: ${{ steps.compute.outputs.publish_winget }}
dry_run: ${{ steps.compute.outputs.dry_run }}
channel: ${{ steps.compute.outputs.channel }}
steps:
Expand Down Expand Up @@ -128,6 +129,7 @@ jobs:
DISPATCH_DRY_RUN: ${{ inputs.dry_run }}
SR_PUBLISHED: ${{ steps.semantic-release.outputs.new_release_published }}
SR_VERSION: ${{ steps.semantic-release.outputs.new_release_version }}
WINGET_PUBLISH_ENABLED: ${{ vars.WINGET_PUBLISH_ENABLED }}
run: |
set -euo pipefail
if [[ "$EVENT" == "workflow_dispatch" ]]; then
Expand Down Expand Up @@ -157,6 +159,7 @@ jobs:
brew_name=""
scoop_name=""
publish_brew_scoop=false
publish_winget=false
;;
beta)
shell=legacy
Expand All @@ -165,6 +168,7 @@ jobs:
brew_name=supabase-beta
scoop_name=supabase-beta
publish_brew_scoop=true
publish_winget=false
;;
stable)
shell=legacy
Expand All @@ -173,6 +177,10 @@ jobs:
brew_name=supabase
scoop_name=supabase
publish_brew_scoop=true
publish_winget=false
if [[ "$WINGET_PUBLISH_ENABLED" == "true" ]]; then
publish_winget=true
fi
;;
*)
echo "Unknown channel: $channel" >&2
Expand All @@ -188,6 +196,7 @@ jobs:
echo "brew_name=$brew_name"
echo "scoop_name=$scoop_name"
echo "publish_brew_scoop=$publish_brew_scoop"
echo "publish_winget=$publish_winget"
echo "dry_run=$dry_run"
echo "channel=$channel"
} >> "$GITHUB_OUTPUT"
Expand All @@ -214,6 +223,7 @@ jobs:
publish_brew_scoop: ${{ needs.plan.outputs.publish_brew_scoop == 'true' }}
brew_name: ${{ needs.plan.outputs.brew_name }}
scoop_name: ${{ needs.plan.outputs.scoop_name }}
publish_winget: ${{ needs.plan.outputs.publish_winget == 'true' }}
dry_run: ${{ needs.plan.outputs.dry_run == 'true' }}
channel: ${{ needs.plan.outputs.channel }}
secrets:
Expand All @@ -223,6 +233,7 @@ jobs:
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
DF_FIREWALL_TOKEN: ${{ secrets.DF_FIREWALL_TOKEN }}
WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }}

# Posts to the release Slack channel once the pipeline succeeds. Listing
# `release` in `needs` without a status function in `if:` keeps the implicit
Expand Down
39 changes: 32 additions & 7 deletions apps/cli/docs/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ This document is the operational playbook for releasing the Supabase CLI TypeScr

1. **Ring 1 — Local Verdaccio.** Fastest feedback loop. Build and install the CLI from a local npm registry on your own machine. No network side-effects; no repo pushes.
2. **Ring 2 — User-owned PoC repos.** End-to-end validation through the exact same Homebrew / Scoop / GitHub-Release code paths production uses, but pointed at a reviewer's own GitHub account and a non-`supabase` artifact name. This is how ADR 0011 gates 2 and 3 are validated without risking the real production channels.
3. **Ring 3 — Production.** The real `supabase` npm package + `supabase/homebrew-tap` + `supabase/scoop-bucket` + GitHub Releases on `supabase/cli`. Driven by GitHub Actions (`release.yml`, which dispatches three channels — `alpha`, `beta`, `stable` — into the shared `release-shared.yml`).
3. **Ring 3 — Production.** The real `supabase` npm package + `supabase/homebrew-tap` + `supabase/scoop-bucket` + a Winget PR to `microsoft/winget-pkgs` + GitHub Releases on `supabase/cli`. Driven by GitHub Actions (`release.yml`, which dispatches three channels — `alpha`, `beta`, `stable` — into the shared `release-shared.yml`).

```mermaid
flowchart LR
local["Ring 1: Local Verdaccio<br/>pnpm cli-release<br/>--next or --legacy"]
poc["Ring 2: User-owned PoC repos<br/>avallete/supabase-cli-release-poc<br/>avallete/homebrew-supabase-shim-poc<br/>avallete/scoop-bucket<br/>--name supabase-shim-poc"]
prod["Ring 3: Production<br/>supabase/cli<br/>supabase/homebrew-tap<br/>supabase/scoop-bucket<br/>(default name: supabase)"]
prod["Ring 3: Production<br/>supabase/cli<br/>supabase/homebrew-tap<br/>supabase/scoop-bucket<br/>microsoft/winget-pkgs PR<br/>(default name: supabase)"]

local --> poc --> prod
```
Expand Down Expand Up @@ -86,7 +86,7 @@ gh auth status # verify: ✓ Logged in to github.com account <you>

### Dry-run: generate artifacts without pushing

The `--dry-run` flag on both updater scripts produces the `Formula/<name>.rb` and `<name>.json` in `dist/` and prints them, without cloning or pushing to the tap/bucket. Good for inspecting changes before they go out.
The `--dry-run` flag on the updater scripts produces the generated downstream files in `dist/` and prints them, without cloning or pushing to the tap/bucket. Good for inspecting changes before they go out.

```sh
# Build all eight platform archives + linux packages + checksums.txt.
Expand All @@ -107,10 +107,13 @@ bun apps/cli/scripts/update-scoop.ts --version 0.0.1 \
--bucket avallete/scoop-bucket \
--name supabase-shim-poc \
--dry-run

```

Inspect `dist/supabase-shim-poc.rb` and `dist/supabase-shim-poc.json`. The `sha256` / `hash` fields resolve against `dist/checksums.txt`; the `url` fields point at `https://github.com/avallete/supabase-cli-release-poc/releases/download/v0.0.1/...` (the release host specified by `--repo`).

Winget is intentionally not part of Ring 2: WingetCreate's non-interactive update path depends on an existing package identifier in `microsoft/winget-pkgs`, so production automation starts only after the one-time `Supabase.CLI` bootstrap PR is accepted.

### Upload the GitHub Release

The updater scripts do **not** create the GitHub Release or upload `dist/`\* — in production that's `[release-shared.yml](../../../.github/workflows/release-shared.yml)`'s `publish` job. For a PoC run, do it manually with `gh release create`:
Expand Down Expand Up @@ -230,7 +233,7 @@ flowchart TD
ff --> pushMain
dispatch[workflow_dispatch<br/>channel + version] --> plan

plan["plan (ubuntu-latest)<br/>cycjimmy/semantic-release-action --dry-run<br/>computes channel, version, shell, npm_tag, brew/scoop name"]
plan["plan (ubuntu-latest)<br/>cycjimmy/semantic-release-action --dry-run<br/>computes channel, version, shell, npm_tag, downstream names"]
plan --> shared

shared["release-shared.yml"]
Expand All @@ -240,8 +243,22 @@ flowchart TD
pub --> rel["softprops/action-gh-release<br/>(draft) → gh release edit --draft=false"]
rel --> hb["publish-homebrew<br/>App-token-authed clone of homebrew-tap<br/>update-homebrew.ts --name <brew_name>"]
rel --> sc["publish-scoop<br/>App-token-authed clone of scoop-bucket<br/>update-scoop.ts --name <scoop_name>"]
rel --> wg["publish-winget<br/>WingetCreate update --submit<br/>update-winget.ps1 -PackageId Supabase.CLI"]
```

Only the stable channel is submitted to Winget (`Supabase.CLI`). Beta / alpha are intentionally skipped until there is a concrete need for separate prerelease package identifiers.

### One-time Winget bootstrap

Before enabling `publish-winget` in production, submit the first `Supabase.CLI` version to `microsoft/winget-pkgs`. The initial manifest should use the existing GitHub Release ZIP assets:

- `https://github.com/supabase/cli/releases/download/v<version>/supabase_<version>_windows_amd64.zip`
- `https://github.com/supabase/cli/releases/download/v<version>/supabase_<version>_windows_arm64.zip`

The installer manifest must use `InstallerType: zip`, `NestedInstallerType: portable`, and `NestedInstallerFiles` entries for `supabase.exe` with `PortableCommandAlias: supabase`. While stable releases still ship the legacy shell, also include `supabase-go.exe` as a nested portable file.

Open the bootstrap PR from the GitHub account that should own the Microsoft CLA and future WingetCreate submissions. After that PR is accepted and `winget show --id Supabase.CLI` resolves, configure the release repository with `WINGET_CREATE_GITHUB_TOKEN` and set `WINGET_PUBLISH_ENABLED=true`; stable release CI can then use WingetCreate's non-interactive `update` command for every later version.

### Trigger

Most releases are automatic — merge a PR into `develop` (beta) or approve the weekly Prod-Deploy PR into `main` (stable). For an `alpha` cut or a one-off override, dispatch manually:
Expand Down Expand Up @@ -283,17 +300,19 @@ The matrix does not yet include `windows-11-arm` (gate 6) or an Alpine musl runn
3. `softprops/action-gh-release` creates a **draft** Release `v<version>` on `supabase/cli` with all tar / zip / deb / rpm / apk + `checksums.txt`.
4. `gh release edit v<version> --draft=false` finalises it (immutable from this point).

### Post-publish: Homebrew + Scoop
### Post-publish: Homebrew + Scoop + Winget

Both updaters run automatically from `release-shared.yml`'s `publish-homebrew` and `publish-scoop` jobs after the GitHub Release is finalised. Each job mints a GitHub App token scoped to `homebrew-tap` / `scoop-bucket` (via `actions/create-github-app-token` with the `GH_APP_CLIENT_ID` + `GH_APP_PRIVATE_KEY` secrets), runs `gh auth setup-git` + sets a `github-actions[bot]` git identity, then invokes `apps/cli/scripts/update-homebrew.ts` / `update-scoop.ts` with `--name <brew_name>` / `--name <scoop_name>`:

- `stable` → `--name supabase` (the default formula / manifest, what `brew install supabase` resolves)
- `beta` → `--name supabase-beta` (a separate formula / manifest for the prerelease channel)
- `alpha` → skipped (Homebrew + Scoop are not part of the v3 alpha story; npm only)

Stable releases also run `publish-winget` when `WINGET_PUBLISH_ENABLED=true`, invoking `apps/cli/scripts/update-winget.ps1 -PackageId Supabase.CLI -Submit` on a Windows runner. The script downloads WingetCreate, calls `wingetcreate update` with the released Windows ZIP URLs and explicit `x64` / `arm64` architecture overrides, and submits the PR against `microsoft/winget-pkgs`. The job requires a `WINGET_CREATE_GITHUB_TOKEN` secret for the account that should submit Winget PRs.

### Verification

After `release-shared.yml` finishes (all jobs including `publish-homebrew` and `publish-scoop`):
After `release-shared.yml` finishes (all jobs including `publish-homebrew`, `publish-scoop`, and `publish-winget`):

```sh
npm view supabase@0.1.0 dist-tags # expect: latest: 0.1.0 (or beta: 0.1.0-beta.N for beta channel)
Expand All @@ -307,6 +326,11 @@ supabase --version # expect: supabase v0.1.0
scoop update
scoop install supabase # or: scoop install supabase-beta
supabase --version # expect: supabase v0.1.0

# Windows (Winget, stable only, after the microsoft/winget-pkgs PR is accepted):
winget source update
winget install --id Supabase.CLI
supabase --version # expect: supabase v0.1.0
```

The npm package page should also show **Provenance** linking back to `supabase/cli` + `release.yml` (OIDC-attested build).
Expand All @@ -319,6 +343,7 @@ The per-channel artifacts are immutable once published, so rollback = point user
2. **GitHub Release:** `gh release delete v<broken-version> --repo supabase/cli` (or mark it `prerelease: true` via `gh release edit` to keep the tag around). Artifacts remain downloadable unless the release itself is deleted.
3. **Homebrew:** in `supabase/homebrew-tap`, `git revert <commit that wrote Formula/supabase.rb for broken version>` + push. `brew update` picks it up.
4. **Scoop:** same pattern in `supabase/scoop-bucket` — `git revert` the manifest commit.
5. **Winget:** there is no dist-tag equivalent. Submit a follow-up PR to `microsoft/winget-pkgs` removing the broken version or publish a fixed newer CLI version and let Winget select the highest version.

Rollback is straightforward because each channel is its own commit / release. There's no cross-channel state to reconcile.

Expand All @@ -329,5 +354,5 @@ Rollback is straightforward because each channel is its own commit / release. Th
- [ADR 0011](../../../docs/adr/0011-cli-release-and-distribution-strategy.md) — the decision record. Channel choices, signing rationale, open pre-cutover gates.
- `[apps/cli/docs/binary-distribution.md](./binary-distribution.md)` — why each platform package contains two binaries (`supabase` SFE + `supabase-go` sidecar) and how they're resolved at runtime.
- `[tools/release/local-release.ts](../../../tools/release/local-release.ts)` — Ring 1 implementation.
- `[apps/cli/scripts/build.ts](../scripts/build.ts)`, `[publish.ts](../scripts/publish.ts)`, `[sync-versions.ts](../scripts/sync-versions.ts)`, `[update-homebrew.ts](../scripts/update-homebrew.ts)`, `[update-scoop.ts](../scripts/update-scoop.ts)` — release script implementations.
- `[apps/cli/scripts/build.ts](../scripts/build.ts)`, `[publish.ts](../scripts/publish.ts)`, `[sync-versions.ts](../scripts/sync-versions.ts)`, `[update-homebrew.ts](../scripts/update-homebrew.ts)`, `[update-scoop.ts](../scripts/update-scoop.ts)`, `[update-winget.ps1](../scripts/update-winget.ps1)` — release script implementations.
- `[.github/workflows/release.yml](../../../.github/workflows/release.yml)`, `[release-shared.yml](../../../.github/workflows/release-shared.yml)`, `[deploy.yml](../../../.github/workflows/deploy.yml)`, `[deploy-check.yml](../../../.github/workflows/deploy-check.yml)` — Ring 3 pipeline.
64 changes: 64 additions & 0 deletions apps/cli/scripts/update-winget.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
param(
[Parameter(Mandatory = $true)]
[string] $Version,

[string] $PackageId = 'Supabase.CLI',
[string] $Repo = 'supabase/cli',
[string] $WingetCreatePath = '',
[string] $OutDir = '',
[switch] $Submit,
[switch] $DryRun
)

$ErrorActionPreference = 'Stop'

if (-not $IsWindows) {
throw 'WingetCreate must run on Windows.'
}

if ($Submit -and [string]::IsNullOrWhiteSpace($env:WINGET_CREATE_GITHUB_TOKEN)) {
throw 'WINGET_CREATE_GITHUB_TOKEN is required when submitting Winget manifests.'
}

if ([string]::IsNullOrWhiteSpace($OutDir)) {
$OutDir = Join-Path $PWD 'dist\winget'
}

function Resolve-WingetCreate {
if (-not [string]::IsNullOrWhiteSpace($WingetCreatePath)) {
return $WingetCreatePath
}

$downloadPath = Join-Path ([System.IO.Path]::GetTempPath()) 'wingetcreate.exe'
Invoke-WebRequest 'https://aka.ms/wingetcreate/latest' -OutFile $downloadPath
return $downloadPath
}

$wingetCreate = Resolve-WingetCreate
$releaseUrl = "https://github.com/$Repo/releases/tag/v$Version"
$amd64Url = "https://github.com/$Repo/releases/download/v$Version/supabase_${Version}_windows_amd64.zip|x64"
$arm64Url = "https://github.com/$Repo/releases/download/v$Version/supabase_${Version}_windows_arm64.zip|arm64"

$wingetCreateArgs = @(
'update'
$PackageId
'--version'
$Version
'--urls'
$amd64Url
$arm64Url
'--release-notes-url'
$releaseUrl
'--out'
$OutDir
'--prtitle'
"New version: $PackageId version $Version"
'--no-open'
)

if ($Submit -and -not $DryRun) {
$wingetCreateArgs += '--submit'
}

Write-Host "Running WingetCreate for $PackageId $Version"
& $wingetCreate @wingetCreateArgs
Loading