Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
d4e37d5
feat(cli): prompt for worker name if not provided
johnstonmatt Aug 29, 2026
edc7199
fix(cli): gate the workers new prompts on stdin too
johnstonmatt Aug 29, 2026
93c7aa6
feat(cli): prompt for worker name if not provided
johnstonmatt Aug 29, 2026
5c84df0
fix(cli): gate the workers new prompts on stdin too
johnstonmatt Aug 29, 2026
0af0f0a
chore(workers): describe behaviour rather than its history in comments
johnstonmatt Aug 31, 2026
a74e3f9
feat(workers): bring the command family's output onto one shape
johnstonmatt Aug 29, 2026
548a185
test(cli): guard every legacy boolean flag against a required default
johnstonmatt Aug 29, 2026
1d7e06b
chore(workers): describe behaviour rather than its history in comments
johnstonmatt Aug 31, 2026
1a2236d
feat(workers logs): add `supabase workers logs`
johnstonmatt Aug 31, 2026
c4080de
feat(workers logs): print timestamps in local time
johnstonmatt Aug 31, 2026
b7c9864
feat(workers logs): add `--follow` to keep printing new lines
johnstonmatt Aug 31, 2026
2f68ad6
feat: show stream tags in worker logs when multiple sources
johnstonmatt Aug 31, 2026
c66065d
chore(workers): describe behaviour rather than its history in comments
johnstonmatt Aug 31, 2026
acbd6ff
feat(workers push): make build wait opt-in with `--wait` flag
johnstonmatt Aug 29, 2026
88e4832
Merge branch 'FUNC-840/select-workers-new-name' of https://github.com…
johnstonmatt Sep 1, 2026
9ae1f38
fix(cli): point the workers new retry at the experimental path
johnstonmatt Sep 1, 2026
12c4d1c
fix(cli): stop telling users to run a command that does not exist
johnstonmatt Sep 1, 2026
94d1e89
Merge FUNC-840/select-workers-new-name into FUNC-851/general-output-p…
johnstonmatt Sep 1, 2026
4f05bc5
Merge FUNC-851/general-output-polish into FUNC-853/workers-logs-command
johnstonmatt Sep 1, 2026
0bfaca8
Merge FUNC-853/workers-logs-command into FUNC-848/workers-deploy-wait…
johnstonmatt Sep 1, 2026
e024627
test(cli): name the real push invocation in the flag-default guard
johnstonmatt Sep 1, 2026
1f103f9
fix(workers): restore the experimental segment in worker span names
johnstonmatt Sep 1, 2026
de59392
fix(workers): stop echoing an empty --project-ref into retry suggestions
johnstonmatt Sep 1, 2026
d99e6b5
fix(workers): keep push's per-worker progress out of structured formats
johnstonmatt Sep 1, 2026
71af01d
docs(workers): mark the status and delete trailers as text-only
johnstonmatt Sep 1, 2026
11395a3
refactor(workers): rename workers logs --source to --kind
johnstonmatt Sep 1, 2026
6f8770d
docs(workers): mark the logs no-logs hint as text-only
johnstonmatt Sep 1, 2026
46321b4
fix(workers logs): tie the --kind choices to the stream map
johnstonmatt Sep 1, 2026
396498b
fix(workers logs): give -o priority over --output-format
johnstonmatt Sep 1, 2026
d949afd
fix(workers logs): flush telemetry when the project ref cannot resolve
johnstonmatt Sep 1, 2026
335b62a
fix(workers logs): let a Ctrl+C'd tail run its finalizers
johnstonmatt Sep 1, 2026
2597a39
fix(workers logs): stop --tail capping the follow poll
johnstonmatt Sep 1, 2026
f8575ee
fix(workers logs): make --tail 0 --follow mean what it says
johnstonmatt Sep 1, 2026
a00a02c
fix(workers logs): sanitise every rendered field, not just the guest …
johnstonmatt Sep 1, 2026
d06c876
refactor(workers logs): prefix the exported log-level type
johnstonmatt Sep 1, 2026
7009bd6
docs(workers logs): document the SIGINT exit code
johnstonmatt Sep 1, 2026
af1f813
fix(workers logs): bound ts_ms to a representable instant
johnstonmatt Sep 1, 2026
fa318ee
fix(workers logs): retry only the poll failures worth another request
johnstonmatt Sep 1, 2026
468d9e2
fix(workers logs): carry the composed line into stream-json
johnstonmatt Sep 1, 2026
008f160
Merge branch 'develop' of https://github.com/supabase/cli into FUNC-8…
johnstonmatt Sep 1, 2026
b3ea7df
Merge branch 'FUNC-840/select-workers-new-name' of https://github.com…
johnstonmatt Sep 1, 2026
c8977d0
fix(workers logs): green the checks the logs command turned red
johnstonmatt Sep 1, 2026
e0cd17c
Merge branch 'FUNC-853/workers-logs-command' into FUNC-848/workers-de…
johnstonmatt Sep 1, 2026
993fa7e
Merge remote-tracking branch 'origin/FUNC-851/general-output-polish' …
johnstonmatt Sep 1, 2026
be45513
Merge branch 'FUNC-853/workers-logs-command' into FUNC-848/workers-de…
johnstonmatt Sep 1, 2026
b772491
test(config): stop a broken pipe preempting the exit-code diagnosis
johnstonmatt Sep 1, 2026
6a0bab9
Merge branch 'FUNC-853/workers-logs-command' into FUNC-848/workers-de…
johnstonmatt Sep 1, 2026
881a886
feat: add exposure control for worker deployments
johnstonmatt Sep 2, 2026
2f4f766
feat: add --instances flag to workers new command
johnstonmatt Sep 2, 2026
b9c58bf
refactor(workers push): flip the build wait to an opt-out `--no-wait`
johnstonmatt Sep 3, 2026
486a47b
fix(workers push): honour a terminal deploy response instead of polling
johnstonmatt Sep 3, 2026
26722c0
Merge remote-tracking branch 'origin/develop' into FUNC-853/workers-l…
johnstonmatt Sep 3, 2026
603d8ed
Merge branch 'FUNC-853/workers-logs-command' into FUNC-848/workers-de…
johnstonmatt Sep 3, 2026
21b144d
refactor(workers logs): drop the unused window seams and test followW…
johnstonmatt Sep 3, 2026
57f6249
fix(workers logs): report a burst the follow drain could not reach
johnstonmatt Sep 3, 2026
f98b1e0
Merge branch 'FUNC-853/workers-logs-command' into FUNC-848/workers-de…
johnstonmatt Sep 3, 2026
213f804
Revert "test(config): stop a broken pipe preempting the exit-code dia…
johnstonmatt Sep 3, 2026
a19dc4d
Merge branch 'FUNC-853/workers-logs-command' into FUNC-848/workers-de…
johnstonmatt Sep 3, 2026
f9647a0
fix(workers push): report in-flight builds and drop a stale image
johnstonmatt Sep 3, 2026
539986a
Merge remote-tracking branch 'origin/develop' into FUNC-848/workers-d…
johnstonmatt Sep 3, 2026
49cea1d
Merge branch 'FUNC-848/workers-deploy-wait-flag' into FUNC-859/enable…
johnstonmatt Sep 4, 2026
4d0cc08
fix(workers): register --exposure as a value-consuming flag
johnstonmatt Sep 4, 2026
2f05c7b
Merge remote-tracking branch 'origin/develop' into FUNC-859/enable-pr…
johnstonmatt Sep 4, 2026
35ebbaf
fix(workers): refuse a blank recorded exposure instead of defaulting it
johnstonmatt Sep 4, 2026
ce7c54d
fix(workers): refuse a config number that renders as TOML the schema …
johnstonmatt Sep 4, 2026
d9eca62
test(workers new): cover the exposure and instance output, and fix th…
johnstonmatt Sep 4, 2026
d67d67b
feat(workers push): say when --exposure will not outlive the deploy
johnstonmatt Sep 4, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@

