Skip to content
Open
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
23 changes: 18 additions & 5 deletions apps/cli/src/legacy/commands/functions/download/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@

## Subprocesses

| Command | When | Purpose |
| ------------------------------------ | ----------------------------------- | ----------------------------------- |
| `supabase-go functions download ...` | `--use-docker` or `--legacy-bundle` | preserve hidden compatibility modes |
| Command | When | Purpose |
| ------------------------------------ | ----------------------------------------------------------------- | ----------------------------------- |
| `supabase-go functions download ...` | `--use-docker` (default) or `--legacy-bundle`, unless `--use-api` | preserve hidden compatibility modes |

The delegated call runs with `SUPABASE_TELEMETRY_DISABLED=1` so the Go child's
own `cli_command_executed` doesn't double-count on top of this command's own
telemetry (mirrors `db pull`/`db diff`'s delegated-call pattern). In
`--output-format json|stream-json`, the child's stdout is captured and
discarded instead of inherited (`LegacyGoProxy.execCapture`) — the raw text
never reaches the terminal, and this command emits the `Output` envelope
itself once the child exits successfully.

## Environment Variables

Expand All @@ -50,16 +58,21 @@ Prints progress and success messages as functions are downloaded.

### `--output-format json`

Prints a structured success result with the downloaded function slugs and project ref.
Prints a structured success result with the downloaded function slugs and project ref. On the
Docker/legacy-bundle proxy path, the Go child's stdout is captured/discarded (never inherited) so
it can't corrupt the envelope; the slug list is resolved independently for the payload.

### `--output-format stream-json`

Prints a structured success result with the downloaded function slugs and project ref.
Same envelope as `json` above (including on the proxy path).

## Notes

- If no function name is provided, downloads all functions.
- Requires a linked project (`--project-ref` or linked project config).
- Native downloads reject path traversal and symlink escapes before writing source files.
- `--use-docker` and `--legacy-bundle` are hidden flags forwarded to the Go binary for backward compatibility; they are mutually exclusive with `--use-api`.
- `--use-docker` defaults to `true` (Go parity), so a bare `supabase functions download` proxies to the Go binary's Docker-based unbundler unless `--use-api` resolves to `true`, which forces the native server-side download path instead (`apps/cli-go/cmd/functions.go:51-53`: `if useApi { useDocker = false }` reads the resolved flag value, not presence — `--use-api=false` still proxies).
- If Docker is not running, the Go binary itself prints `WARNING: Docker is not running` to stderr and falls back to its own server-side unbundler — the command still exits `0` without Docker installed or running.
- The mutual-exclusivity check only counts flags the user explicitly passed on the command line, not `--use-docker`'s default value — so `--use-api` alone never trips the "mutually exclusive" error. The Go proxy call itself also only ever forwards one of `--use-docker`/`--legacy-bundle`, never both, even though `--use-docker` defaults to `true`.
- Refreshes the linked-project telemetry cache and flushes telemetry state after resolving a project ref.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const config = {
),
useDocker: Flag.boolean("use-docker").pipe(
Flag.withDescription("Use Docker to unbundle functions locally."),
Flag.withDefault(true),
Comment thread
Coly010 marked this conversation as resolved.
Flag.withHidden,
),
legacyBundle: Flag.boolean("legacy-bundle").pipe(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, test } from "vitest";
import { makeTempHome, runSupabase } from "../../../../../tests/helpers/cli.ts";

// Argument-validation negatives for `functions download`. This validation
// lives in the Go CLI today (the legacy TS command proxies to it); a
// black-box subprocess test keeps these assertions valid through the
// eventual native TS port — it guards behavior, not implementation. Mirrors
// `deploy.e2e.test.ts`'s coverage for the sibling `--use-docker` default bug
// (CLI-1862).
//
// The mutex-conflict cases below fail before any network call (flag parsing
// / mutex validation), so no auth or linked project is required.

const E2E_TIMEOUT_MS = 30_000;
const SLUG = "download-e2e-basic";
const FAKE_TOKEN = `sbp_${"0".repeat(40)}`;
const FAKE_REF = "a".repeat(20);

describe("supabase functions download (legacy) — argument validation", () => {
const conflicts = [
{ name: "--use-api + --use-docker", flags: ["--use-api", "--use-docker"] },
{ name: "--use-api + --legacy-bundle", flags: ["--use-api", "--legacy-bundle"] },
{ name: "--use-docker + --legacy-bundle", flags: ["--use-docker", "--legacy-bundle"] },
] as const;

for (const { name, flags } of conflicts) {
test(`rejects ${name} as mutually exclusive`, { timeout: E2E_TIMEOUT_MS }, async () => {
using home = makeTempHome();
const { exitCode, stderr } = await runSupabase(
["functions", "download", SLUG, "--project-ref", FAKE_REF, ...flags],
{
entrypoint: "legacy",
home: home.dir,
env: { HOME: home.dir, SUPABASE_ACCESS_TOKEN: FAKE_TOKEN },
},
);
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/none of the others can be|mutually exclusive/i);
});
}

// CLI-1862: `--use-docker` now defaults to `true` (Go parity). Before the
// fix, that default was counted as "explicitly selected" by the mutex
// check, so passing `--use-api` alone was incorrectly rejected as
// conflicting with the (unpassed) `--use-docker` default. Covered in
// `download.integration.test.ts` ("does not treat the --use-docker default
// as conflicting with an explicit --use-api") via a mocked platform API
// instead of here: now that the mutex check is fixed, `--use-api` alone
// passes validation and proceeds to the native downloader, which calls the
// real Management API with the fake token/ref — an argument-validation
// test shouldn't depend on that network round-trip.

// CLI-1862: the TS→Go proxy call must not forward the now-defaulted
// `--use-docker` alongside an explicit `--legacy-bundle` — the Go binary
// re-parses this argv itself and enforces the same mutual exclusivity, so
// forwarding both breaks `--legacy-bundle` outright. Covered in
// `download.integration.test.ts` ("forwards only --legacy-bundle to the Go
// proxy...") via a mocked `LegacyGoProxy` instead of here: unlike
// `--use-api`, `--legacy-bundle` routes to the Go binary's `RunLegacy`
// downloader, which calls `InstallOrUpgradeDeno` before any network call
// (`apps/cli-go/internal/functions/download/download.go`). Each e2e run
// gets a fresh `SUPABASE_HOME`, so this would trigger a real, uncached
// Deno download from GitHub on every run — a real cross-boundary
// dependency this suite shouldn't take on to prove a pure TS-side routing
// decision.
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Effect, Option } from "effect";
import { Effect, Option, Stdio } from "effect";
import {
downloadFunctions,
makeGoProxyDownloadArgs,
Expand All @@ -20,11 +20,14 @@ export const legacyFunctionsDownload = Effect.fn("legacy.functions.download")(fu
const linkedProjectCache = yield* LegacyLinkedProjectCache;
const telemetryState = yield* LegacyTelemetryState;
const proxy = yield* LegacyGoProxy;
const stdio = yield* Stdio.Stdio;
const rawArgs = yield* stdio.args;
Comment thread
Coly010 marked this conversation as resolved.
let resolvedProjectRef = Option.none<string>();

yield* downloadFunctions(flags, {
api,
projectRoot: cliConfig.workdir,
rawArgs,
resolveProjectRef: (projectRef) =>
resolver.resolve(projectRef).pipe(
Effect.tap((ref) =>
Expand All @@ -33,8 +36,23 @@ export const legacyFunctionsDownload = Effect.fn("legacy.functions.download")(fu
}),
),
),
proxyDownload: (proxyFlags, projectRef) =>
proxy.exec(makeGoProxyDownloadArgs(proxyFlags, projectRef)),
// The delegated Go binary runs its own `Execute()` and would otherwise
// fire its own `cli_command_executed` on top of this command's own
// `withLegacyCommandInstrumentation` wrapper. Suppress it so proxied
// invocations record exactly one event, matching Go (mirrors `db pull` /
// `db diff`'s delegated-call pattern).
//
// In machine-output mode the child's stdout is captured and discarded
// instead of inherited, matching `db pull`/`db diff`'s delegated-call
// pattern for the CLI-1546 "stdout is payload-only in machine mode"
// invariant — `downloadFunctions` emits the `Output` envelope itself.
proxyDownload: (proxyFlags, projectRef, captureOutput) => {
const args = makeGoProxyDownloadArgs(proxyFlags, projectRef);
const env = { SUPABASE_TELEMETRY_DISABLED: "1" };
return captureOutput
? Effect.asVoid(proxy.execCapture(args, { env, stdin: "ignore" }))
Comment thread
Coly010 marked this conversation as resolved.
: proxy.exec(args, { env });
},
}).pipe(
Effect.ensuring(
Effect.suspend(() =>
Expand Down
Loading
Loading