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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import { legacyRoot } from "./root.ts";
* `boolean`. The flag only misbehaves when a real invocation omits it, which is
* precisely the invocation no handler test makes — so the guard walks the
* command tree instead of waiting for a command to be exercised end to end.
*
* `workers push --wait`, added in this stack, is the flag that prompted it: it
* first shipped with neither closer and made a plain `supabase workers push`
* fail to parse at all.
*/

/**
Expand Down
46 changes: 27 additions & 19 deletions apps/cli/src/legacy/commands/workers/push/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,24 @@
| `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`.
`GET /v2/projects/{ref}/workers/{name}` is requested **only with `--wait`**, and
is then polled until `build_state` leaves `building`. Without it the command
returns on the deploy response, which carries the accepted spec and a
`build_state` of `building`.

## Exit Codes

| Code | Condition |
| ---- | ------------------------------------------------------- |
| `0` | success |
| `1` | no workers named and none found in the project |
| `1` | a worker's source is missing, not a directory, or empty |
| `1` | a worker's source directory cannot be read |
| `1` | a worker's source links to a path outside itself |
| `1` | build context upload failed |
| `1` | the build reached `failed`, or never left `building` |
| `1` | API error, or project not enrolled in the alpha |
| Code | Condition |
| ---- | ------------------------------------------------------------------- |
| `0` | success |
| `1` | no workers named and none found in the project |
| `1` | a worker's source is missing, not a directory, or empty |
| `1` | a worker's source directory cannot be read |
| `1` | a worker's source links to a path outside itself |
| `1` | build context upload failed |
| `1` | the deploy was answered with `build_state: failed` |
| `1` | with `--wait`: the build reached `failed`, or never left `building` |
| `1` | API error, or project not enrolled in the alpha |

## Environment Variables

Expand All @@ -72,16 +76,20 @@ 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.

Without `--wait` the deploy returns with the build still running, so the
follow-up hint (`workers status`, and `--wait`) is emitted as a success trailer:
stderr, once, at the end of the run rather than between workers. **Text output
only** — like the rest of the human deploy report it sits behind
`output.format === "text"` and the `-o` check, so `--output-format json`,
`stream-json` and every legacy `-o` mode emit no hint. Machine callers read
`build_state` from the payload instead. The hint carries an explicit
`--project-ref` when the flag supplied one, since it is copy-pasted verbatim.

A multi-worker run stops at the first failure, and names the workers it never
attempted on stderr in **every** format, machine ones included: that run is a
attempted on stderr in **every** format, machine ones included — unlike the
trailer above, `reportUnattempted` has no format guard: that run is a
CI run, where nobody watched the loop and "what still needs deploying" is the
question the failure raises. The per-worker `Deploying Worker n/N:` announcement
is text-only by contrast, since it is progress rather than an outcome.

Both retry suggestions — the one on a failed build and the one on a build that
never settled — carry an explicit `--project-ref` when the flag supplied the
ref, since they are copy-pasted verbatim. A suggestion that dropped it would
re-resolve against whatever this checkout happens to be linked to.
question the failure raises.

The presigned `PUT` above is the one request whose URL is itself a credential.
`--debug` logs every request URL, so `legacyHttpClientLayer` redacts query
Expand Down
15 changes: 15 additions & 0 deletions apps/cli/src/legacy/commands/workers/push/push.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ const config = {
),
Flag.optional,
),
wait: Flag.boolean("wait").pipe(
// Off by default: the deploy POST is answered once the platform has accepted
// the spec and the uploaded context, and the server-side container build
// that follows routinely runs for minutes. Blocking on it made the common
// case — a deploy that builds fine — the slowest thing in the loop, so the
// wait is opt-in for the callers that actually need the build's verdict.
Flag.withDescription(
"Wait for the server-side build to finish, and fail if it does not succeed. Off by default: the command returns once the deploy is accepted.",
),
Flag.withDefault(false),
),
projectRef: Flag.string("project-ref").pipe(
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
Expand Down Expand Up @@ -51,6 +62,10 @@ export const legacyWorkersPushCommand = Command.make("push", config).pipe(
command: "supabase workers push api web",
description: "Deploy several workers by name",
},
{
command: "supabase workers push api --wait",
description: "Deploy and block until the build succeeds or fails",
},
]),
Command.withHandler((flags) =>
legacyWorkersPush(flags).pipe(
Expand Down
65 changes: 56 additions & 9 deletions apps/cli/src/legacy/commands/workers/push/push.handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Effect, FileSystem, Option, Predicate, type Schedule } from "effect";
import type { PlatformError } from "effect/PlatformError";
import { Output } from "../../../../shared/output/output.service.ts";
import { emitSuccessTrailer } from "../../../../shared/cli/success-trailer.ts";
import { legacyRenderWorkerDetails } from "../workers.format.ts";
import {
legacyEmitWorkersMachineOutput,
Expand Down Expand Up @@ -67,6 +68,11 @@ import type { LegacyWorkersPushFlags } from "./push.command.ts";
* code takes the same path, with the base image and a copy synthesized in place
* of your Dockerfile. Every runtime this CLI offers has code to package, so
* there is no path here that skips the upload.
*
* The command returns once the platform accepts the deploy. The container build
* that follows runs for minutes, and blocking on it made every successful
* deploy as slow as the slowest one — so `--wait` opts into the build's
* verdict, for CI and for anyone who needs the image version before continuing.
*/