## Files Written

| Path | Format | When |
| ----------------------------------------------- | ------ | -------------------------------------------------------------------------- |
| `<workdir>/supabase/config.toml` | TOML | on success — appends `[workers.<name>]`, preserving surrounding formatting |
| `<workdir>/supabase/workers/<name>/*` | varies | on success, unless `--source` names another directory |
| `<workdir>/<source>/*` | varies | on success, when `--source` is given |
| `<SUPABASE_HOME or ~/.supabase>/telemetry.json` | JSON | whenever the handler runs — flushed on success and on failure |
| Path | Format | When |
| ----------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<workdir>/supabase/config.toml` | TOML | on success — appends `[workers.<name>]` with `runtime`, `size` and `exposure` always, `instances` only when it differs from the default of 1, and `source` only when `--source` was passed, preserving surrounding formatting |
| `<workdir>/supabase/workers/<name>/*` | varies | on success, unless `--source` names another directory |
| `<workdir>/<source>/*` | varies | on success, when `--source` is given |
| `<SUPABASE_HOME or ~/.supabase>/telemetry.json` | JSON | whenever the handler runs — flushed on success and on failure |

Workers are recorded in `config.toml` only. The project config loader prefers
`supabase/config.json` when one exists, but the entry writer is a TOML text
Expand All @@ -40,13 +40,22 @@ prompt refuses a name that is not a DNS label or that `config.toml` already
records — so nothing is asked, and nothing written, for a name the command was
going to refuse. With `-o json|yaml|toml|env`, a redirected stdout, or a stdin
that is not a terminal, there is nowhere to ask, and the command fails instead
of defaulting: unlike the runtime and size, the name has no default to fall back
on. Every prompt is gated on both streams, so
of defaulting: unlike the runtime, size and exposure, the name has no default to
fall back on. Every prompt is gated on both streams, so
`printf 'api\n' | supabase experimental workers new` takes that failure path
rather than reading the worker name off the pipe.

`runtime`, `size` and `exposure` are always written, defaults included: they are
closed sets the command prompts for, and pinning the answer is the point of
recording it. `instances` is written only when it differs from the default of 1 —
it has no prompt, because how many instances a worker needs is not something a
scaffold can guess, and an absent `instances` means exactly what `instances = 1`
means to `push`. A `0` is an explicit count that scales the worker to nothing, so
it is written like any other. It is rendered as a bare TOML number rather than a
quoted string, because the config schema types it as a number.

Writes to `config.toml` are append-only. A worker already recorded under
`[workers.<name>]` is refused outright — before the runtime and size prompts,
`[workers.<name>]` is refused outright — before the dial prompts,
and before anything reaches disk — because editing an entry the user owns is
not this command's job.

Expand Down Expand Up @@ -95,7 +104,8 @@ No custom events — only the `cli_command_executed` that the instrumentation
wrapper emits for every command.

Nothing is emitted for a failure the parser catches, such as a
`--runtime`/`--size` value outside the choice list. The wrapper is installed by
`--runtime`/`--size`/`--exposure` value outside the choice list, or a negative
`--instances`. The wrapper is installed by
`Command.withHandler`, so a command that never reaches its handler never reaches
the instrumentation either — and `telemetry.json` is not written. A missing name
is _not_ one of those: the argument is optional, so a bare `workers new` reaches
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ 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 { commandRuntimeLayer } from "../../../../../shared/runtime/command-runtime.layer.ts";
import { WORKER_RUNTIMES, WORKER_SIZES } from "../../../../../shared/workers/worker-runtimes.ts";
import {
WORKER_EXPOSURES,
WORKER_RUNTIMES,
WORKER_SIZES,
} from "../../../../../shared/workers/worker-runtimes.ts";
import { legacyCliSettingsLayer } from "../../../../config/legacy-cli-settings.layer.ts";
import { legacyDebugLoggerLayer } from "../../../../shared/legacy-debug-logger.layer.ts";
import { legacyTelemetryStateLayer } from "../../../../telemetry/legacy-telemetry-state.layer.ts";
Expand All @@ -29,6 +33,24 @@ const config = {
),
Flag.optional,
),
exposure: Flag.choice("exposure", WORKER_EXPOSURES).pipe(
Flag.withDescription(
"Whether the worker is reachable from the internet, recorded as `exposure` in supabase/config.toml. Prompted when omitted.",
),
Flag.optional,
),
instances: Flag.integer("instances").pipe(
// Bounded at the parser, the same way `push --instances` and the config
// schema's own `instances` are.
Flag.filter(
(instances) => instances >= 0,
(instances) => `--instances ${instances} is negative; pass zero or more.`,
),
Flag.withDescription(
"Number of instances to record in supabase/config.toml. Not prompted for, and recorded only when it differs from the default of 1.",
),
Flag.optional,
),
source: Flag.string("source").pipe(
Flag.withDescription(
"Scaffold the worker here instead of the default workers directory, recorded as `source` in supabase/config.toml.",
Expand All @@ -50,22 +72,30 @@ const legacyWorkersNewRuntimeLayer = Layer.mergeAll(

export const legacyWorkersNewCommand = Command.make("new", config).pipe(
Command.withDescription(
"Scaffold a worker directory from a runtime's starter files and record the choice in supabase/config.toml. Nothing is deployed.",
"Scaffold a worker directory from a runtime's starter files and record the choices in supabase/config.toml. Nothing is deployed.",
),
Command.withShortDescription("Scaffold a worker locally"),
Command.withExamples([
{
command: "supabase experimental workers new",
description: "Prompt for the name, then for runtime and size",
description: "Prompt for the name, then for runtime, size and exposure",
},
{
command: "supabase experimental workers new api",
description: "Scaffold supabase/workers/api, prompting for runtime and size",
description: "Scaffold supabase/workers/api, prompting for runtime, size and exposure",
},
{
command: "supabase experimental workers new api --runtime node",
description: "Scaffold supabase/workers/api on the node runtime",
},
{
command: "supabase experimental workers new api --exposure private",
description: "Scaffold a worker with no internet-facing URL",
},
{
command: "supabase experimental workers new api --instances 3",
description: "Scaffold a worker that deploys at three instances",
},
{
command: "supabase experimental workers new api --source packages/api",
description: "Scaffold the worker outside the workers directory",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,21 @@ import {
resolveWorkerSource,
} from "../../../../../shared/workers/worker-paths.ts";
import {
DEFAULT_WORKER_EXPOSURE,
DEFAULT_WORKER_INSTANCES,
DEFAULT_WORKER_RUNTIME,
DEFAULT_WORKER_SIZE,
parseWorkerExposure,
parseWorkerRuntime,
parseWorkerSize,
validateWorkerNameMessage,
vcpuForSize,
WORKER_EXPOSURE_DESCRIPTIONS,
WORKER_EXPOSURES,
WORKER_RUNTIME_DESCRIPTIONS,
WORKER_RUNTIMES,
WORKER_SIZES,
type WorkerExposure,
type WorkerRuntime,
type WorkerSize,
} from "../../../../../shared/workers/worker-runtimes.ts";
Expand All @@ -51,8 +57,10 @@ import type { LegacyWorkersNewFlags } from "./new.command.ts";
* chosen runtime's starter files and record the choice in `config.toml`.
* Nothing is deployed; this is entirely local-disk work.
*
* The name, runtime and size are all resolved *before* anything is written, so a
* cancelled prompt leaves nothing behind for this worker at all.
* The name, runtime, size and exposure are all resolved *before* anything is
* written, so a cancelled prompt leaves nothing behind for this worker at all.
* `--instances` is recorded rather than resolved: it has no prompt, and it only
* reaches `config.toml` when it differs from the default.
*/

