Skip to content

fix(cli): port functions download to native TypeScript (CLI-1963) - #6082

Draft
Coly010 wants to merge 14 commits into
developfrom
columferry/cli-1963-port-functions-download-to-native-typescript-both-shells
Draft

fix(cli): port functions download to native TypeScript (CLI-1963)#6082
Coly010 wants to merge 14 commits into
developfrom
columferry/cli-1963-port-functions-download-to-native-typescript-both-shells

Conversation

@Coly010

@Coly010 Coly010 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What

Ports supabase functions download's default Docker-unbundle path (--use-docker, default true) from wholesale Go-binary delegation to native TypeScript, in both the legacy and next shells. --use-api was already native before this PR; this closes the remaining default-path gap.

--legacy-bundle (hidden, deprecated pre-1.120.0 fallback) is deliberately left delegating to the Go binary — see "Scope decision" below.

Ground truth: apps/cli-go/internal/functions/download/download.go (downloadWithDockerUnbundle, downloadOne, extractOne, getErrorLogger). Verified against it via an independent go-parity-auditor pass; see inline comments in download.ts for file:line citations.

Linear: https://linear.app/supabase/issue/CLI-1963/port-functions-download-to-native-typescript-both-shells

Scope decision: --legacy-bundle stays delegated

This hidden flag requires installing/upgrading a real Deno binary on the host (InstallOrUpgradeDeno: downloads a release zip from denoland/deno or a third-party ARM64 fork, extracts, chmods, installs to ~/.supabase/deno) and shelling out to an embedded Deno script that itself pulls deno.land modules at runtime. This is unique in the Go CLI — no other command, and no already-ported TS command, manages a downloaded third-party binary on the host. Porting it would give the TS CLI a first-of-its-kind capability (unverified binary download + host install + runtime network fetches) purely to support functions deployed by a 3+-year-old CLI release. Full rationale, including the go-parity-auditor's findings on this seam, is recorded as a comment on the Linear issue. docs/go-cli-porting-status.md reflects the partial (not fully-native) status accordingly.

