diff --git a/.github/workflows/release-shared.yml b/.github/workflows/release-shared.yml
index 1b6f7cd607..fc0b8ffc44 100644
--- a/.github/workflows/release-shared.yml
+++ b/.github/workflows/release-shared.yml
@@ -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
@@ -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)
@@ -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:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1e72b5d6c9..50a8473d01 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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:
@@ -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
@@ -157,6 +159,7 @@ jobs:
brew_name=""
scoop_name=""
publish_brew_scoop=false
+ publish_winget=false
;;
beta)
shell=legacy
@@ -165,6 +168,7 @@ jobs:
brew_name=supabase-beta
scoop_name=supabase-beta
publish_brew_scoop=true
+ publish_winget=false
;;
stable)
shell=legacy
@@ -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
@@ -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"
@@ -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:
@@ -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
diff --git a/apps/cli/docs/release-process.md b/apps/cli/docs/release-process.md
index 84c6801897..03fa895eef 100644
--- a/apps/cli/docs/release-process.md
+++ b/apps/cli/docs/release-process.md
@@ -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
pnpm cli-release
--next or --legacy"]
poc["Ring 2: User-owned PoC repos
avallete/supabase-cli-release-poc
avallete/homebrew-supabase-shim-poc
avallete/scoop-bucket
--name supabase-shim-poc"]
- prod["Ring 3: Production
supabase/cli
supabase/homebrew-tap
supabase/scoop-bucket
(default name: supabase)"]
+ prod["Ring 3: Production
supabase/cli
supabase/homebrew-tap
supabase/scoop-bucket
microsoft/winget-pkgs PR
(default name: supabase)"]
local --> poc --> prod
```
@@ -86,7 +86,7 @@ gh auth status # verify: ✓ Logged in to github.com account
### Dry-run: generate artifacts without pushing
-The `--dry-run` flag on both updater scripts produces the `Formula/.rb` and `.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.
@@ -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`:
@@ -230,7 +233,7 @@ flowchart TD
ff --> pushMain
dispatch[workflow_dispatch
channel + version] --> plan
- plan["plan (ubuntu-latest)
cycjimmy/semantic-release-action --dry-run
computes channel, version, shell, npm_tag, brew/scoop name"]
+ plan["plan (ubuntu-latest)
cycjimmy/semantic-release-action --dry-run
computes channel, version, shell, npm_tag, downstream names"]
plan --> shared
shared["release-shared.yml"]
@@ -240,8 +243,22 @@ flowchart TD
pub --> rel["softprops/action-gh-release
(draft) → gh release edit --draft=false"]
rel --> hb["publish-homebrew
App-token-authed clone of homebrew-tap
update-homebrew.ts --name "]
rel --> sc["publish-scoop
App-token-authed clone of scoop-bucket
update-scoop.ts --name "]
+ rel --> wg["publish-winget
WingetCreate update --submit
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/supabase__windows_amd64.zip`
+- `https://github.com/supabase/cli/releases/download/v/supabase__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:
@@ -283,7 +300,7 @@ 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` on `supabase/cli` with all tar / zip / deb / rpm / apk + `checksums.txt`.
4. `gh release edit v --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 ` / `--name `:
@@ -291,9 +308,11 @@ Both updaters run automatically from `release-shared.yml`'s `publish-homebrew` a
- `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)
@@ -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).
@@ -319,6 +343,7 @@ The per-channel artifacts are immutable once published, so rollback = point user
2. **GitHub Release:** `gh release delete v --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 ` + 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.
@@ -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.
diff --git a/apps/cli/scripts/update-winget.ps1 b/apps/cli/scripts/update-winget.ps1
new file mode 100644
index 0000000000..0ec8e6b960
--- /dev/null
+++ b/apps/cli/scripts/update-winget.ps1
@@ -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
diff --git a/apps/cli/src/next/commands/functions/deploy/deploy.integration.test.ts b/apps/cli/src/next/commands/functions/deploy/deploy.integration.test.ts
index 3f19ed9573..79dc69997c 100644
--- a/apps/cli/src/next/commands/functions/deploy/deploy.integration.test.ts
+++ b/apps/cli/src/next/commands/functions/deploy/deploy.integration.test.ts
@@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest";
import { makeApiClient, FunctionResponse } from "@supabase/api/effect";
import { BunServices } from "@effect/platform-bun";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
-import { mkdir, rm, writeFile } from "node:fs/promises";
+import { mkdir, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, sep } from "node:path";
import { Effect, Layer, Option, Sink, Stdio, Stream } from "effect";
@@ -343,6 +343,14 @@ function resolveDockerOutputPath(args: ReadonlyArray): string {
throw new Error(`unable to resolve host output path for ${dockerOutputPath}`);
}
+function toDockerBindPath(hostPath: string): string {
+ return hostPath.replaceAll("\\", "/").replace(/^[A-Za-z]:/, "");
+}
+
+function readonlyDockerBind(hostPath: string): string {
+ return `${hostPath}:${toDockerBindPath(hostPath)}:ro`;
+}
+
function mockChildProcessSpawner(
opts: {
readonly exitCode?: number;
@@ -1163,6 +1171,9 @@ describe("functions deploy", () => {
yield* Effect.promise(() =>
writeFile(join(tempDir, "supabase", "custom_import_map.json"), '{"imports":{}}\n'),
);
+ const importMapPath = yield* Effect.promise(() =>
+ realpath(join(tempDir, "supabase", "custom_import_map.json")),
+ );
const { out, api, layer } = setup(tempDir, {
rawArgs: ["functions", "deploy", "hello-world", "--use-docker"],
@@ -1207,15 +1218,7 @@ describe("functions deploy", () => {
expect(api.requests[1]?.urlParams).toContain("slug=hello-world");
expect(api.requests[1]?.urlParams).toContain("verify_jwt=false");
expect(child.spawned.at(-1)?.args).toContain("public.ecr.aws/supabase/edge-runtime:v1.68.4");
- expect(child.spawned.at(-1)?.args).toContain(
- `${join(tempDir, "supabase", "custom_import_map.json")}:${join(
- tempDir,
- "supabase",
- "custom_import_map.json",
- )
- .replaceAll("\\", "/")
- .replace(/^[A-Za-z]:/, "")}:ro`,
- );
+ expect(child.spawned.at(-1)?.args).toContain(readonlyDockerBind(importMapPath));
expect(out.stderrText).toContain("Bundling Function: hello-world\n");
expect(out.stderrText).toContain("Deploying Function: hello-world (script size:");
expect(out.stdoutText).toContain(
@@ -1498,6 +1501,7 @@ describe("functions deploy", () => {
yield* Effect.promise(() => writeLocalFunction(tempDir, "hello-world"));
yield* Effect.promise(() => mkdir(dirname(staticFile), { recursive: true }));
yield* Effect.promise(() => writeFile(staticFile, "hello
\n"));
+ const realStaticFile = yield* Effect.promise(() => realpath(staticFile));
const { layer } = setup(tempDir, {
rawArgs: ["functions", "deploy", "hello-world", "--use-docker"],
@@ -1511,9 +1515,7 @@ describe("functions deploy", () => {
}).pipe(Effect.provide(layer));
expect(child.spawned).toHaveLength(4);
- expect(child.spawned.at(-1)?.args).toContain(
- `${staticFile}:${staticFile.replaceAll("\\", "/").replace(/^[A-Za-z]:/, "")}:ro`,
- );
+ expect(child.spawned.at(-1)?.args).toContain(readonlyDockerBind(realStaticFile));
}).pipe(Effect.ensuring(cleanupTempDir(tempDir)));
});
diff --git a/apps/cli/src/shared/functions/deploy.ts b/apps/cli/src/shared/functions/deploy.ts
index 17291cd764..cb4d4e43e3 100644
--- a/apps/cli/src/shared/functions/deploy.ts
+++ b/apps/cli/src/shared/functions/deploy.ts
@@ -670,12 +670,7 @@ async function walkImportPaths(
modulePath = toSlash(join(dirname(current), modulePath));
}
- const resolvedModule = resolve(modulePath);
- if (!isContainedInAnyPath(allowedRoots, resolvedModule)) {
- await onWarning(`WARN: Skipping import path outside project root: ${modulePath}\n`);
- continue;
- }
- queue.push(toSlash(resolvedModule));
+ queue.push(toSlash(resolve(modulePath)));
}
}
}
@@ -846,7 +841,6 @@ async function resolveImportMapAllowedRoots(projectRoot: string, importMapPath:
}
async function writeSourceDeployForm(
- cwd: string,
projectRoot: string,
config: ResolvedDeployFunctionConfig,
metadata: SourceDeployMetadata,
@@ -863,7 +857,7 @@ async function writeSourceDeployForm(
return;
}
uploadedAssets.add(realPathname);
- const relativePath = toApiRelativePath(cwd, pathname);
+ const relativePath = toApiRelativePath(realProjectRoot, realPathname);
await Effect.runPromise(outputRaw(`Uploading asset (${config.slug}): ${relativePath}\n`));
form.append("file", new File([contents], relativePath));
};
@@ -1445,7 +1439,7 @@ const uploadFunctionSource = Effect.fnUntraced(function* (
const output = yield* Output;
const files = yield* Effect.tryPromise({
try: async () => {
- const form = await writeSourceDeployForm(cwd, projectRoot, config, metadata, (text) =>
+ const form = await writeSourceDeployForm(projectRoot, config, metadata, (text) =>
output.raw(text, "stderr"),
);
return form.getAll("file").flatMap((part) => (part instanceof Blob ? [part] : []));
diff --git a/docs/adr/0011-cli-release-and-distribution-strategy.md b/docs/adr/0011-cli-release-and-distribution-strategy.md
index f2a8e51f2f..d4245315da 100644
--- a/docs/adr/0011-cli-release-and-distribution-strategy.md
+++ b/docs/adr/0011-cli-release-and-distribution-strategy.md
@@ -12,6 +12,7 @@ The CLI is currently distributed through:
- **npm / npx** — the primary install path
- **Homebrew** — `supabase/homebrew-tap`
- **Scoop** — `supabase/scoop-bucket`
+- **Winget** — `microsoft/winget-pkgs` manifest PRs for stable Windows installs
- **apt / rpm / apk** — Linux package manager files hosted on GitHub Releases
- **GitHub Releases** — platform archives + checksums for direct download
@@ -30,6 +31,7 @@ We replace GoReleaser with a pipeline built on **Bun `--compile` single-file exe
| Linux packages | **`nfpm`** (binary only, installed from GoReleaser's apt repo) for `.deb` / `.rpm` / `.apk` |
| Homebrew | Existing `supabase/homebrew-tap`, updated by [`apps/cli/scripts/update-homebrew.ts`](../../apps/cli/scripts/update-homebrew.ts) |
| Scoop | Existing `supabase/scoop-bucket`, updated by [`apps/cli/scripts/update-scoop.ts`](../../apps/cli/scripts/update-scoop.ts) |
+| Winget | Stable-only `Supabase.CLI` manifests submitted to `microsoft/winget-pkgs` by WingetCreate through [`apps/cli/scripts/update-winget.ps1`](../../apps/cli/scripts/update-winget.ps1) |
| apt / rpm repo hosting | **None** — stay GitHub-Release-downloads-only |
| Dist-tags | `latest` = stable legacy shell, `beta` = prerelease legacy shell from `develop` (`X.Y.Z-beta.N`), `alpha` = next shell (TS-native). No separate `next` or `canary` tags |
| CI | `[release.yml](../../.github/workflows/release.yml)` (pushes to `develop` / `main` plus manual dispatch) invokes `[release-shared.yml](../../.github/workflows/release-shared.yml)` — build → smoke-test matrix → publish → draft Release → finalize |
@@ -145,6 +147,7 @@ flowchart TD
finalize["gh release edit --draft=false"]
hbUpdate["update-homebrew.ts
Formula push"]
scoopUpdate["update-scoop.ts
manifest push"]
+ wingetUpdate["update-winget.ps1
WingetCreate manifest PR"]
dispatch --> releaseYml
pushDev --> releaseYml
@@ -153,9 +156,10 @@ flowchart TD
shared --> build --> smoke --> publish --> ghRelease --> finalize
finalize -.follow-up.-> hbUpdate
finalize -.follow-up.-> scoopUpdate
+ finalize -.stable only.-> wingetUpdate
```
-`release.yml` selects the channel (stable / beta / alpha); `release-shared.yml` performs build → smoke-test → publish → GitHub Release. Homebrew and Scoop updates follow stable and beta publishes (alpha stays npm-only). Operational detail: [`apps/cli/docs/release-process.md`](../../apps/cli/docs/release-process.md).
+`release.yml` selects the channel (stable / beta / alpha); `release-shared.yml` performs build → smoke-test → publish → GitHub Release. Homebrew and Scoop updates follow stable and beta publishes (alpha stays npm-only). Winget follows stable only through the `Supabase.CLI` package identifier. Operational detail: [`apps/cli/docs/release-process.md`](../../apps/cli/docs/release-process.md).
### Per-channel publish mechanisms
@@ -191,6 +195,12 @@ Windows-arm64 is live in the TS pipeline as of this branch — `packages/cli-win
Production bucket: `supabase/scoop-bucket`.
+#### Winget
+
+The first `Supabase.CLI` manifest is a one-time bootstrap PR to `microsoft/winget-pkgs`, using the existing `supabase__windows_amd64.zip` and `supabase__windows_arm64.zip` GitHub Release assets. The installer type is `zip` with `NestedInstallerType: portable`, so the manifest exposes `supabase.exe` as the `supabase` command without introducing an MSI/MSIX build. Legacy-shell bootstrap manifests also list the colocated `supabase-go.exe` sidecar so proxied commands keep working after Winget extraction.
+
+After the bootstrap PR is accepted, production stable releases run [`apps/cli/scripts/update-winget.ps1`](../../apps/cli/scripts/update-winget.ps1) when `WINGET_PUBLISH_ENABLED=true`. The script downloads WingetCreate and calls `wingetcreate update Supabase.CLI` with the released Windows ZIP URLs and explicit architecture overrides. The job submits a PR using `WINGET_CREATE_GITHUB_TOKEN`. Beta and alpha are skipped for now; if they become necessary, use separate package identifiers rather than publishing prereleases into `Supabase.CLI`.
+
#### apt / rpm / apk
`nfpm` runs inside `[apps/cli/scripts/build.ts](../../apps/cli/scripts/build.ts)` and emits six Linux packages (deb/rpm/apk × arm64/amd64). All six are uploaded to the GitHub Release as direct downloads; there is no hosted repo. Installation is `dpkg -i supabase__linux_amd64.deb` or equivalent.
@@ -322,7 +332,7 @@ This section tracks the work that has landed against the pre-cutover gates. Deta
- [`apps/cli/docs/release-process.md`](../../apps/cli/docs/release-process.md) — the operational playbook: local Verdaccio → user-owned PoC repos → production pipeline.
- [`apps/cli/docs/binary-distribution.md`](../../apps/cli/docs/binary-distribution.md) — runtime resolution details for the two-binary legacy model (TS SFE + Go binary).
- [`.github/workflows/release-shared.yml`](../../.github/workflows/release-shared.yml), [`release.yml`](../../.github/workflows/release.yml) — the pipeline implementation (`release.yml` selects stable / beta / alpha channel per trigger).
-- [`apps/cli/scripts/build.ts`](../../apps/cli/scripts/build.ts), [`publish.ts`](../../apps/cli/scripts/publish.ts), [`sync-versions.ts`](../../apps/cli/scripts/sync-versions.ts), [`update-homebrew.ts`](../../apps/cli/scripts/update-homebrew.ts), [`update-scoop.ts`](../../apps/cli/scripts/update-scoop.ts) — release scripts.
+- [`apps/cli/scripts/build.ts`](../../apps/cli/scripts/build.ts), [`publish.ts`](../../apps/cli/scripts/publish.ts), [`sync-versions.ts`](../../apps/cli/scripts/sync-versions.ts), [`update-homebrew.ts`](../../apps/cli/scripts/update-homebrew.ts), [`update-scoop.ts`](../../apps/cli/scripts/update-scoop.ts), [`update-winget.ps1`](../../apps/cli/scripts/update-winget.ps1) — release scripts.
- [`apps/cli-go/.goreleaser.yml`](../../apps/cli-go/.goreleaser.yml), [`release.yml`](../../apps/cli-go/.github/workflows/release.yml), [`release-beta.yml`](../../apps/cli-go/.github/workflows/release-beta.yml) — the Go CLI release config we mirror (no signing, `windows_arm64` target, GitHub-App-scoped publish tokens).
- [CLI-1330](https://linear.app/supabase/issue/CLI-1330) — origin ticket.
- [CLI-1344](https://linear.app/supabase/issue/CLI-1344/investigate-if-we-can-consolidate-local-release-logic-and-pipeline) — sub-issue: consolidate local and pipeline release logic.