/** `values`, with `defaultValue` first, so a prompt pre-selects what it shows first. */
Expand Down Expand Up @@ -174,6 +182,55 @@ const resolveSize = Effect.fnUntraced(function* (options: {
return DEFAULT_WORKER_SIZE;
});

/**
* Recorded on every scaffold, not just when it is asked for: `push` sends a
* complete spec each time, so a worker whose `exposure` is absent from
* `config.toml` is deployed public by the next bare `push`. Writing the value
* down — default included, the way `runtime` and `size` are — is what makes
* `--exposure private` stick past the deploy that chose it.
*/
const resolveExposure = Effect.fnUntraced(function* (options: {
Comment thread
johnstonmatt marked this conversation as resolved.
readonly explicit: Option.Option<WorkerExposure>;
/** Whether there is a terminal to ask on — see `canPromptFor`. */
readonly canPrompt: boolean;
}) {
if (Option.isSome(options.explicit)) {
return options.explicit.value;
}

if (options.canPrompt) {
const output = yield* Output;
const selected = yield* output.promptSelect(
"Should this worker be reachable from the internet?",
defaultFirst([...WORKER_EXPOSURES], DEFAULT_WORKER_EXPOSURE).map((exposure) => ({
value: exposure,
label: exposure,
hint: WORKER_EXPOSURE_DESCRIPTIONS[exposure],
})),
);
return parseWorkerExposure(selected) ?? DEFAULT_WORKER_EXPOSURE;
}

return DEFAULT_WORKER_EXPOSURE;
});

/**
* The instance count to record, and whether to record it at all.
*
* Not prompted for, unlike the other dials: how many instances a worker needs is
* an operational answer nobody has while scaffolding it, so the flag records
* one when it is given and the file stays quiet when it is not.
*
* `undefined` — meaning "write no key" — for the default count, because an
* absent `instances` and `instances = 1` mean the same thing to `push`, and a
* scaffold should not commit a line that says nothing. A `0` is not that: it
* scales the worker to nothing, so it is written like any other explicit count.
*/
function recordedInstances(explicit: Option.Option<number>): number | undefined {
const instances = Option.getOrUndefined(explicit);
return instances === undefined || instances === DEFAULT_WORKER_INSTANCES ? undefined : instances;
}

/**
* Whether the destination is free for a scaffold: nothing there, or an empty
* directory. A plain file counts as occupied, so it is refused by name rather
Expand Down Expand Up @@ -226,11 +283,13 @@ export const legacyWorkersNew = Effect.fn("legacy.experimental.workers.new")(fun
);
}

// Resolved before anything is written, so cancelling either prompt leaves
// Resolved before anything is written, so cancelling any prompt leaves
// nothing behind — the name included. With nowhere to ask, the defaults
// stand; only the name has nothing to fall back to.
const runtime = yield* resolveRuntime({ explicit: flags.runtime, canPrompt });
const size = yield* resolveSize({ explicit: flags.size, canPrompt });
const exposure = yield* resolveExposure({ explicit: flags.exposure, canPrompt });
const instances = recordedInstances(flags.instances);

// Validated before anything is written: this is the directory the starter
// files land in, so a value naming the project root, `supabase/`, or
Expand Down Expand Up @@ -289,6 +348,8 @@ export const legacyWorkersNew = Effect.fn("legacy.experimental.workers.new")(fun
patch: {
runtime,
size,
exposure,
Comment thread
johnstonmatt marked this conversation as resolved.
...(instances === undefined ? {} : { instances }),
...(source === undefined ? {} : { source }),
},
});
Expand All @@ -310,6 +371,11 @@ export const legacyWorkersNew = Effect.fn("legacy.experimental.workers.new")(fun
runtime,
size,
vcpu: vcpuForSize(size),
exposure,
// The count a deploy will use, whether or not it was written down — a
// payload that omitted it for the default would read as "unknown" rather
// than "one".
instances: instances ?? DEFAULT_WORKER_INSTANCES,
source: sourceDisplay,
config_path: project.configPath,
};
Expand All @@ -335,7 +401,10 @@ export const legacyWorkersNew = Effect.fn("legacy.experimental.workers.new")(fun
legacyRenderWorkerDetails([
["Runtime", runtime],
["Size", `${size} (${vcpuForSize(size)} vCPU)`],
["Access", "public"],
["Access", exposure],
// `declared`, the way `workers status` labels the same number: nothing
// is running yet, so a bare count would read as a live tally.
["Instances", `${instances ?? DEFAULT_WORKER_INSTANCES} declared`],
Comment thread
johnstonmatt marked this conversation as resolved.
]),
);
// On the success trailer rather than inline, the way `bootstrap` emits its
Expand Down
Loading
Loading