Bugs found and fixed along the way

  • CLI-1891-class validation gap: slugs sourced from the Management API's function list (the "download all" path) weren't validated before download — the new Docker path's temp-file write would have reopened a path-traversal vector Go's own downloadAll already guards against. Fixed with the same per-slug validation Go uses, before any per-slug network/filesystem work.
  • next shell's --use-docker flag was missing Flag.withDefault(true) — a real default-value divergence from legacy (which already had it) and from Go. Note: this changes next's bare functions download invocation to attempt Docker unbundling by default (degrading gracefully to the server-side path with a warning if Docker isn't running), matching Go and the legacy shell — flagging explicitly since it's the one behavior change to next in this diff.
  • Brotli double-decompression bug: this CLI's HTTP transport (FetchHttpClient, backed by the platform fetch) already transparently auto-decodes Content-Encoding: br responses while still reporting the header — confirmed empirically with a local brotli-serving test server. Go's manual brotli.NewReader step doesn't need porting; doing so anyway would throw on already-decoded bytes. Removed the manual decode entirely.
  • Temp eszip cleanup wasn't defer-equivalent: it only ran after a successful Docker run, so a network/volume/spawn failure left supabase/.temp/output_<slug>.eszip on disk forever. Wrapped in Effect.ensuring so it runs on every path, matching Go's defer fsys.Remove(eszipPath).
  • .suggestion's leading newline was trimmed by the generic CLI error normalizer, losing Go's blank separator line before the --legacy-bundle hint (Fprintln(os.Stderr, CmdSuggestion)). Now read raw instead of trimmed.
  • "invalid eszip v2" suggestion matched as a substring, not Go's exact per-line match (strings.EqualFold(line, "invalid eszip v2")) — a container log line like "error: invalid eszip v2 header" would have wrongly triggered the deno-v2 upgrade suggestion. Fixed to match Go exactly.
  • suggestLegacyBundle was only attached on a non-zero container exit — Go attaches it to any extractOne failure (network/volume creation, container create/start, log streaming). Widened to cover the same scope.
  • Legacy Docker-download path could resolve the wrong project config: resolveEdgeRuntimeImage's loadProjectConfig call omitted search: false/tomlOnly: true, so a --workdir nested under an unrelated ancestor project could pick up the ancestor's config.toml, and a stray supabase/config.json could win over config.toml — both diverge from Go's flags.LoadConfig, which only ever reads supabase/config.toml from the exact resolved workdir. Now gated on goViperCompat (legacy only; next keeps package defaults).
  • --network-id container:<name|id> was treated as a user-created network: the shared isUserDefinedDockerNetwork predicate (used by deploy.ts/serve.ts/download.ts/start's container lifecycle) didn't exclude Docker's container: network mode, so this preflight ran docker network inspect/create container:redis before docker run instead of passing the mode straight through — Go's NetworkMode.IsUserDefined() explicitly excludes IsContainer(). Fixed in the shared predicate, so deploy/serve/start get the same fix.
  • A repeated --network-id flag honored the first occurrence, not the last: explicitStringFlag (shared/cli/cobra-flag-groups.ts) returned as soon as it found a match, but pflag/viper string flags are shared-variable, last-Set()-wins — confirmed empirically with a scratch pflag.FlagSet.Parse probe on --network-id old --network-id ci-net. A caller overriding a default network (or explicitly clearing one with a trailing --network-id=) would have kept the earlier value. Fixed to scan the whole argv and keep the last match, matching the last-wins pattern this file's own explicitBooleanLongFlag already used.
  • Malformed function-list entries were silently dropped instead of failing loudly: listRemoteFunctionSlugs filtered out any list entry with a missing/non-string slug rather than surfacing it — defeating part of the CLI-1891 validation above for exactly the "compromised/malformed API response" case that validation exists for. Go's FunctionResponse.Slug is a required, non-pointer field, so a missing/null slug decodes to "" and fails ValidateFunctionSlug loudly rather than vanishing from the list. Fixed to preserve the entry (coerced to "") so the existing per-slug validation catches it, matching Go instead of reporting "No functions found" or a silent partial download.

Refactoring

  • Hoisted the Docker-orchestration primitives download.ts needs (runChildProcess, isDockerRunning, ensureDockerNetwork, ensureDockerNamedVolume, localDockerId, resolveEdgeRuntimeVersion, etc.) out of deploy.ts into a new shared/functions/functions-docker.ts, per this workspace's "Hoist Before You Duplicate" policy — deploy.ts, serve.ts, and legacy/commands/start/lib/container-lifecycle.ts (existing consumers of the moved symbols) now import from the new module.
  • Deduplicated the edge-runtime-version pin-file lookup, previously copy-pasted verbatim across all four deploy/download handler files (both shells), into one resolveEdgeRuntimeVersionPin helper in functions.shared.ts.

Judgement calls left open (not blocking, noted for visibility)

  • Docker-run composite (network/volume ensure + command build + run + stdout/stderr routing) is still duplicated between deploy.ts's bundleFunctionWithDocker and download.ts's new downloadWithDockerUnbundle. A shared "run an edge-runtime container once" helper is a reasonable follow-up, but extracting it now means touching deploy.ts's working, tested Docker-bundling path for marginal DRY benefit this late in an already-large diff — left as-is.
  • serve.ts has its own, separately-implemented edge-runtime-version-pin lookup (different default constant, different "v"-prefix handling) that wasn't folded into the new shared helper — pre-existing, not introduced by this PR, and unifying it means touching another already-tested file outside this issue's scope.
  • A pre-existing image-tag construction bug (supabase/edge-runtime:v${pin} double-prefixes "v" when the pin file already contains a "v"-prefixed tag) exists in both deploy.ts and now download.ts (inherited, not introduced) — legacy/shared/legacy-edge-runtime-image.ts already has the correct fix for a different call site; worth a follow-up to route both through it.
  • apps/cli/docs/go-cli-porting-status.md has a separate, older "Functions" section (around line 147) that's stale in a different way (predates this PR, marks the whole functions family as missing) — out of scope for this PR, noted for a future doc-cleanup pass.
  • No ECR→GHCR→Docker-Hub registry retry for the edge-runtime image pull: resolveEdgeRuntimeImage resolves a single legacyGetRegistryImageUrl value, so an ECR outage/throttle now fails this native Docker path where the previous Go-delegated default path (via DockerResolveImageIfNotCached) would have retried GHCR/Docker Hub. Pre-existing and shared with deploy.ts/serve.ts's own already-shipped native Docker paths — legacyGetRegistryImageUrlCandidates's retry has only ever been wired up for start. Extending it to all three functions Docker paths is a cross-cutting follow-up, not something to fix piecemeal for download alone (review round on this PR).
  • No default+env config layer when supabase/config.toml is absent: loadProjectConfig returns null for a project with no config file, so resolveEdgeRuntimeImage's denoVersion falls through to undefined and always resolves the v2 default, whereas Go's flags.LoadConfig still merges template defaults and applies viper.AutomaticEnv() (SUPABASE_EDGE_RUNTIME_DENO_VERSION, supabase/.env) even with no file on disk. Pre-existing and cross-cutting: @supabase/config has no equivalent of Go's generic env-var struct binding at all, config.toml present or not, so deploy.ts's identical resolveEdgeRuntimeVersion(deployConfig?.edge_runtime.deno_version, ...) call already has this gap. A fix belongs in the shared config-loading layer every native caller goes through, not duplicated per call site — left open (review round on this PR).
  • SUPABASE_NETWORK_ID isn't honored for Docker network selection: Go's root viper.AutomaticEnv()+BindPFlags (cmd/root.go:316-334) lets SUPABASE_NETWORK_ID override --network-id when the flag itself is never passed; the raw-argv explicitNonEmptyStringFlag lookup here has no env-var fallback. Pre-existing and cross-cutting: start.handler.ts's LegacyNetworkIdFlag and deploy.ts's/serve.ts's own network-id resolution don't check it either — no native command does today. Belongs in one shared place for the global --network-id resolution, not duplicated per Docker-path call site — left open (review round on this PR).
  • No Config.Validate parity wiring for native functions Docker paths: resolveEdgeRuntimeImage's loadedConfig?.config?.project_id ?? projectRef only substitutes on null/undefined, so a config.toml with an explicit project_id = "" resolves to the empty string here instead of failing up front the way Go's Config.Validate does ("Missing required field in config: project_id", pkg/config/config.go:990-991, run unconditionally inside flags.LoadConfig before any Docker/API work). Pre-existing and cross-cutting, not introduced by this PR: deploy.ts's identical deployConfig?.project_id ?? projectRef fallback (deploy.ts:2201) has the same gap, and no native functions Docker path (deploy, serve, download) routes its loaded config through Config.Validate parity checks at all — that port has exactly one home today (legacy-config-validate.ts's legacyValidateResolvedConfig), wired up only for the db/migration loader and the status/stop resolver. Belongs in the shared config-loading layer every native caller goes through, not duplicated per Docker-path call site — left open (review round on this PR).

Verification

  • bun run test (full unit + integration suite) — all passing.
  • bun run --parallel "*:check" (types, lint, fmt, knip) — all passing.
  • Full reference-repointing grep sweep for stranded apps/cli-go/internal/functions/download references — none found; all remaining comments cite Go source that still exists and is still exercised by the --legacy-bundle path.

Ports `supabase functions download`'s default Docker-unbundle path
(`--use-docker`, default true) from wholesale Go-binary delegation to
native TypeScript, in both the legacy and next shells. `--use-api` was
already native; `--legacy-bundle` (hidden, deprecated pre-1.120.0
fallback requiring a host Deno-binary install with no precedent
elsewhere in this codebase) is deliberately left delegating to the Go
binary, per the parity-audit rationale recorded on the Linear issue.

Hoists the Docker-orchestration primitives `download.ts` needs
(`runChildProcess`, `isDockerRunning`, `ensureDockerNetwork`,
`ensureDockerNamedVolume`, `localDockerId`, `resolveEdgeRuntimeVersion`,
etc.) out of `deploy.ts` into a new `functions-docker.ts`, and
deduplicates the `edge-runtime-version` pin file lookup that was
copy-pasted across all four `deploy`/`download` handler files into a
single `resolveEdgeRuntimeVersionPin` helper.

Along the way, fixes:
- CLI-1891-class validation gap: slugs sourced from the Management
  API's function list weren't validated before the new Docker path's
  temp-file write, reopening a path-traversal vector Go's own
  `downloadAll` already guards against.
- The `next` shell's `--use-docker` flag was missing `Flag.withDefault(true)`,
  a real default-value divergence from both `legacy` and Go.
- A brotli-decompression bug: this CLI's HTTP transport already
  auto-decodes `Content-Encoding: br` responses (confirmed empirically),
  so re-running `brotliDecompressSync` on the eszip body threw on
  already-decoded bytes.
- Temp eszip cleanup only ran after a successful Docker run; wrapped in
  `Effect.ensuring` so it also runs on network/volume/spawn failures,
  matching Go's `defer`.
- The `.suggestion` field's leading newline (needed to reproduce Go's
  blank separator line before the `--legacy-bundle` hint) was being
  trimmed away by the generic CLI error normalizer.