const resolveRuntime = Effect.fnUntraced(function* (options: {
Expand Down Expand Up @@ -183,6 +189,8 @@ const deployOneWorker = Effect.fnUntraced(function* (input: {
*/
readonly refSuffix: string;
readonly instances: Option.Option<number>;
/** `--wait`: block on the server-side build instead of returning once it starts. */
readonly wait: boolean;
readonly pollSchedule?: Schedule.Schedule<unknown>;
readonly pollRetrySchedule?: Schedule.Schedule<unknown>;
/** Suppresses this step's human output when `-o` owns stdout. */
Expand Down Expand Up @@ -322,18 +330,27 @@ const deployOneWorker = Effect.fnUntraced(function* (input: {
};

const deploying = yield* output.task("Deploying worker...");
yield* deployWorker(api, projectRef, name, { spec, contextUploadId }).pipe(
// The response to the deploy itself is the last thing this command can learn
// without waiting: the platform answers it only after accepting the spec and
// the uploaded context, and it carries the accepted spec back. Everything
// after this point is the server-side container build.
const accepted = yield* deployWorker(api, projectRef, name, { spec, contextUploadId }).pipe(
Effect.tapError(() => deploying.fail()),
);

const settled = yield* awaitWorkerBuild(api, projectRef, name, {
schedule: input.pollSchedule,
retrySchedule: input.pollRetrySchedule,
refSuffix: input.refSuffix,
onPoll: (polled) =>
polled.buildState === "building" ? deploying.message("Building worker...") : Effect.void,
}).pipe(Effect.tapError(() => deploying.fail()));

const settled = input.wait
? yield* awaitWorkerBuild(api, projectRef, name, {
Comment on lines +341 to +342

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 terminal deploy responses before polling

When --wait is set and the deploy POST already returns build_state: active or failed—both are permitted by V2DeployAWorkerOutput—this branch discards that terminal verdict and performs a GET anyway. If that read is temporarily unavailable, still returns the documented post-deploy 404 race, or observes a concurrent deployment, the command can time out, fail, or even succeed contrary to its own deploy response. Only poll when accepted.buildState === "building"; otherwise use accepted directly.

Useful? React with 👍 / 👎.

schedule: input.pollSchedule,
retrySchedule: input.pollRetrySchedule,
refSuffix: input.refSuffix,
onPoll: (polled) =>
polled.buildState === "building" ? deploying.message("Building worker...") : Effect.void,
}).pipe(Effect.tapError(() => deploying.fail()))
: accepted;

// Checked whether or not the build was waited on. A deploy answered with a
// spec already in `failed` is a refusal the command should report as one,
// rather than exiting zero on a worker that will never come up.
if (settled.buildState === "failed") {
yield* deploying.clear();
return yield* Effect.fail(
Expand Down Expand Up @@ -364,13 +381,38 @@ const deployOneWorker = Effect.fnUntraced(function* (input: {
);
yield* output.raw(
legacyRenderWorkerDetails([
// Labelled `State`, and placed first, the way `workers status` renders
// the same field: without `--wait` it is the one row that says the
// worker is not serving yet, so it should not be hunted for at the
// bottom of the block.
["State", settled.buildState],
["Runtime", runtime],
["Size", formatApiSize(settled.spec.size)],
// Empty without `--wait`: no image exists until the build produces one,
// and `legacyRenderWorkerDetails` drops an empty-valued row.
["Image", settled.imageVersion ?? ""],
["Access", settled.spec.exposure],
["URL", url ?? ""],
]),
);
if (settled.buildState === "building") {
// A success trailer rather than an inline stderr line: this is a "what to
// run next" hint, which `stop`, `bootstrap`, `migration repair` and
// `gen signing-key` all route through `emitSuccessTrailer` so it prints
// once at the end of the run instead of scrolling away. It matters here
// more than for those: pushing several workers would otherwise bury each
// worker's hint under the next worker's packaging and deploy output.
//
// One short sentence per line, with the command and the flag aqua'd the
// way every other follow-up hint in this shell writes them. The single
// wrapped paragraph this replaced re-flowed differently at every terminal
// width and buried both commands mid-sentence.
yield* emitSuccessTrailer(
`\nYour build was submitted successfully.\n` +
`Run ${legacyAqua(`supabase workers status ${name}${input.refSuffix}`)} to check on it.\n` +
`Add ${legacyAqua("--wait")} to block on the build next time.\n`,
Comment on lines +410 to +413

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 accepted-build hints when a later worker fails

In a text-mode multi-worker push without --wait, an accepted worker queues its status command here, but if any later worker fails the real runCli path exits nonzero and never drains SuccessTrailer (shared/cli/run.ts only calls takeAll for exit code 0). The first remote build is still running, yet its only follow-up guidance is silently discarded; the handler-level tests do not expose this because their missing SuccessTrailer service makes emitSuccessTrailer write immediately. Surface these already-submitted build hints on partial failure rather than retaining them only for an entirely successful batch.

Useful? React with 👍 / 👎.

);
}
}

return {
Expand Down Expand Up @@ -419,6 +461,10 @@ const reportUnattempted = Effect.fnUntraced(function* (skipped: ReadonlyArray<st
* per-project capacity and shred the progress output. The first failure stops
* the run, because a build that failed is usually the thing to fix before
* spending minutes on the rest.
*
* Without `--wait` that serialization only covers the package/upload/deploy
* legs; the builds themselves then run concurrently on the platform, which is
* what the caller asked for by not waiting.
*/
export const legacyWorkersPush = Effect.fn("legacy.workers.push")(function* (
flags: LegacyWorkersPushFlags,
Expand Down Expand Up @@ -490,6 +536,7 @@ export const legacyWorkersPush = Effect.fn("legacy.workers.push")(function* (
projectRef,
refSuffix,
instances: flags.instances,
wait: flags.wait,
machineOutput,
...(options.pollSchedule === undefined ? {} : { pollSchedule: options.pollSchedule }),
...(options.pollRetrySchedule === undefined
Expand Down
Loading
Loading