-
Notifications
You must be signed in to change notification settings - Fork 523
feat(cli): add stack command options and destruction #6576
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
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
21ac3aa
test(cli): allow cache-key checks time under load
jgoux 030eff7
feat(cli): add stack command options and destruction
jgoux 8a45f14
fix(cli): respect stack exclusion dependencies
jgoux d3af815
fix(cli): serialize concurrent stack creation
jgoux 1797aac
fix(cli): refine stack command errors and confirmation
jgoux 1868b23
Merge remote-tracking branch 'origin/develop' into feat/stack-options…
jgoux 6c2a2ac
fix(stack): serialize graceful native shutdown
jgoux d22f13e
Merge remote-tracking branch 'origin/develop' into feat/stack-options…
jgoux File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
apps/cli/src/commands/experimental/stack/destroy/SIDE_EFFECTS.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # `supabase stack destroy` | ||
|
|
||
| Permanently stops and removes one managed stack, including its persisted data. | ||
|
|
||
| The command targets the current project stack by default, or an explicit `--stack` name or | ||
| `--stack-id`. It requires an interactive text terminal on both stdout and stdin for confirmation; | ||
| `--yes` is required for redirected, non-interactive, and machine-readable invocations. | ||
| `SUPABASE_YES` participates in the existing confirmation setting; | ||
| an explicit `--yes=false` overrides it. It never accepts `--all`. | ||
|
|
||
| The stack package reads the selected descriptor and removes resources and state under | ||
| `${SUPABASE_HOME:-~/.supabase}/managed/stacks/<id>`. It owns stopping the Supervisor, removing | ||
| native processes or containers, and deleting persistent stack data. The CLI does not delete paths | ||
| or Docker resources itself and makes no Management API calls. Project files are retained. | ||
|
|
||
| The confirmation prompt identifies the stack name, project directory, and immutable stack ID. | ||
| Rejection or missing noninteractive confirmation performs no destructive operation. Text output | ||
| reports the destroyed stack ID. JSON returns | ||
| `{ "destroyed": true, "id": "...", "message": "" }`; stream-JSON wraps the same payload in | ||
| the standard result event. Success exits `0`; invalid targets, confirmation refusal, and | ||
| destruction failures exit `1`; interruption follows the command runtime's interruption exit. | ||
| Standard command instrumentation records command metadata without exporting credentials, and | ||
| telemetry state flushes after both successful and failed runs. |
34 changes: 34 additions & 0 deletions
34
apps/cli/src/commands/experimental/stack/destroy/destroy.command.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { 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 { withCommandTelemetry } from "../../../../telemetry/command-telemetry.ts"; | ||
| import { stackDestroy } from "./destroy.handler.ts"; | ||
|
|
||
| const config = { | ||
| stack: Flag.string("stack").pipe( | ||
| Flag.withDescription( | ||
| "Destroy the stack with this name (defaults to the current project stack).", | ||
| ), | ||
| Flag.optional, | ||
| ), | ||
| stackId: Flag.string("stack-id").pipe( | ||
| Flag.withDescription("Destroy an existing stack by id."), | ||
| Flag.optional, | ||
| ), | ||
| } as const; | ||
|
|
||
| export type StackDestroyFlags = CliCommand.Command.Config.Infer<typeof config>; | ||
|
|
||
| export const stackDestroyCommand = Command.make("destroy", config).pipe( | ||
| Command.withDescription("Permanently destroy a managed local Supabase stack and its data."), | ||
| Command.withShortDescription("Destroy a managed local stack"), | ||
| Command.withExamples([ | ||
| { | ||
| command: "supabase stack destroy --stack feature-a --yes", | ||
| description: "Permanently destroy the feature-a stack", | ||
| }, | ||
| ]), | ||
| Command.withHandler((flags) => | ||
| stackDestroy(flags).pipe(withCommandTelemetry({ flags, config }), withJsonErrorHandling), | ||
| ), | ||
| ); |
37 changes: 37 additions & 0 deletions
37
apps/cli/src/commands/experimental/stack/destroy/destroy.errors.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { Data } from "effect"; | ||
| import { | ||
| actionability, | ||
| type CliErrorActionabilityDeclaration, | ||
| ErrorActionabilityId, | ||
| } from "../../../../shared/telemetry/error-actionability.ts"; | ||
|
|
||
| export class StackCommandDestroyError extends Data.TaggedError("ExperimentalStackDestroyError")<{ | ||
| readonly reason: | ||
| | "flags" | ||
| | "confirmation" | ||
| | "cancelled" | ||
| | "invalid-config" | ||
| | "runtime" | ||
| | "lifecycle" | ||
| | "unknown"; | ||
| readonly message: string; | ||
| readonly suggestion?: string; | ||
| readonly cause?: unknown; | ||
| }> { | ||
| get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { | ||
| switch (this.reason) { | ||
| case "flags": | ||
| case "confirmation": | ||
| return actionability.provideFlags; | ||
|
jgoux marked this conversation as resolved.
|
||
| case "cancelled": | ||
| return actionability.cancelled; | ||
| case "invalid-config": | ||
| case "lifecycle": | ||
| return actionability.invalidConfig; | ||
| case "runtime": | ||
| return actionability.dockerNotRunning; | ||
| case "unknown": | ||
| return actionability.unknown; | ||
| } | ||
| } | ||
| } | ||
138 changes: 138 additions & 0 deletions
138
apps/cli/src/commands/experimental/stack/destroy/destroy.handler.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| import { Effect, Match, Option } from "effect"; | ||
| import { isStackError, StackIdSchema } from "@supabase/stack/effect"; | ||
| import { Output } from "../../../../shared/output/output.service.ts"; | ||
| import { OutputFlag, resolveYes } from "../../../../command-internal/global-flags.ts"; | ||
| import { promptYesNo } from "../../../../command-internal/prompt-yes-no.ts"; | ||
| import { Tty } from "../../../../shared/runtime/tty.service.ts"; | ||
| import { CommandSettings } from "../../../../config/command-settings.service.ts"; | ||
| import { TelemetryState } from "../../../../telemetry/telemetry-state.service.ts"; | ||
| import { | ||
| StackApi, | ||
| StackTargetError, | ||
| rejectStackOutput, | ||
| validateStackId, | ||
| validateStackTarget, | ||
| } from "../stack.shared.ts"; | ||
| import type { StackDestroyFlags } from "./destroy.command.ts"; | ||
| import { StackCommandDestroyError } from "./destroy.errors.ts"; | ||
|
|
||
| const mapTargetError = (error: StackTargetError) => | ||
| new StackCommandDestroyError({ | ||
| reason: error.reason, | ||
| message: error.message, | ||
| ...(error.suggestion === undefined ? {} : { suggestion: error.suggestion }), | ||
| cause: error, | ||
| }); | ||
|
jgoux marked this conversation as resolved.
|
||
|
|
||
| const destroyError = (error: unknown): StackCommandDestroyError => { | ||
| const stackError = isStackError(error) ? error : undefined; | ||
| const classification = | ||
| stackError === undefined | ||
| ? { reason: "unknown" as const } | ||
| : Match.value(stackError).pipe( | ||
| Match.tag("StackNotFoundError", "InvalidStackIdentityError", () => ({ | ||
| reason: "flags" as const, | ||
| })), | ||
| Match.tag("ContainerEngineError", () => ({ | ||
| reason: "runtime" as const, | ||
| suggestion: | ||
| "Check that the selected container engine is installed and its daemon is running, then retry the command.", | ||
| })), | ||
| Match.tag( | ||
| "StackOwnershipConflictError", | ||
| "StackNotRunningError", | ||
| "StackMustBeStoppedError", | ||
| "StackLifecycleConflictError", | ||
| "StackRuntimeError", | ||
| "StackCleanupError", | ||
| "StackDestructionError", | ||
| "StackUpgradeRequiredError", | ||
| () => ({ reason: "lifecycle" as const }), | ||
| ), | ||
| Match.tag( | ||
| "InvalidStackConfigError", | ||
| "StackStateFormatUnsupportedError", | ||
| "InvalidProjectRootError", | ||
| "StackStateInvalidError", | ||
| () => ({ reason: "invalid-config" as const }), | ||
| ), | ||
| Match.orElse(() => ({ reason: "unknown" as const })), | ||
| ); | ||
| return new StackCommandDestroyError({ | ||
| ...classification, | ||
| message: stackError?.message ?? String(error), | ||
| cause: error, | ||
| }); | ||
| }; | ||
|
|
||
| export const stackDestroy = Effect.fn("experimental.stack.destroy")(function* ( | ||
| flags: StackDestroyFlags, | ||
| ) { | ||
| const telemetryState = yield* TelemetryState; | ||
| const body = Effect.gen(function* () { | ||
| const output = yield* Output; | ||
| const settings = yield* CommandSettings; | ||
| const api = yield* StackApi; | ||
| const outputFlag = yield* Effect.serviceOption(OutputFlag); | ||
| yield* rejectStackOutput(outputFlag).pipe(Effect.mapError(mapTargetError)); | ||
| yield* validateStackTarget({ | ||
| stack: Option.getOrUndefined(flags.stack), | ||
| stackId: Option.getOrUndefined(flags.stackId), | ||
| }).pipe(Effect.mapError(mapTargetError)); | ||
|
|
||
| const target = yield* Effect.gen(function* () { | ||
| if (Option.isSome(flags.stackId)) { | ||
| const id = yield* validateStackId(flags.stackId.value).pipe( | ||
| Effect.mapError(mapTargetError), | ||
| ); | ||
| return yield* api.inspectStack(id).pipe( | ||
| Effect.map(({ descriptor }) => descriptor), | ||
| Effect.mapError(destroyError), | ||
| ); | ||
| } | ||
| const found = yield* api | ||
| .findStack({ | ||
| projectRoot: settings.workdir, | ||
| ...(Option.isSome(flags.stack) ? { name: flags.stack.value } : {}), | ||
| }) | ||
| .pipe(Effect.mapError(destroyError)); | ||
| if (Option.isSome(found)) return found.value; | ||
| return yield* new StackCommandDestroyError({ | ||
| reason: "flags", | ||
| message: `No managed stack${Option.isSome(flags.stack) ? ` named "${flags.stack.value}"` : ""} was found for this project.`, | ||
| suggestion: "Choose an existing --stack name or omit --stack for the current project.", | ||
| }); | ||
| }); | ||
| const yes = yield* resolveYes; | ||
| const tty = yield* Tty; | ||
| if (!yes && (!tty.stdinIsTty || !output.interactive || output.format !== "text")) | ||
| return yield* new StackCommandDestroyError({ | ||
| reason: "confirmation", | ||
| message: "Destroying a stack requires confirmation; rerun with --yes.", | ||
| suggestion: "Pass --yes when running non-interactively or in a machine-readable format.", | ||
| }); | ||
| const confirmed = yield* promptYesNo( | ||
| output, | ||
| yes, | ||
| `Permanently destroy stack "${target.name}" at ${target.projectRoot} (${target.id}) and all of its data?`, | ||
| false, | ||
| ); | ||
| if (!confirmed) | ||
| return yield* new StackCommandDestroyError({ | ||
| reason: "cancelled", | ||
| message: "Stack destruction was not confirmed.", | ||
| }); | ||
| const stack = yield* api | ||
| .openStack(StackIdSchema.make(target.id)) | ||
| .pipe(Effect.mapError(destroyError)); | ||
| const destroying = yield* output.task(`Destroying stack ${target.id}...`); | ||
| yield* stack.destroy.pipe( | ||
| Effect.tapError((error) => destroying.fail(error.message)), | ||
| Effect.tap(() => destroying.clear()), | ||
| Effect.mapError(destroyError), | ||
| ); | ||
| if (output.format === "text") yield* output.raw(`Stack ${target.id} destroyed.\n`); | ||
| else yield* output.success("", { destroyed: true, id: target.id }); | ||
| }); | ||
| return yield* body.pipe(Effect.ensuring(telemetryState.flush)); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.