@Coly010

Coly010 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6c6cb942b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/shared/functions/download.ts Outdated
Comment thread apps/cli/src/shared/functions/download.ts Outdated
Comment thread apps/cli/src/shared/functions/download.ts Outdated
Comment thread apps/cli/src/shared/functions/download.ts
Comment thread apps/cli/src/shared/functions/download.ts Outdated
Comment thread apps/cli/src/shared/functions/download.ts Outdated
Coly010 added 6 commits August 5, 2026 14:56
…view: CLI-1963)

Go's DockerStart only overrides the Docker network when
len(viper.GetString("network-id")) > 0 (internal/utils/docker.go:379-382).
The native functions download/deploy Docker paths used
explicitStringFlag(...) ?? localDockerId(...), which returns "" (not
undefined) for --network-id=, so an explicit empty override was invoked
verbatim instead of falling back to the generated network.

Adds explicitNonEmptyStringFlag (cobra-flag-groups.ts), which folds in Go's
len(value) > 0 gate, and switches both download.ts and deploy.ts's docker
network resolution to it.
…: CLI-1963)

Go's replaceImageTag (pkg/config/utils.go:81-84) appends the raw content of
supabase/.temp/edge-runtime-version verbatim after the image's `:`, so a pin
can legitimately already carry its own `v` prefix (both forms are exercised
elsewhere in this codebase, e.g. legacy-edge-runtime-image.unit.test.ts's
"v9.9.9" fixture vs. deploy.integration.test.ts's bare "9.9.9"). The native
download Docker path always prepended `v` to the resolved version, so a
v-prefixed pin produced `supabase/edge-runtime:vv9.9.9`, which Docker fails
to pull.

Hoists serve.ts's existing edgeRuntimeImageTag helper (which already handled
this correctly) into the shared functions-docker.ts, and applies it in
download.ts and deploy.ts, which had the same unprefixed-vs-prefixed bug in
their own inline `v${version}` construction.
…ON response (review: CLI-1963)

