Skip to content
Draft
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
54 changes: 54 additions & 0 deletions apps/cli/src/legacy/commands/workers/delete/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# `supabase workers delete <name>`

> **TS-only command.** `supabase workers` has no Go counterpart — there is no
> `apps/cli-go/internal/workers` to match, and nothing is proxied. See
> `docs/go-cli-divergences.md`.

## Files Read

| Path | Format | When |
| -------------------------------- | ------ | ---------------------------------------------- |
| `<workdir>/supabase/config.toml` | TOML | always, to report the source directory it kept |

## Files Written

| Path | Format | When |
| ---- | ------ | ---- |
| — | — | — |

The worker's directory and its `[workers.<name>]` entry are deliberately left
on disk; only the remote worker is deleted.

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| -------- | ----------------------------------- | ------------ | ------------ | --------------------------------------- |
| `GET` | `/v2/projects/{ref}/workers/{name}` | Bearer token | none | `spec.instances` (for the confirmation) |
| `DELETE` | `/v2/projects/{ref}/workers/{name}` | Bearer token | none | status only |

## Exit Codes

| Code | Condition |
| ---- | ------------------------------------------------------- |
| `0` | success (a `404` on DELETE counts — it is already gone) |
| `1` | invalid worker name |
| `1` | nothing deployed under that name |
| `1` | the typed confirmation did not match the worker's name |
| `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) |

## 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. `workers` has no Go counterpart, so there is no
`phtelemetry.*` call to reproduce.
44 changes: 44 additions & 0 deletions apps/cli/src/legacy/commands/workers/delete/delete.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 { legacyWorkersDelete } from "./delete.handler.ts";

const config = {
name: Argument.string("name").pipe(Argument.withDescription("Worker to delete.")),
yes: Flag.boolean("yes").pipe(
Flag.withAlias("y"),
Flag.withDescription("Skip the confirmation prompt."),
),
projectRef: Flag.string("project-ref").pipe(
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
} as const;

export type LegacyWorkersDeleteFlags = CliCommand.Command.Config.Infer<typeof config>;

export const legacyWorkersDeleteCommand = Command.make("delete", config).pipe(
Command.withDescription(
"Delete a worker from the linked Supabase project. Irreversible; its local directory and supabase/config.toml entry are kept.",
),
Command.withShortDescription("Delete a worker from Supabase"),
Command.withExamples([
{
command: "supabase workers delete api",
description: "Delete a worker, confirming by typing its name",
},
{
command: "supabase workers delete api --yes",
description: "Skip the confirmation prompt (scripts and CI)",
},
]),
Command.withHandler((flags) =>
legacyWorkersDelete(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
),
Command.provide(legacyManagementApiRuntimeLayer(["workers", "delete"])),
);
128 changes: 128 additions & 0 deletions apps/cli/src/legacy/commands/workers/delete/delete.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Effect, Option } from "effect";
import { Output } from "../../../../shared/output/output.service.ts";
import { renderWorkerDetails } from "../workers.format.ts";
import { legacyEmitWorkersGoOutput } from "../workers.output.ts";
import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts";
import { displayPath } from "../../../../shared/workers/worker-paths.ts";
import { deleteWorker, getWorker } from "../../../../shared/workers/workers-api.ts";
import {
WorkerDeleteNotConfirmedError,
WorkerNotDeployedError,
} from "../../../../shared/workers/workers.errors.ts";
import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts";
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
import {
legacyDescribeWorker,
legacyLoadWorkersProject,
legacyValidateWorkerName,
} from "../workers.shared.ts";
import type { LegacyWorkersDeleteFlags } from "./delete.command.ts";

/**
* `supabase workers delete [name]` — delete the worker; its instances and image
* are torn down asynchronously. Whether it exists is asked of the API, never of
* a local file.
*
* Note what it does *not* remove: the worker's directory and its `config.toml`
* entry stay on disk, so `push <name>` brings it straight back — which is why
* the command says so.
*
* Being irreversible, an interactive session has to type the worker's name back
* to proceed — the same "confirm by typing it" pattern as GitHub's own repo
* deletion, rather than a bare y/n that is too easy to reflexively confirm.
* `--yes` skips it for scripts, as does a non-interactive session, where there
* would be nothing to read.
*/
export const legacyWorkersDelete = Effect.fn("legacy.workers.delete")(function* (
flags: LegacyWorkersDeleteFlags,
) {
const output = yield* Output;
const api = yield* LegacyPlatformApi;
const resolver = yield* LegacyProjectRefResolver;
const linkedProjectCache = yield* LegacyLinkedProjectCache;
const telemetryState = yield* LegacyTelemetryState;

const project = yield* legacyLoadWorkersProject();
const name = yield* legacyValidateWorkerName(flags.name);
const worker = legacyDescribeWorker(project, name);
const projectRef = yield* resolver.resolve(flags.projectRef);

// Go writes the linked-project cache and flushes telemetry in
// `PersistentPostRun`, so both happen whether the command succeeds or fails.
yield* Effect.gen(function* () {
const fetching = yield* output.task(`Reading "${name}"...`);
const found = yield* getWorker(api, projectRef, name).pipe(
Effect.tapError(() => fetching.fail()),
);
yield* fetching.clear();

if (Option.isNone(found)) {
return yield* Effect.fail(
new WorkerNotDeployedError({
detail: `Nothing is deployed for "${name}" in project ${projectRef}.`,
suggestion: `Deploy it with \`supabase workers push ${name}\`.`,
}),
);
}

if (!flags.yes && output.format === "text" && output.interactive) {
const instances = found.value.spec.instances;
yield* output.raw(
`This permanently deletes "${name}" from project ${projectRef}.` +
(instances > 0
? ` ${instances} running instance${instances === 1 ? "" : "s"} will be terminated.`
: "") +
"\n",
);
const typed = yield* output.promptText(`Type ${name} to confirm`);
// Trimmed: a trailing space from a paste is not a different answer, and
// making someone re-run a destructive command over one is just friction.
if (typed.trim() !== name) {
return yield* Effect.fail(
new WorkerDeleteNotConfirmedError({
detail: `The confirmation did not match "${name}", so nothing was deleted.`,
suggestion: `Re-run \`supabase workers delete ${name}\` and type the name exactly, or pass --yes.`,
}),
);
}
}

const deleting = yield* output.task(`Deleting "${name}"...`);
yield* deleteWorker(api, projectRef, name).pipe(Effect.tapError(() => deleting.fail()));
yield* deleting.succeed(`Deleted "${name}".`);

const sourceDisplay = displayPath(project.projectRoot, worker.sourceDir);

const payload = {
worker_name: name,
project_ref: projectRef,
kept_source: sourceDisplay,
};

// `-o` asks for a machine-readable stdout, so nothing human may be written
// to it — `output.success` logs to stdout in text mode.
if (yield* legacyEmitWorkersGoOutput(payload)) {
return;
}

if (output.format !== "text") {
yield* output.success("", payload);
return;
}

{
// "Deleted" reads more final than it is: the source and its config entry
// are still here, and redeploying is one command away.
yield* output.raw(
renderWorkerDetails([
["kept", `${sourceDisplay} · its supabase/config.toml entry`],
["next", `supabase workers push ${name}`],
]),
);
}
}).pipe(
Effect.ensuring(linkedProjectCache.cache(projectRef)),
Effect.ensuring(telemetryState.flush),
);
});
Loading