-
Notifications
You must be signed in to change notification settings - Fork 514
feat(cli): add supabase workers push #6262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { describe, expect, test } from "vitest"; | ||
| import { legacyRedactHttpUrl } from "./legacy-http-debug.layer.ts"; | ||
|
|
||
| /** | ||
| * `--debug` logs every request URL to stderr. For a presigned object-store URL | ||
| * the query string *is* the credential — for the Workers build-context upload, | ||
| * one that authorizes overwriting the archive a deploy is about to build from — | ||
| * so it must not survive into scrollback or a CI log. | ||
| */ | ||
| describe("legacyRedactHttpUrl", () => { | ||
| test.each([ | ||
| [ | ||
| "an AWS presigned upload", | ||
| "https://store.example/bucket/ctx.tar.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=deadbeef", | ||
| "https://store.example/bucket/ctx.tar.gz?<redacted>", | ||
| ], | ||
| [ | ||
| "a GCS presigned upload", | ||
| "https://store.example/bucket/ctx.tar.gz?X-Goog-Signature=deadbeef", | ||
| "https://store.example/bucket/ctx.tar.gz?<redacted>", | ||
| ], | ||
| [ | ||
| "a lowercase signature parameter", | ||
| "https://store.example/o/ctx?signature=deadbeef&expires=123", | ||
| "https://store.example/o/ctx?<redacted>", | ||
| ], | ||
| [ | ||
| "a bare token parameter", | ||
| "https://store.example/o/ctx?token=deadbeef", | ||
| "https://store.example/o/ctx?<redacted>", | ||
| ], | ||
| ])("redacts the query string of %s", (_label, url, expected) => { | ||
| expect(legacyRedactHttpUrl(url)).toBe(expected); | ||
| expect(legacyRedactHttpUrl(url)).not.toContain("deadbeef"); | ||
| }); | ||
|
|
||
| // The debug log is only useful if ordinary requests still read normally, so | ||
| // redaction has to be the exception rather than the rule. | ||
| test.each([ | ||
| ["a Management API route", "https://api.supabase.com/v2/projects/abc/workers/api"], | ||
| ["an ordinary query string", "https://api.supabase.com/v1/projects?limit=10"], | ||
| ["a URL with no query at all", "https://api.supabase.com/v1/projects"], | ||
| ])("leaves %s untouched", (_label, url) => { | ||
| expect(legacyRedactHttpUrl(url)).toBe(url); | ||
| }); | ||
|
|
||
| test("passes through something that is not a parseable URL", () => { | ||
| expect(legacyRedactHttpUrl("not a url at all")).toBe("not a url at all"); | ||
| }); | ||
|
|
||
| test("keeps the path, which is what makes the log line worth having", () => { | ||
| expect(legacyRedactHttpUrl("https://store.example/bucket/deep/ctx.tar.gz?sig=x")).toContain( | ||
| "/bucket/deep/ctx.tar.gz", | ||
| ); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # `supabase workers push [name...] (alias: deploy)` | ||
|
|
||
| > **No live test yet.** `workers` runs against the v2 Management API, which the | ||
| > supabase/cli-e2e-ci supabox stack is not expected to serve, so a `*.live.test.ts` | ||
| > here would be permanently skipped or permanently red. Revisit when the v2 | ||
| > Workers routes are available on that stack. | ||
|
|
||
| ## Files Read | ||
|
|
||
| | Path | Format | When | | ||
| | ---------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------- | | ||
| | `<workdir>/supabase/config.toml` | TOML | always, for each worker's runtime, size, source | | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a project contains AGENTS.md reference: apps/cli/AGENTS.md:L359-L366 Useful? React with 👍 / 👎. |
||
| | `<worker source>/**` | any | always — packaged into the build context | | ||
| | `<SUPABASE_HOME or ~/.supabase>/profile` | plain text | when neither `--profile` nor `SUPABASE_PROFILE` is set — names the profile, defaulting to `supabase` | | ||
| | `<SUPABASE_PROFILE>` (YAML) | YAML | when `SUPABASE_PROFILE` is a filesystem path rather than a built-in name; a read failure aborts the command | | ||
|
|
||
| ## Files Written | ||
|
|
||
| | Path | Format | When | | ||
| | ----------------------------------------------- | ------ | --------------------------------------------------------------- | | ||
| | `<SUPABASE_HOME or ~/.supabase>/telemetry.json` | JSON | always — flushed on success and on failure | | ||
| | `<workdir>/supabase/.temp/linked-project.json` | JSON | after the project ref resolves, when the cache does not hold it | | ||
|
|
||
| ## API Routes | ||
|
|
||
| | Method | Path | Auth | Request body | Response (used fields) | | ||
| | ------ | -------------------------------------------- | ------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------ | | ||
| | `POST` | `/v2/projects/{ref}/workers/{name}/uploads` | Bearer token | none | `data.id`, `data.attributes.url/method` | | ||
| | `PUT` | presigned upload URL (control-plane storage) | URL signature — **no** Supabase credentials | `.tar.gz` build context | status only | | ||
| | `POST` | `/v2/projects/{ref}/workers/{name}/deploy` | Bearer token | `{data:{type,attributes:{spec,context_upload_id}}}` | `data.attributes.build_state` | | ||
| | `GET` | `/v2/projects/{ref}/workers/{name}` | Bearer token | none | `build_state`, `state_reason`, `image_version`, `spec` | | ||
| | `GET` | `/v1/projects/{ref}` | Bearer token | none | linked-project cache miss only — name, org, region | | ||
|
|
||
| `GET` is polled until `build_state` leaves `building`. | ||
|
|
||
| ## Exit Codes | ||
|
|
||
| | Code | Condition | | ||
| | ---- | ---------------------------------------------------- | | ||
| | `0` | success | | ||
| | `1` | no workers named and none found in the project | | ||
| | `1` | a worker's source directory is missing or empty | | ||
| | `1` | build context upload failed | | ||
| | `1` | the build reached `failed`, or never left `building` | | ||
| | `1` | API error, or project not enrolled in the alpha | | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| | Variable | Purpose | Required? | | ||
| | ----------------------- | ---------------------------------------------------- | ------------------------------------------------------- | | ||
| | `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | | ||
| | `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | | ||
| | `SUPABASE_WORKDIR` | project directory the command acts on | no (falls back to `--workdir`, then the ancestor walk) | | ||
| | `SUPABASE_HOME` | directory holding `telemetry.json` | no (falls back to `~/.supabase`) | | ||
|
|
||
| ## Telemetry Events Fired | ||
|
|
||
| | Event | When | Notable properties / groups | | ||
| | ---------------------- | ------------------------------------------ | ----------------------------------- | | ||
| | `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` | | ||
|
|
||
| No custom events — only the `cli_command_executed` that the instrumentation | ||
| wrapper emits for every command. | ||
|
|
||
| ## Output Formats | ||
|
|
||
| `-o env` is refused **before** the first deploy rather than at emit time: the | ||
| payload always carries a `workers` array, which a flat `KEY=value` list cannot | ||
| express, and discovering that at the end would fail the command with the remote | ||
| project already changed. | ||
|
|
||
| The presigned `PUT` above is the one request whose URL is itself a credential. | ||
| `--debug` logs every request URL, so `legacyHttpClientLayer` redacts query | ||
| strings that carry a signature. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { Argument, Command, Flag } from "effect/unstable/cli"; | ||
| import type * as CliCommand from "effect/unstable/cli/Command"; | ||
| import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts"; | ||
| import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts"; | ||
| import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts"; | ||
| import { legacyWorkersPush } from "./push.handler.ts"; | ||
|
|
||
| const config = { | ||
| names: Argument.string("name").pipe( | ||
| Argument.withDescription("Workers to deploy. Deploys every worker in the project if omitted."), | ||
| Argument.variadic(), | ||
| ), | ||
| instances: Flag.integer("instances").pipe( | ||
| // Bounded at the parser, the same way `[workers.<name>] instances` is bounded | ||
| // in the config schema. Left unchecked it reached the deploy endpoint — after | ||
| // the build context had been packaged and uploaded — as a scaling request the | ||
| // platform cannot honour. | ||
| Flag.filter( | ||
| (instances) => instances >= 0, | ||
| (instances) => `--instances ${instances} is negative; pass zero or more.`, | ||
| ), | ||
| Flag.withDescription( | ||
| "Number of instances to run, overriding `instances` in supabase/config.toml for this deploy. Falls back to the recorded value, then 1.", | ||
| ), | ||
| Flag.optional, | ||
|
johnstonmatt marked this conversation as resolved.
|
||
| ), | ||
| projectRef: Flag.string("project-ref").pipe( | ||
| Flag.withDescription("Project ref of the Supabase project."), | ||
| Flag.optional, | ||
| ), | ||
| } as const; | ||
|
|
||
| export type LegacyWorkersPushFlags = CliCommand.Command.Config.Infer<typeof config>; | ||
|
|
||
| export const legacyWorkersPushCommand = Command.make("push", config).pipe( | ||
|
johnstonmatt marked this conversation as resolved.
|
||
| Command.withAlias("deploy"), | ||
| Command.withDescription( | ||
| "Build and deploy workers into the linked Supabase project. Reads each worker's runtime, size and source directory from supabase/config.toml.", | ||
| ), | ||
| Command.withShortDescription("Build and deploy workers"), | ||
| Command.withExamples([ | ||
| { | ||
| command: "supabase workers push", | ||
| description: "Deploy every worker in the project", | ||
| }, | ||
| { | ||
| command: "supabase workers push api", | ||
| description: "Deploy a single worker", | ||
| }, | ||
| { | ||
| command: "supabase workers push api web", | ||
| description: "Deploy several workers by name", | ||
| }, | ||
| ]), | ||
| Command.withHandler((flags) => | ||
| legacyWorkersPush(flags).pipe( | ||
| withLegacyCommandInstrumentation({ flags }), | ||
| withJsonErrorHandling, | ||
| ), | ||
| ), | ||
| Command.provide(legacyManagementApiRuntimeLayer(["workers", "push"])), | ||
| ); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After the prior cache and telemetry additions, the compatibility document still omits side effects performed by the resolver and auth layers:
legacy-project-ref.layer.ts:87-95consumesSUPABASE_PROJECT_ID, reads<workdir>/supabase/.temp/project-ref, and may callGET /v1/projectsfor interactive selection, whilelegacy-credentials.layer.ts:403-443reads the profile and legacy keyring entries or<SUPABASE_HOME>/access-token. These happen before the Workers requests and leave the Files Read, API Routes, and Environment Variables sections incomplete.AGENTS.md reference: apps/cli/AGENTS.md:L359-L366
Useful? React with 👍 / 👎.