v1GetAFunctionBody's generated contract marks its response kind: "json", so
executeRaw() defaults to Accept: application/json for it (buildRequest's
unconditional acceptJson for json-kind operations). Go's own downloadOne
(the Docker-unbundle path this mirrors) sends no Accept header at all,
unlike the server-side path's explicit multipart/form-data override, so the
default JSON negotiation here could receive a negotiated JSON response
instead of the raw eszip bytes and fail downstream in edge-runtime unbundle.

Overrides the request's Accept header to */* (no preference) — the closest
equivalent this API surface has to Go sending no header.
…er (review: CLI-1963)

Go's Run calls flags.LoadConfig(fsys) unconditionally at the very top,
before checking useDocker or whether Docker itself is running
(download.go:135-138). The native download path only resolved/validated the
project config (via resolveEdgeRuntimeImage) inside the isDockerRunning()
branch, so a default `functions download` with an invalid
edge_runtime.deno_version proceeded straight to the API/filesystem
side-effecting server-side path whenever Docker was down or --use-api was
passed, instead of failing up front like Go.

Resolves resolveEdgeRuntimeImage unconditionally before branching on
--use-api/--use-docker/Docker's running state.
…eview: CLI-1963)

Go's DockerStart drops the named-volume bind entirely on Bitbucket
(internal/utils/docker.go:400-405) rather than just skipping its explicit
creation — `docker run -v <name>:...` would otherwise still implicitly
create the named volume, which Bitbucket's restricted Docker environment
doesn't allow. The native Docker-unbundle path's ensureDockerNamedVolume
already skipped the explicit `docker volume create` under
BITBUCKET_CLONE_DIR, but the manually-built `docker run -v ...` bind list
still unconditionally included the named-volume bind, so the container run
itself could still fail in Bitbucket's restricted environment.

Applies the same BITBUCKET_CLONE_DIR carve-out deploy.ts's buildDockerBinds
already uses.
…p (review: CLI-1963)

Go gates the Docker-unbundle path's temp-eszip cleanup on
viper.GetBool("DEBUG") (download.go:203), so an explicit --debug=false
resolves to false (cleanup runs). The native path used
hasGlobalLongFlag(rawArgs, "debug"), a presence-only check, so --debug=false
was treated the same as --debug and skipped cleanup — the opposite of Go.

