-
Notifications
You must be signed in to change notification settings - Fork 524
feat(cli): add stack restart #6603
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
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
76 changes: 76 additions & 0 deletions
76
apps/cli/src/commands/experimental/stack/restart/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,76 @@ | ||
| # `supabase stack restart` | ||
|
|
||
| The command is available only when `experimental.stack` is enabled through project | ||
| configuration or `SUPABASE_EXPERIMENTAL_STACK=1`. It has no top-level alias. | ||
|
|
||
| ## Files Read | ||
|
|
||
| Reads the selected stack's descriptor and `<SUPABASE_HOME>/managed/stacks/<id>/state.json`, | ||
| plus owner metadata in `control.json` when present. Restart does not load project | ||
| configuration; it uses the selected stack's saved effective configuration and preparation policy. | ||
| Feature routing and workdir discovery may still read `supabase/config.toml` or | ||
| `supabase/config.json`. Use `SUPABASE_EXPERIMENTAL_STACK=1` with `--stack-id` when | ||
| project configuration is invalid or unavailable. Runtime startup can read files | ||
| referenced by the saved definition, such as signing material. | ||
|
|
||
| ## Files Written | ||
|
|
||
| The CLI does not rewrite project configuration. The stack package updates its | ||
| state record, owner metadata, runtime files, logs, and service data beneath the | ||
| selected stack directory. Restart preserves the stack ID and data; it never calls | ||
| create or destroy. There is no explicit `prepare()` call. Runtime startup reuses | ||
| cached artifacts and may fetch missing ones; the saved `background` preparation | ||
| policy can also prefetch enabled lazy services, while `on-demand` skips that prefetch. | ||
|
|
||
| ## API Routes | ||
|
|
||
| No Management API routes. The command uses local stack control RPC and delegates | ||
| artifact downloads, container operations, and service startup to the package. | ||
| Artifact URLs and registry requests depend on the selected runtime and releases. | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| - `SUPABASE_HOME`: managed state location; defaults to the user's `.supabase` directory. | ||
| - `HOME`: participates in default home resolution. | ||
| - Project configuration and function dotenv overrides are not reloaded by the | ||
| restart handler. Saved secret values are not emitted. | ||
| - Standard CLI settings, output, and telemetry environment controls apply through | ||
| the existing CLI layers; restart adds no command-specific environment variables. | ||
|
|
||
| ## Exit Codes | ||
|
|
||
| | Code | Condition | | ||
| | ----- | --------------------------------------------------------- | | ||
| | `0` | The selected stack restarted successfully. | | ||
| | `1` | Invalid flags, missing stack, or a stop or start failure. | | ||
| | `130` | The CLI waiter was interrupted. | | ||
|
|
||
| ## Telemetry Events Fired | ||
|
|
||
| Telemetry state is flushed to `<SUPABASE_HOME or ~/.supabase>/telemetry.json` | ||
| after successful and failed runs. Standard command instrumentation emits `cli_command_executed` for success or | ||
| failure, with canonical command identity `stack restart`, duration, sanitized flags, and error classification. Restart adds | ||
| no custom telemetry event and does not emit configuration or credential values. | ||
|
|
||
| ## Output | ||
|
|
||
| - `--output-format text`: stack ID, runtime, lifecycle, configured endpoints, and | ||
| dormant capabilities. Progress is cleared after success or failed before propagation. | ||
| - `--output-format json`: one status object containing `id`, `lifecycle`, | ||
| `desired_lifecycle`, `runtime`, `endpoints`, `versions`, `capabilities`, and `artifacts`. | ||
| - `--output-format stream-json`: standard progress events, followed by a `result` | ||
| event carrying the same status object, or an `error` event on failure. | ||
|
|
||
| Legacy `-o/--output` is rejected with guidance to use `--output-format`. | ||
|
|
||
| ## Notes | ||
|
|
||
| Targets one existing stack through `--stack`, `--stack-id`, or the current | ||
| project. The command stops and starts without an explicit configuration. Stop failure prevents | ||
| start; start failure leaves the same stack stopped and available for recovery. Interrupting | ||
| the CLI waiter follows the package's owner lifecycle contract and does not invoke | ||
| destroy from the command handler. | ||
| An unconfigured stack must be initialized with `supabase stack start` before it can be restarted. | ||
|
|
||
| Restart reuses saved one-off `start` flags such as `--exclude`, `--eager`, and | ||
| `--preparation`; a normal `start` reloads project configuration and current flags. |
34 changes: 34 additions & 0 deletions
34
apps/cli/src/commands/experimental/stack/restart/restart.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 { stackRestart } from "./restart.handler.ts"; | ||
|
|
||
| const config = { | ||
| stack: Flag.string("stack").pipe( | ||
| Flag.withDescription("Restart a named stack (defaults to the current project stack)."), | ||
| Flag.optional, | ||
| ), | ||
| stackId: Flag.string("stack-id").pipe( | ||
| Flag.withDescription("Restart an existing stack by id."), | ||
| Flag.optional, | ||
| ), | ||
| } as const; | ||
|
|
||
| export type StackRestartFlags = CliCommand.Command.Config.Infer<typeof config>; | ||
|
|
||
| export const stackRestartCommand = Command.make("restart", config).pipe( | ||
| Command.withDescription( | ||
| "Restart an existing managed local Supabase stack using its saved configuration, including the previous start's service selection and preparation policy.", | ||
| ), | ||
| Command.withShortDescription("Restart a managed local stack"), | ||
| Command.withExamples([ | ||
| { | ||
| command: "supabase stack restart", | ||
| description: "Restart the current project stack", | ||
| }, | ||
| ]), | ||
| Command.withHandler((flags) => | ||
| stackRestart(flags).pipe(withCommandTelemetry({ flags, config }), withJsonErrorHandling), | ||
| ), | ||
| ); |
41 changes: 41 additions & 0 deletions
41
apps/cli/src/commands/experimental/stack/restart/restart.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,41 @@ | ||
| import { Data } from "effect"; | ||
| import { | ||
| actionability, | ||
| type CliErrorActionabilityDeclaration, | ||
| ErrorActionabilityId, | ||
| } from "../../../../shared/telemetry/error-actionability.ts"; | ||
|
|
||
| export class StackCommandRestartError extends Data.TaggedError("ExperimentalStackRestartError")<{ | ||
| readonly reason: | ||
| | "flags" | ||
| | "not-found" | ||
| | "invalid-config" | ||
| | "port" | ||
| | "lifecycle" | ||
| | "runtime" | ||
| | "registry" | ||
| | "artifact" | ||
| | "unknown"; | ||
| readonly message: string; | ||
| readonly suggestion?: string; | ||
| readonly cause?: unknown; | ||
| }> { | ||
| get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { | ||
| switch (this.reason) { | ||
| case "flags": | ||
| case "not-found": | ||
| return actionability.provideFlags; | ||
| case "invalid-config": | ||
| case "port": | ||
| case "lifecycle": | ||
| return actionability.invalidConfig; | ||
| case "runtime": | ||
| return actionability.dockerNotRunning; | ||
| case "registry": | ||
| case "artifact": | ||
| return actionability.externalNetwork; | ||
| case "unknown": | ||
| return actionability.unknown; | ||
| } | ||
| } | ||
| } |
154 changes: 154 additions & 0 deletions
154
apps/cli/src/commands/experimental/stack/restart/restart.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,154 @@ | ||
| import { Effect, Match, Option } from "effect"; | ||
| import type { StackDescriptor, StackError, StackId } from "@supabase/stack/effect"; | ||
| import { Output } from "../../../../shared/output/output.service.ts"; | ||
| import { OutputFlag } from "../../../../command-internal/global-flags.ts"; | ||
| import { CommandSettings } from "../../../../config/command-settings.service.ts"; | ||
| import { TelemetryState } from "../../../../telemetry/telemetry-state.service.ts"; | ||
| import { | ||
| StackApi, | ||
| StackTargetError, | ||
| rejectStackOutput, | ||
| renderStackStatus, | ||
| stackStatusPayload, | ||
| validateStackId, | ||
| validateStackTarget, | ||
| } from "../stack.shared.ts"; | ||
| import type { StackRestartFlags } from "./restart.command.ts"; | ||
| import { StackCommandRestartError } from "./restart.errors.ts"; | ||
|
|
||
| const mapTargetError = (error: StackTargetError) => | ||
| new StackCommandRestartError({ | ||
| reason: error.reason, | ||
| message: error.message, | ||
| ...(error.suggestion === undefined ? {} : { suggestion: error.suggestion }), | ||
| cause: error, | ||
| }); | ||
|
|
||
| const mapStackError = (error: StackError) => { | ||
| const classification = Match.value(error).pipe( | ||
| Match.tag("StackNotFoundError", () => ({ reason: "not-found" as const })), | ||
| Match.tag("InvalidStackIdentityError", () => ({ reason: "flags" as const })), | ||
| Match.tag("PortUnavailableError", "PortAllocationError", () => ({ | ||
| reason: "port" as const, | ||
| suggestion: | ||
| "Free the conflicting port and retry, or stop the stack and use supabase stack start to apply updated project port configuration.", | ||
| })), | ||
| Match.tag( | ||
| "InvalidStackConfigError", | ||
| "StackVersionUnsupportedError", | ||
| "InvalidProjectRootError", | ||
| "StackStateInvalidError", | ||
| "StackStateFormatUnsupportedError", | ||
| "StackSecretMismatchError", | ||
| "InvalidJwtSigningMaterialError", | ||
| () => ({ reason: "invalid-config" as const }), | ||
| ), | ||
| Match.tag("StackRuntimeMismatchError", () => ({ | ||
| reason: "flags" as const, | ||
| suggestion: | ||
| "Restart preserves the existing runtime; choose a different existing stack if needed.", | ||
| })), | ||
| Match.tag( | ||
| "StackLifecycleConflictError", | ||
| "StackNotRunningError", | ||
| "StackMustBeStoppedError", | ||
| "StackOwnershipConflictError", | ||
| "StackUpgradeRequiredError", | ||
| () => ({ | ||
| reason: "lifecycle" as const, | ||
| suggestion: "Run supabase stack status to inspect the stack state.", | ||
| }), | ||
| ), | ||
| Match.tag("ContainerEngineError", () => ({ | ||
| reason: "runtime" as const, | ||
| suggestion: "Ensure the selected container engine is running and retry the command.", | ||
| })), | ||
| Match.tag("ContainerPullError", () => ({ | ||
| reason: "registry" as const, | ||
| suggestion: "Check registry connectivity and image availability, then retry the command.", | ||
| })), | ||
| Match.tag("ArtifactIntegrityError", "StackPreparationError", () => ({ | ||
| reason: "artifact" as const, | ||
| suggestion: "Retry the stack restart with --debug if the artifact cannot be prepared.", | ||
| })), | ||
| Match.tag("StackRuntimeError", () => ({ | ||
| reason: "unknown" as const, | ||
| suggestion: "Retry the stack restart with --debug and inspect the runtime diagnostics.", | ||
| })), | ||
| Match.tag("StackCleanupError", () => ({ | ||
| reason: "unknown" as const, | ||
| suggestion: "Retry the stack restart with --debug and inspect cleanup diagnostics.", | ||
| })), | ||
| Match.orElse(() => ({ reason: "unknown" as const })), | ||
| ); | ||
| return new StackCommandRestartError({ | ||
| ...classification, | ||
| message: error.message, | ||
| cause: error, | ||
| }); | ||
| }; | ||
|
|
||
| export const stackRestart = Effect.fn("experimental.stack.restart")(function* ( | ||
| flags: StackRestartFlags, | ||
| ) { | ||
| 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)); | ||
|
|
||
| let id: StackId; | ||
| let desiredLifecycle: StackDescriptor["desiredLifecycle"]; | ||
| if (Option.isSome(flags.stackId)) { | ||
| id = yield* validateStackId(flags.stackId.value).pipe(Effect.mapError(mapTargetError)); | ||
| const inspection = yield* api.inspectStack(id).pipe(Effect.mapError(mapStackError)); | ||
| desiredLifecycle = inspection.descriptor.desiredLifecycle; | ||
| } else { | ||
| const found = yield* api | ||
| .findStack({ | ||
| projectRoot: settings.workdir, | ||
| ...(Option.isSome(flags.stack) ? { name: flags.stack.value } : {}), | ||
| }) | ||
| .pipe(Effect.mapError(mapStackError)); | ||
| if (Option.isNone(found)) | ||
| return yield* new StackCommandRestartError({ | ||
| reason: "not-found", | ||
| message: Option.isSome(flags.stack) | ||
| ? `No managed stack named "${flags.stack.value}" was found for this project.` | ||
| : "No managed stack exists for the selected project.", | ||
| suggestion: Option.isSome(flags.stack) | ||
| ? "Choose an existing --stack name or omit --stack for the current project." | ||
| : "Run supabase stack start first.", | ||
| }); | ||
| id = found.value.id; | ||
| desiredLifecycle = found.value.desiredLifecycle; | ||
| } | ||
| if (desiredLifecycle === "unconfigured") | ||
| return yield* new StackCommandRestartError({ | ||
| reason: "lifecycle", | ||
| message: "The selected stack has not been configured yet.", | ||
| suggestion: "Run supabase stack start to configure the stack first.", | ||
| }); | ||
| const stack = yield* api.openStack(id).pipe(Effect.mapError(mapStackError)); | ||
| const task = yield* output.task("Restarting local Supabase stack..."); | ||
| yield* stack.stop.pipe( | ||
| Effect.mapError(mapStackError), | ||
| Effect.tapError((error) => task.fail(error.message)), | ||
| ); | ||
| const status = yield* stack.start().pipe( | ||
| Effect.mapError(mapStackError), | ||
| Effect.tapError((error) => task.fail(error.message)), | ||
| Effect.tap(() => task.clear()), | ||
| ); | ||
| if (output.format === "text") yield* output.raw(renderStackStatus(status)); | ||
| else yield* output.success("", stackStatusPayload(status)); | ||
| return status; | ||
| }); | ||
| 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.