Adds explicitBooleanLongFlag (cobra-flag-groups.ts), which reads the last
explicit occurrence's pflag-parsed boolean value instead of mere presence,
and switches this call site to it. SUPABASE_DEBUG env-var fallback remains
a separate, pre-existing gap shared by every other
hasGlobalLongFlag(rawArgs, "debug") site (e.g. deploy.ts) and the legacy
debug logger, left open rather than fixed piecemeal here.
@Coly010

Coly010 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51524c6206

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +875 to +877
image: legacyGetRegistryImageUrl(
`supabase/edge-runtime:${edgeRuntimeImageTag(edgeRuntimeVersion)}`,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Go's registry fallback for edge-runtime pulls

In the native Docker download path this resolves the edge-runtime image to a single legacyGetRegistryImageUrl value. I checked Go's downloadWithDockerUnbundle -> DockerRunOnceWithConfig -> DockerStart, which calls DockerResolveImageIfNotCached and tries ECR, GHCR, then Docker Hub when no registry override is set; when ECR is unavailable/throttled and the image is not already cached, docker run public.ecr.aws/... fails here instead of falling back to a later candidate, so the newly ported default download path regresses while the previous Go-delegated path would still succeed.

AGENTS.md reference: apps/cli/AGENTS.md:L249-L257

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed as a real gap — left open rather than fixed here; documented in a comment above resolveEdgeRuntimeImage's image: field and in the PR description's "Judgement calls left open" section.

You're right that Go's DockerStart resolves config.Image through DockerResolveImageIfNotCached (internal/utils/docker.go:326-348,363-365), which tries ECR→GHCR→Docker-Hub for every container it starts — including this exact edge-runtime unbundle container — so an ECR outage/throttle the previous Go-delegated default path would have survived can now fail this native path outright.

This isn't specific to download.ts, though: deploy.ts's and serve.ts's own already-shipped native Docker paths resolve the edge-runtime image identically — a single legacyGetRegistryImageUrl value, no retry. legacyGetRegistryImageUrlCandidates's ECR/GHCR/Docker-Hub retry has, per its own doc comment, only ever been wired up for start. Fixing it only for download here would leave the identical gap in two already-shipped commands and add asymmetry between three otherwise-parallel call sites — and since docker run here shells out to the Docker CLI rather than the Engine API DockerResolveImageIfNotCached uses, a real fix needs its own docker image inspect/pull probing step (something like start's image-prepull.ts), which is meaningfully more than a one-line change. Tracking this as a shared, cross-cutting follow-up across all three functions Docker paths rather than patching it piecemeal in this PR.

Comment on lines +875 to +877
image: legacyGetRegistryImageUrl(
`supabase/edge-runtime:${edgeRuntimeImageTag(edgeRuntimeVersion)}`,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Go's registry fallback for edge-runtime pulls

In the native Docker download path this resolves the edge-runtime image to a single legacyGetRegistryImageUrl value. I checked Go's downloadWithDockerUnbundle -> DockerRunOnceWithConfig -> DockerStart, which calls DockerResolveImageIfNotCached and tries ECR, GHCR, then Docker Hub when no registry override is set; when ECR is unavailable/throttled and the image is not already cached, docker run public.ecr.aws/... fails here instead of falling back to a later candidate, so the newly ported default download path regresses while the previous Go-delegated path would still succeed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed as a real gap — left open rather than fixed here; documented in a comment above resolveEdgeRuntimeImage's image: field and in the PR description's "Judgement calls left open" section.

You're right that Go's DockerStart resolves config.Image through DockerResolveImageIfNotCached (internal/utils/docker.go:326-348,363-365), which tries ECR→GHCR→Docker-Hub for every container it starts — including this exact edge-runtime unbundle container — so an ECR outage/throttle the previous Go-delegated default path would have survived can now fail this native path outright.

This isn't specific to download.ts, though: deploy.ts's and serve.ts's own already-shipped native Docker paths resolve the edge-runtime image identically — a single legacyGetRegistryImageUrl value, no retry. legacyGetRegistryImageUrlCandidates's ECR/GHCR/Docker-Hub retry has, per its own doc comment, only ever been wired up for start. Fixing it only for download here would leave the identical gap in two already-shipped commands and add asymmetry between three otherwise-parallel call sites — and since docker run here shells out to the Docker CLI rather than the Engine API DockerResolveImageIfNotCached uses, a real fix needs its own docker image inspect/pull probing step (something like start's image-prepull.ts), which is meaningfully more than a one-line change. Tracking this as a shared, cross-cutting follow-up across all three functions Docker paths rather than patching it piecemeal in this PR.

Comment thread apps/cli/src/shared/functions/download.ts
Comment thread apps/cli/src/shared/functions/download.ts
Coly010 added 2 commits August 5, 2026 16:57
… mode (review: CLI-1963)

Go's container.NetworkMode.IsUserDefined() explicitly excludes IsContainer()
(docker/api/types/container/hostconfig_unix.go:23-25), so DockerNetworkCreateIfNotExists
never inspects or creates a network for --network-id container:<name|id> — the mode
attaches to another container's stack and is passed straight through to `docker run
--network`. The shared isUserDefinedDockerNetwork predicate (used by deploy.ts,
serve.ts, download.ts, and start's container lifecycle) didn't exclude this case, so
the Docker download path's preflight would have run `docker network inspect`/`create
container:redis` before `docker run`. Fixed once in the shared predicate so every
consumer gets the same fix.
…workdir, toml-only (review: CLI-1963)

Go's flags.LoadConfig only ever resolves supabase/config.toml from the already-resolved
workdir, with no ancestor climb and no concept of a JSON project config
(pkg/config/utils.go:43-48). resolveEdgeRuntimeImage's loadProjectConfig call omitted
search: false/tomlOnly: true, so the legacy shell's Docker download path could pick up
an unrelated ancestor project's config.toml, or prefer a stray supabase/config.json over
config.toml — both diverging from Go. Gated on goViperCompat so the next shell keeps the
package's existing (non-Go-parity) defaults, matching legacy-local-project-context.ts and
start.handler.ts's established pattern for the same options.

Also documents (not fixed here) a separate, pre-existing gap the same review round
surfaced: resolveEdgeRuntimeImage resolves a single registry URL with no ECR/GHCR/Docker
Hub retry, unlike Go's DockerResolveImageIfNotCached — shared with deploy.ts/serve.ts's
own already-shipped native Docker paths, so it's a cross-cutting follow-up rather than a
download-only fix.
@Coly010

Coly010 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Note for whoever picks this up next: this PR now has a merge conflict with develop in apps/cli/docs/go-cli-porting-status.md only — the "Partially ported commands" summary row/percentages, which both this PR and the already-merged CLI-1967 doc-drift-fix (#6074) touch. Not resolving it as part of this pass since it's outside the scope of adjudicating the 4 open review threads and needs a human call on which counts are current; flagging rather than auto-resolving per the merge-conflict safety rule.

…3-port-functions-download-to-native-typescript-both-shells

# Conflicts:
#	apps/cli/docs/go-cli-porting-status.md
@Coly010

Coly010 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

const slugs = Option.isSome(flags.functionName)
? [flags.functionName.value]
: yield* listRemoteFunctionSlugs(dependencies.api, projectRef);

P2 Badge Validate config before legacy-bundle pre-list

For --legacy-bundle with TS machine output and no function name, this branch lists remote functions before it delegates to the Go child, but Go's download.Run calls flags.LoadConfig before choosing RunLegacy or making the list request. Fresh evidence is this legacy-bundle machine branch still pre-lists here, so an invalid supabase/config.toml can now perform or mask an API list before the config error that the previous Go-delegated invocation reported first.

AGENTS.md reference: apps/cli/AGENTS.md:L249-L257

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +859 to +863
const loadedConfig = yield* loadProjectConfig(dependencies.projectRoot, {
projectRef,
goViperCompat: dependencies.goViperCompat,
// `search: false`/`tomlOnly: true` only under `goViperCompat` (the legacy caller, whose
// `dependencies.projectRoot` is `cliConfig.workdir` — already Go's fully-resolved chdir

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load Go defaults when config.toml is absent

For the legacy Docker download path in a project that has no supabase/config.toml, this package loader returns null before loading Go's default config/environment layer, whereas Go's flags.LoadConfig still loads defaults and nested env files before any API or Docker work. In practice, SUPABASE_EDGE_RUNTIME_DENO_VERSION=1 (from the environment or supabase/.env) is honored by the previous Go-delegated default path but is ignored here, so the new native path can run the v2 edge-runtime image against a project that Go would unbundle with the deno-v1 image.

AGENTS.md reference: apps/cli/AGENTS.md:L249-L257

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed as a real gap — left open rather than fixed here, same as the registry-fallback thread above.

Verified in Go source: flags.LoadConfigConfig.LoadloadFromFile (pkg/config/config.go:579-611) merges the template defaults and enables viper.AutomaticEnv()/SetEnvPrefix("SUPABASE") before attempting the file read, and mergeFileConfig (config.go:701-716) simply returns nil on os.ErrNotExist — so Go still resolves SUPABASE_EDGE_RUNTIME_DENO_VERSION/supabase/.env even with no config.toml on disk. loadProjectConfig here returns null outright in that case, so denoVersion falls through to undefined and always resolves the v2 default.

But this isn't specific to the "config.toml absent" case, and it isn't specific to download.ts: @supabase/config's loadProjectConfig has no equivalent of Go's generic ExperimentalBindStruct+AutomaticEnv struct-field binding at all — it only expands literal env(...) references already written inside the TOML. deploy.ts's already-shipped native Docker path calls the exact same resolveEdgeRuntimeVersion(deployConfig?.edge_runtime.deno_version, ...) and has the identical gap whether or not config.toml exists.

A fix belongs in the shared config-loading layer every native caller goes through (gen types, next start, functions dev/serve/deploy, and now download), not duplicated per call site in this PR. Documented inline above resolveEdgeRuntimeImage's denoVersion read and in the PR description's "Judgement calls left open" section.

Comment on lines +989 to +990
explicitNonEmptyStringFlag(dependencies.rawArgs, "network-id") ??
localDockerId("network", projectId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor SUPABASE_NETWORK_ID for Docker downloads

When legacy users set SUPABASE_NETWORK_ID instead of passing --network-id, Go's root viper binding resolves that environment variable and DockerStart overrides the network whenever viper.GetString("network-id") is non-empty. This raw-argv-only lookup ignores that supported env path, so CI or local setups that provide the custom network through SUPABASE_NETWORK_ID now run the edge-runtime unbundle on the generated supabase_network_<project> network instead of the network the previous Go-delegated default path joined.

AGENTS.md reference: apps/cli/AGENTS.md:L249-L257

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed as a real gap — left open rather than fixed here, same as the registry-fallback thread above.

Verified in Go source: root init() enables viper.AutomaticEnv() with SetEnvPrefix("SUPABASE") and a -_ replacer, then binds every persistent flag — including --network-id — via viper.BindPFlags (cmd/root.go:316-334). With that binding, viper.GetString("network-id") resolves SUPABASE_NETWORK_ID whenever --network-id itself isn't passed on the command line, ahead of the flag's empty default. This raw-argv-only explicitNonEmptyStringFlag lookup has no equivalent env-var fallback.

But this isn't specific to download.ts either: no native command resolves SUPABASE_NETWORK_ID today. start.handler.ts's LegacyNetworkIdFlag (an Effect CLI flag with no env binding) and deploy.ts's/serve.ts's own network-id resolution have the identical gap — including start, the most heavily-audited native port in this codebase.

A fix belongs in one shared place for the global --network-id resolution (mirroring how Go resolves it once, at the root, for every command), not duplicated per Docker-path call site in this PR. Documented inline above the networkMode resolution and in the PR description's "Judgement calls left open" section.

… gaps (review: CLI-1963)

Codex flagged that resolveEdgeRuntimeImage falls back to the v2 default
when config.toml is absent (ignoring SUPABASE_EDGE_RUNTIME_DENO_VERSION),
and that networkMode resolution never checks SUPABASE_NETWORK_ID the way
Go's viper AutomaticEnv does for the --network-id persistent flag.

Both are confirmed real gaps, but pre-existing and cross-cutting rather
than introduced here: deploy.ts has the identical deno_version fallback
today (config.toml present or not, since @supabase/config has no generic
env-var struct binding at all), and start.handler.ts/deploy.ts/serve.ts's
own network-id resolution don't check SUPABASE_NETWORK_ID either. Fixing
either belongs in one shared place, not duplicated per Docker-path call
site in download.ts alone -- left open, matching this PR's existing
precedent for the registry-fallback gap. Documented inline and in the PR
description's "Judgement calls left open" section instead of silently
resolving the review threads.
@Coly010

Coly010 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4530b1fcb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/shared/cli/cobra-flag-groups.ts Outdated
Comment thread apps/cli/src/shared/functions/download.ts
// per call site here — left open (review round on CLI-1963's `functions
// download` port).
const denoVersion = loadedConfig?.config?.edge_runtime.deno_version;
const projectId = loadedConfig?.config?.project_id ?? projectRef;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject Go-invalid configs before Docker downloads

When a legacy project has a Go-invalid config such as project_id = "", loadProjectConfig still returns the empty string and this nullish fallback does not use projectRef, so the new default Docker download path proceeds with supabase_network_/supabase_edge_runtime_ instead of failing flags.LoadConfig with Missing required field in config: project_id before any Docker/API work. This is separate from the earlier load-order fix: even after resolveEdgeRuntimeImage runs, the decoded config is not passed through Go's Config.Validate parity checks, including configured function slugs; route legacy callers through the shared validator before using the config.

AGENTS.md reference: apps/cli/AGENTS.md:L241-L243

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed as a real gap — left open rather than fixed here, same treatment as the registry-fallback/config-defaults/network-id-env threads above.

Verified in Go source: Config.Validate (pkg/config/config.go:990-991) rejects project_id = "" with "Missing required field in config: project_id", run unconditionally inside flags.LoadConfig before Run ever checks useDocker/Docker's running state — before any Docker/API work. This native path's loadedConfig?.config?.project_id ?? projectRef only substitutes on null/undefined, so an explicit empty project_id sails through to supabase_network_/supabase_edge_runtime_ naming instead.

This is pre-existing and cross-cutting, not specific to this PR's new code: deploy.ts's identical deployConfig?.project_id ?? projectRef fallback (deploy.ts:2201) has the exact same gap, and no native functions Docker path (deploy, serve, download) routes its loaded config through Config.Validate parity checks at all today — that port has exactly one home, legacy-config-validate.ts's legacyValidateResolvedConfig, wired up only for the db/migration loader and the status/stop resolver. Wiring Config.Validate into every native config-consuming command belongs in the shared config-loading layer, not duplicated per Docker-path call site.

Documented in a comment above resolveEdgeRuntimeImage's projectId field (7834238) and added to the PR description's "Judgement calls left open" section.

Coly010 added 3 commits August 5, 2026 19:44
… flag (review: CLI-1963)

pflag/viper string flags are shared-variable, last-Set()-wins (confirmed
empirically with a scratch pflag.FlagSet.Parse probe: --network-id old
--network-id ci-net resolves to ci-net; a trailing --network-id= clears an
earlier non-empty value). explicitStringFlag returned on the first argv
match instead of scanning for the last, unlike this file's own
explicitBooleanLongFlag and the legacy shell's legacyPflagStringValue,
which already implement last-wins. Fixed to keep scanning, plus regression
tests covering the repeated-override and repeated-then-cleared cases.
…opping them (review: CLI-1963)

Go's FunctionResponse.Slug (apps/cli-go/pkg/api/types.gen.go:6465) is a
required, non-pointer string: a list entry with a missing or null "slug"
decodes to the zero value "" rather than erroring, and that empty slug
then fails ValidateFunctionSlug loudly in downloadAll
(download.go:182-188) instead of vanishing from the list. listRemoteFunctionSlugs's
flatMap filtered such entries out entirely, defeating part of the
CLI-1891 validation this PR added for exactly this "compromised/malformed
API response" threat model. Preserve the entry (coerced to "") so the
existing validateRemoteSlug/validateSlug check catches it, matching Go
instead of reporting "No functions found." or a silent partial download.
…ad configs (review: CLI-1963)

Go's Config.Validate (pkg/config/config.go:990-991) rejects a config.toml
with project_id = "" up front, inside flags.LoadConfig, before any
Docker/API work. resolveEdgeRuntimeImage's `?? projectRef` fallback only
substitutes on null/undefined, so an explicit empty project_id sails
through instead. Pre-existing and cross-cutting, not specific to this PR:
deploy.ts's identical deployConfig?.project_id ?? projectRef fallback
(deploy.ts:2201) has the same gap, and no native functions Docker path
(deploy/serve/download) routes its config through Config.Validate parity
checks at all -- that port has one home today
(legacy-config-validate.ts's legacyValidateResolvedConfig), wired up only
for the db/migration loader and status/stop resolver. Left open, same
treatment as the registry-fallback/config-defaults/network-id-env gaps
already documented above -- belongs in the shared config-loading layer
every native caller goes through, not duplicated per call site.
@Coly010

Coly010 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant