From aed2c7ed321e8e74f71a1e964768a68fe98383b0 Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Fri, 15 May 2026 10:19:20 -0700 Subject: [PATCH 1/6] Add `environment fork`, `environment deploy`, `instance copy` (alias `promote`) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three thin wrappers around SDK primitives that the preview command already composes: - `mass environment fork ` — exposes Environments.Fork with --copy-environment-defaults, --copy-secrets, --copy-remote-references, and --attributes. Idempotent against the same parent. - `mass environment deploy ` — exposes Environments.Deploy; cancels any in-flight environment deployment and schedules a fresh provision wave. - `mass instance copy ` (alias `promote`) — exposes Instances.Copy with --overrides (path to JSON/YAML), --copy-secrets, --copy-remote-references, and --message. Components must match. Helpdocs and generated docs included. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/environment.go | 109 ++++++++++++++++++++++ cmd/instance.go | 69 ++++++++++++++ docs/generated/mass_environment.md | 2 + docs/generated/mass_environment_deploy.md | 63 +++++++++++++ docs/generated/mass_environment_fork.md | 88 +++++++++++++++++ docs/generated/mass_instance.md | 1 + docs/generated/mass_instance_copy.md | 89 ++++++++++++++++++ docs/helpdocs/environment/deploy.md | 30 ++++++ docs/helpdocs/environment/fork.md | 49 ++++++++++ docs/helpdocs/instance/copy.md | 52 +++++++++++ 10 files changed, 552 insertions(+) create mode 100644 docs/generated/mass_environment_deploy.md create mode 100644 docs/generated/mass_environment_fork.md create mode 100644 docs/generated/mass_instance_copy.md create mode 100644 docs/helpdocs/environment/deploy.md create mode 100644 docs/helpdocs/environment/fork.md create mode 100644 docs/helpdocs/instance/copy.md diff --git a/cmd/environment.go b/cmd/environment.go index 60a3b66..e77a052 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -100,6 +100,30 @@ func NewCmdEnvironment() *cobra.Command { environmentPreviewCmd.Flags().StringToStringP("attributes", "a", nil, "Custom attributes for ABAC (e.g. -a environment=preview,region=uswest). Overrides `attributes:` in the config file.") environmentPreviewCmd.Flags().Bool("follow", false, "Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.") + environmentForkCmd := &cobra.Command{ + Use: "fork [parent-environment] [new-ID]", + Short: "Fork an existing environment", + Example: `mass environment fork ecomm-production staging`, + Long: helpdocs.MustRender("environment/fork"), + Args: cobra.ExactArgs(2), + RunE: runEnvironmentFork, + } + environmentForkCmd.Flags().StringP("name", "n", "", "Environment name (defaults to new-ID if not provided)") + environmentForkCmd.Flags().StringP("description", "d", "", "Optional environment description") + environmentForkCmd.Flags().StringToStringP("attributes", "a", nil, "Custom attributes for ABAC (e.g. -a region=uswest)") + environmentForkCmd.Flags().Bool("copy-environment-defaults", false, "Copy the parent's default resource connections into the fork") + environmentForkCmd.Flags().Bool("copy-secrets", false, "Copy every instance's secrets from the parent into the fork") + environmentForkCmd.Flags().Bool("copy-remote-references", false, "Copy every instance's remote references from the parent into the fork") + + environmentDeployCmd := &cobra.Command{ + Use: "deploy [environment]", + Short: "Deploy every instance in an environment, in dependency order", + Example: `mass environment deploy ecomm-staging`, + Long: helpdocs.MustRender("environment/deploy"), + Args: cobra.ExactArgs(1), + RunE: runEnvironmentDeploy, + } + environmentCmd.AddCommand(environmentExportCmd) environmentCmd.AddCommand(environmentGetCmd) environmentCmd.AddCommand(environmentListCmd) @@ -107,6 +131,8 @@ func NewCmdEnvironment() *cobra.Command { environmentCmd.AddCommand(environmentUpdateCmd) environmentCmd.AddCommand(environmentDefaultCmd) environmentCmd.AddCommand(environmentPreviewCmd) + environmentCmd.AddCommand(environmentForkCmd) + environmentCmd.AddCommand(environmentDeployCmd) return environmentCmd } @@ -443,3 +469,86 @@ func runEnvironmentPreview(cmd *cobra.Command, args []string) error { } return nil } + +func runEnvironmentFork(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + parentID := args[0] + newLocalID := args[1] + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + description, err := cmd.Flags().GetString("description") + if err != nil { + return err + } + attrs, err := cmd.Flags().GetStringToString("attributes") + if err != nil { + return err + } + copyDefaults, err := cmd.Flags().GetBool("copy-environment-defaults") + if err != nil { + return err + } + copySecrets, err := cmd.Flags().GetBool("copy-secrets") + if err != nil { + return err + } + copyRefs, err := cmd.Flags().GetBool("copy-remote-references") + if err != nil { + return err + } + + if name == "" { + name = newLocalID + } + + cmd.SilenceUsage = true + + mdClient, mdClientErr := massdriver.NewClient() + if mdClientErr != nil { + return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) + } + + input := environments.ForkInput{ + ID: newLocalID, + Name: name, + Description: description, + Attributes: cli.AttributesToAnyMap(attrs), + CopyEnvironmentDefaults: copyDefaults, + CopySecrets: copySecrets, + CopyRemoteReferences: copyRefs, + } + + env, err := mdClient.Environments.Fork(ctx, parentID, input) + if err != nil { + return err + } + + fmt.Printf("✅ Environment `%s` forked from `%s`\n", env.ID, parentID) + fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).EnvironmentURL(env.ID)) + return nil +} + +func runEnvironmentDeploy(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + environmentID := args[0] + + cmd.SilenceUsage = true + + mdClient, mdClientErr := massdriver.NewClient() + if mdClientErr != nil { + return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) + } + + env, err := mdClient.Environments.Deploy(ctx, environmentID) + if err != nil { + return err + } + + fmt.Printf("🚀 Deploying environment `%s` — instances roll out in dependency order asynchronously\n", env.ID) + fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).EnvironmentURL(env.ID)) + return nil +} diff --git a/cmd/instance.go b/cmd/instance.go index f5d63af..4e03b1e 100644 --- a/cmd/instance.go +++ b/cmd/instance.go @@ -117,6 +117,20 @@ func NewCmdInstance() *cobra.Command { instanceOrphanCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt") instanceOrphanCmd.Flags().Bool("delete-state", false, "Also delete the remote Terraform/OpenTofu state files (irreversible)") + instanceCopyCmd := &cobra.Command{ + Use: `copy [source] [destination]`, + Aliases: []string{"promote"}, + Short: "Copy an instance's configuration into another instance of the same component", + Example: `mass instance copy ecomm-staging-db ecomm-production-db --copy-secrets`, + Long: helpdocs.MustRender("instance/copy"), + Args: cobra.ExactArgs(2), + RunE: runInstanceCopy, + } + instanceCopyCmd.Flags().StringP("message", "m", "", "Optional message attached to the plan deployment created on the destination") + instanceCopyCmd.Flags().StringP("overrides", "o", "", "Path to a JSON or YAML file of param overrides deep-merged onto the source params") + instanceCopyCmd.Flags().Bool("copy-secrets", false, "Copy secrets from the source instance to the destination") + instanceCopyCmd.Flags().Bool("copy-remote-references", false, "Copy remote-reference overrides from the source instance to the destination") + instanceCmd.AddCommand(instanceDeployCmd) instanceCmd.AddCommand(instanceExportCmd) instanceCmd.AddCommand(instanceGetCmd) @@ -124,6 +138,7 @@ func NewCmdInstance() *cobra.Command { instanceCmd.AddCommand(instanceVersionCmd) instanceCmd.AddCommand(instanceDestroyCmd) instanceCmd.AddCommand(instanceOrphanCmd) + instanceCmd.AddCommand(instanceCopyCmd) return instanceCmd } @@ -306,6 +321,60 @@ func readParams(path string) (map[string]any, error) { return params, nil } +func runInstanceCopy(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + sourceID := args[0] + destinationID := args[1] + message, err := cmd.Flags().GetString("message") + if err != nil { + return err + } + overridesPath, err := cmd.Flags().GetString("overrides") + if err != nil { + return err + } + copySecrets, err := cmd.Flags().GetBool("copy-secrets") + if err != nil { + return err + } + copyRefs, err := cmd.Flags().GetBool("copy-remote-references") + if err != nil { + return err + } + + cmd.SilenceUsage = true + + var overrides map[string]any + if overridesPath != "" { + overrides, err = readParams(overridesPath) + if err != nil { + return err + } + } + + mdClient, mdClientErr := massdriver.NewClient() + if mdClientErr != nil { + return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) + } + + input := instances.CopyInput{ + Overrides: overrides, + CopySecrets: copySecrets, + CopyRemoteReferences: copyRefs, + Message: message, + } + + inst, err := mdClient.Instances.Copy(ctx, sourceID, destinationID, input) + if err != nil { + return err + } + + fmt.Printf("✅ Instance `%s` copied to `%s` — plan deployment created on the destination\n", sourceID, destinationID) + fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).InstanceURL(inst.ID)) + return nil +} + func runInstanceExport(cmd *cobra.Command, args []string) error { ctx := context.Background() diff --git a/docs/generated/mass_environment.md b/docs/generated/mass_environment.md index f64efb1..fe68ac3 100644 --- a/docs/generated/mass_environment.md +++ b/docs/generated/mass_environment.md @@ -28,7 +28,9 @@ Environments can be modeled by application stage (production, staging, developme * [mass](/cli/commands/mass) - Massdriver Cloud CLI * [mass environment create](/cli/commands/mass_environment_create) - Create an environment * [mass environment default](/cli/commands/mass_environment_default) - Set an environment default connection +* [mass environment deploy](/cli/commands/mass_environment_deploy) - Deploy every instance in an environment, in dependency order * [mass environment export](/cli/commands/mass_environment_export) - Export an environment from Massdriver +* [mass environment fork](/cli/commands/mass_environment_fork) - Fork an existing environment * [mass environment get](/cli/commands/mass_environment_get) - Get an environment from Massdriver * [mass environment list](/cli/commands/mass_environment_list) - List environments * [mass environment preview](/cli/commands/mass_environment_preview) - Converge a preview environment from a YAML config diff --git a/docs/generated/mass_environment_deploy.md b/docs/generated/mass_environment_deploy.md new file mode 100644 index 0000000..c926dca --- /dev/null +++ b/docs/generated/mass_environment_deploy.md @@ -0,0 +1,63 @@ +--- +id: mass_environment_deploy.md +slug: /cli/commands/mass_environment_deploy +title: Mass Environment Deploy +sidebar_label: Mass Environment Deploy +--- +## mass environment deploy + +Deploy every instance in an environment, in dependency order + +### Synopsis + +# Deploy Environment + +Triggers a deployment of every instance in an environment, in dependency +order. Any in-flight environment deployment is cancelled and replaced. + +The command returns as soon as the deployment is enqueued; instances are +provisioned asynchronously. Watch the deployments stream in the UI or list +them with `mass deployment list`. + +## Usage + +```bash +mass environment deploy +``` + +## Arguments + +- `environment`: full identifier of the environment to deploy + (e.g. `ecomm-staging`). + +## Examples + +```bash +# Deploy every instance in the staging environment. +mass environment deploy ecomm-staging + +# Deploy a freshly-forked preview env. +mass environment fork ecomm-production pr42 --copy-environment-defaults +mass environment deploy ecomm-pr42 +``` + + +``` +mass environment deploy [environment] [flags] +``` + +### Examples + +``` +mass environment deploy ecomm-staging +``` + +### Options + +``` + -h, --help help for deploy +``` + +### SEE ALSO + +* [mass environment](/cli/commands/mass_environment) - Environment management diff --git a/docs/generated/mass_environment_fork.md b/docs/generated/mass_environment_fork.md new file mode 100644 index 0000000..5ddc2f4 --- /dev/null +++ b/docs/generated/mass_environment_fork.md @@ -0,0 +1,88 @@ +--- +id: mass_environment_fork.md +slug: /cli/commands/mass_environment_fork +title: Mass Environment Fork +sidebar_label: Mass Environment Fork +--- +## mass environment fork + +Fork an existing environment + +### Synopsis + +# Fork Environment + +Creates a new environment by forking an existing one. The fork is linked to +its parent via `parent_target_id`, and every instance is seeded with the +parent's params, version, and release channel. + +Re-running `fork` against the same parent with the same `new-ID` resets +the existing fork's state back to the parent's — params reset, defaults +re-apply, and any `--copy-*` flags re-fire. Re-running with a *different* +parent is rejected; a fork's parent is immutable. + +## Usage + +```bash +mass environment fork [flags] +``` + +## Arguments + +- `parent-environment`: full identifier of the environment to fork from + (e.g. `ecomm-production`). +- `new-ID`: local segment of the new environment's identifier. Must match + `^[a-z0-9]{1,20}$` — lowercase alphanumeric only, no dashes. The full + stored identifier becomes `-`. + +## Flags + +- `--name, -n`: human-readable name (defaults to `new-ID`). +- `--description, -d`: optional environment description. +- `--attributes, -a`: custom attributes for ABAC, e.g. + `-a region=us-east-1,data_classification=pii`. +- `--copy-environment-defaults`: also copy the parent's default resource + connections into the fork. +- `--copy-secrets`: copy every instance's secrets from the parent into the + fork. +- `--copy-remote-references`: copy every instance's remote references from + the parent into the fork. + +## Examples + +```bash +# Stand up a staging environment as a copy of production. +mass environment fork ecomm-production staging \ + --copy-environment-defaults \ + --copy-secrets + +# Re-fork to reset edits back to the parent's current state. +mass environment fork ecomm-production staging --copy-environment-defaults +``` + + +``` +mass environment fork [parent-environment] [new-ID] [flags] +``` + +### Examples + +``` +mass environment fork ecomm-production staging +``` + +### Options + +``` + -a, --attributes stringToString Custom attributes for ABAC (e.g. -a region=uswest) (default []) + --copy-environment-defaults Copy the parent's default resource connections into the fork + --copy-remote-references Copy every instance's remote references from the parent into the fork + --copy-secrets Copy every instance's secrets from the parent into the fork + -d, --description string Optional environment description + -h, --help help for fork + -n, --name string Environment name (defaults to new-ID if not provided) +``` + +### SEE ALSO + +* [mass environment](/cli/commands/mass_environment) - Environment management diff --git a/docs/generated/mass_instance.md b/docs/generated/mass_instance.md index 6eb699b..31a8575 100644 --- a/docs/generated/mass_instance.md +++ b/docs/generated/mass_instance.md @@ -30,6 +30,7 @@ Instances are used to: ### SEE ALSO * [mass](/cli/commands/mass) - Massdriver Cloud CLI +* [mass instance copy](/cli/commands/mass_instance_copy) - Copy an instance's configuration into another instance of the same component * [mass instance deploy](/cli/commands/mass_instance_deploy) - Deploy instances * [mass instance destroy](/cli/commands/mass_instance_destroy) - Destroy (decommission) an instance * [mass instance export](/cli/commands/mass_instance_export) - Export instances diff --git a/docs/generated/mass_instance_copy.md b/docs/generated/mass_instance_copy.md new file mode 100644 index 0000000..597ff9a --- /dev/null +++ b/docs/generated/mass_instance_copy.md @@ -0,0 +1,89 @@ +--- +id: mass_instance_copy.md +slug: /cli/commands/mass_instance_copy +title: Mass Instance Copy +sidebar_label: Mass Instance Copy +--- +## mass instance copy + +Copy an instance's configuration into another instance of the same component + +### Synopsis + +# Copy Instance + +Copies one instance's configuration into another instance of the same +component. The source's params (minus any fields the bundle marks +non-copyable) are written to the destination, optionally deep-merged with +`--overrides`. A plan deployment is created on the destination so the +changes can be reviewed before applying. + +Aliased as `promote` — same command, friendlier shape for the common +"promote staging to production" flow: + +```bash +mass instance promote ecomm-staging-db ecomm-production-db +``` + +## Usage + +```bash +mass instance copy [flags] +mass instance promote [flags] +``` + +## Arguments + +- `source`: full identifier of the instance to copy from + (e.g. `ecomm-staging-db`). +- `destination`: full identifier of the instance to copy into + (e.g. `ecomm-production-db`). Must be built from the same component as + the source. + +## Flags + +- `--message, -m`: optional message attached to the plan deployment created + on the destination (think: commit message). +- `--overrides, -o`: path to a JSON or YAML file of param overrides + deep-merged onto the source params before writing. +- `--copy-secrets`: also copy the source's secret values to the destination. +- `--copy-remote-references`: also copy the source's remote-reference + overrides to the destination. + +## Examples + +```bash +# Promote staging's config to production (review the plan before applying). +mass instance promote ecomm-staging-db ecomm-production-db -m "Promote DB config" + +# Promote with a size override and copy secrets. +mass instance copy ecomm-staging-db ecomm-production-db \ + --overrides ./prod-overrides.yaml \ + --copy-secrets \ + -m "Scale up DB for production" +``` + + +``` +mass instance copy [source] [destination] [flags] +``` + +### Examples + +``` +mass instance copy ecomm-staging-db ecomm-production-db --copy-secrets +``` + +### Options + +``` + --copy-remote-references Copy remote-reference overrides from the source instance to the destination + --copy-secrets Copy secrets from the source instance to the destination + -h, --help help for copy + -m, --message string Optional message attached to the plan deployment created on the destination + -o, --overrides string Path to a JSON or YAML file of param overrides deep-merged onto the source params +``` + +### SEE ALSO + +* [mass instance](/cli/commands/mass_instance) - Manage instances of IaC deployed in environments. diff --git a/docs/helpdocs/environment/deploy.md b/docs/helpdocs/environment/deploy.md new file mode 100644 index 0000000..0e2ba91 --- /dev/null +++ b/docs/helpdocs/environment/deploy.md @@ -0,0 +1,30 @@ +# Deploy Environment + +Triggers a deployment of every instance in an environment, in dependency +order. Any in-flight environment deployment is cancelled and replaced. + +The command returns as soon as the deployment is enqueued; instances are +provisioned asynchronously. Watch the deployments stream in the UI or list +them with `mass deployment list`. + +## Usage + +```bash +mass environment deploy +``` + +## Arguments + +- `environment`: full identifier of the environment to deploy + (e.g. `ecomm-staging`). + +## Examples + +```bash +# Deploy every instance in the staging environment. +mass environment deploy ecomm-staging + +# Deploy a freshly-forked preview env. +mass environment fork ecomm-production pr42 --copy-environment-defaults +mass environment deploy ecomm-pr42 +``` diff --git a/docs/helpdocs/environment/fork.md b/docs/helpdocs/environment/fork.md new file mode 100644 index 0000000..a2d112c --- /dev/null +++ b/docs/helpdocs/environment/fork.md @@ -0,0 +1,49 @@ +# Fork Environment + +Creates a new environment by forking an existing one. The fork is linked to +its parent via `parent_target_id`, and every instance is seeded with the +parent's params, version, and release channel. + +Re-running `fork` against the same parent with the same `new-ID` resets +the existing fork's state back to the parent's — params reset, defaults +re-apply, and any `--copy-*` flags re-fire. Re-running with a *different* +parent is rejected; a fork's parent is immutable. + +## Usage + +```bash +mass environment fork [flags] +``` + +## Arguments + +- `parent-environment`: full identifier of the environment to fork from + (e.g. `ecomm-production`). +- `new-ID`: local segment of the new environment's identifier. Must match + `^[a-z0-9]{1,20}$` — lowercase alphanumeric only, no dashes. The full + stored identifier becomes `-`. + +## Flags + +- `--name, -n`: human-readable name (defaults to `new-ID`). +- `--description, -d`: optional environment description. +- `--attributes, -a`: custom attributes for ABAC, e.g. + `-a region=us-east-1,data_classification=pii`. +- `--copy-environment-defaults`: also copy the parent's default resource + connections into the fork. +- `--copy-secrets`: copy every instance's secrets from the parent into the + fork. +- `--copy-remote-references`: copy every instance's remote references from + the parent into the fork. + +## Examples + +```bash +# Stand up a staging environment as a copy of production. +mass environment fork ecomm-production staging \ + --copy-environment-defaults \ + --copy-secrets + +# Re-fork to reset edits back to the parent's current state. +mass environment fork ecomm-production staging --copy-environment-defaults +``` diff --git a/docs/helpdocs/instance/copy.md b/docs/helpdocs/instance/copy.md new file mode 100644 index 0000000..9d38d2d --- /dev/null +++ b/docs/helpdocs/instance/copy.md @@ -0,0 +1,52 @@ +# Copy Instance + +Copies one instance's configuration into another instance of the same +component. The source's params (minus any fields the bundle marks +non-copyable) are written to the destination, optionally deep-merged with +`--overrides`. A plan deployment is created on the destination so the +changes can be reviewed before applying. + +Aliased as `promote` — same command, friendlier shape for the common +"promote staging to production" flow: + +```bash +mass instance promote ecomm-staging-db ecomm-production-db +``` + +## Usage + +```bash +mass instance copy [flags] +mass instance promote [flags] +``` + +## Arguments + +- `source`: full identifier of the instance to copy from + (e.g. `ecomm-staging-db`). +- `destination`: full identifier of the instance to copy into + (e.g. `ecomm-production-db`). Must be built from the same component as + the source. + +## Flags + +- `--message, -m`: optional message attached to the plan deployment created + on the destination (think: commit message). +- `--overrides, -o`: path to a JSON or YAML file of param overrides + deep-merged onto the source params before writing. +- `--copy-secrets`: also copy the source's secret values to the destination. +- `--copy-remote-references`: also copy the source's remote-reference + overrides to the destination. + +## Examples + +```bash +# Promote staging's config to production (review the plan before applying). +mass instance promote ecomm-staging-db ecomm-production-db -m "Promote DB config" + +# Promote with a size override and copy secrets. +mass instance copy ecomm-staging-db ecomm-production-db \ + --overrides ./prod-overrides.yaml \ + --copy-secrets \ + -m "Scale up DB for production" +``` From a06ab5cc1bd1d10e405f20927afd3b97363ff42c Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Fri, 15 May 2026 11:28:46 -0700 Subject: [PATCH 2/6] Add --follow to `environment deploy` Reuses the FollowEnvironment helper landed with the preview --follow flag. Behavior is identical: tail every deployment's logs, prefix each line with the instance id, exit once the rollout reaches a quiet steady state. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/environment.go | 11 ++++++++++- docs/generated/mass_environment_deploy.md | 11 +++++++++-- docs/helpdocs/environment/deploy.md | 6 ++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/cmd/environment.go b/cmd/environment.go index e77a052..2cb362b 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -118,11 +118,12 @@ func NewCmdEnvironment() *cobra.Command { environmentDeployCmd := &cobra.Command{ Use: "deploy [environment]", Short: "Deploy every instance in an environment, in dependency order", - Example: `mass environment deploy ecomm-staging`, + Example: `mass environment deploy ecomm-staging --follow`, Long: helpdocs.MustRender("environment/deploy"), Args: cobra.ExactArgs(1), RunE: runEnvironmentDeploy, } + environmentDeployCmd.Flags().Bool("follow", false, "Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.") environmentCmd.AddCommand(environmentExportCmd) environmentCmd.AddCommand(environmentGetCmd) @@ -535,6 +536,10 @@ func runEnvironmentDeploy(cmd *cobra.Command, args []string) error { ctx := context.Background() environmentID := args[0] + follow, err := cmd.Flags().GetBool("follow") + if err != nil { + return err + } cmd.SilenceUsage = true @@ -550,5 +555,9 @@ func runEnvironmentDeploy(cmd *cobra.Command, args []string) error { fmt.Printf("🚀 Deploying environment `%s` — instances roll out in dependency order asynchronously\n", env.ID) fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).EnvironmentURL(env.ID)) + + if follow { + return environment.FollowEnvironment(ctx, environment.NewFollowAPI(mdClient), env.ID, os.Stdout) + } return nil } diff --git a/docs/generated/mass_environment_deploy.md b/docs/generated/mass_environment_deploy.md index c926dca..fa136c6 100644 --- a/docs/generated/mass_environment_deploy.md +++ b/docs/generated/mass_environment_deploy.md @@ -30,6 +30,12 @@ mass environment deploy - `environment`: full identifier of the environment to deploy (e.g. `ecomm-staging`). +## Flags + +- `--follow`: stream every deployment's logs to stdout until the rollout + completes. Each line is prefixed with the instance id so the interleaved + output stays grep-friendly when multiple deployments run in parallel. + ## Examples ```bash @@ -49,13 +55,14 @@ mass environment deploy [environment] [flags] ### Examples ``` -mass environment deploy ecomm-staging +mass environment deploy ecomm-staging --follow ``` ### Options ``` - -h, --help help for deploy + --follow Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id. + -h, --help help for deploy ``` ### SEE ALSO diff --git a/docs/helpdocs/environment/deploy.md b/docs/helpdocs/environment/deploy.md index 0e2ba91..5a6935e 100644 --- a/docs/helpdocs/environment/deploy.md +++ b/docs/helpdocs/environment/deploy.md @@ -18,6 +18,12 @@ mass environment deploy - `environment`: full identifier of the environment to deploy (e.g. `ecomm-staging`). +## Flags + +- `--follow`: stream every deployment's logs to stdout until the rollout + completes. Each line is prefixed with the instance id so the interleaved + output stays grep-friendly when multiple deployments run in parallel. + ## Examples ```bash From 8495f66b8e6e2e785536890893dfcae5574b9c4e Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Fri, 15 May 2026 14:24:23 -0700 Subject: [PATCH 3/6] Make `instance copy` / `promote` take destination via --to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mass instance promote ecomm-staging-db ecomm-production-db` reads ambiguously — easy to land on the wrong side at the end of a long day. Promote (and copy) now take the destination through a required `--to` flag: mass instance promote ecomm-staging-db --to ecomm-production-db Same for copy. Helpdoc and generated docs follow the new shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/instance.go | 15 ++++++++++----- docs/generated/mass_instance.md | 2 +- docs/generated/mass_instance_copy.md | 25 +++++++++++++------------ docs/helpdocs/instance/copy.md | 18 +++++++++--------- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/cmd/instance.go b/cmd/instance.go index 4e03b1e..b9eff73 100644 --- a/cmd/instance.go +++ b/cmd/instance.go @@ -118,18 +118,20 @@ func NewCmdInstance() *cobra.Command { instanceOrphanCmd.Flags().Bool("delete-state", false, "Also delete the remote Terraform/OpenTofu state files (irreversible)") instanceCopyCmd := &cobra.Command{ - Use: `copy [source] [destination]`, + Use: `copy [source] --to [destination]`, Aliases: []string{"promote"}, - Short: "Copy an instance's configuration into another instance of the same component", - Example: `mass instance copy ecomm-staging-db ecomm-production-db --copy-secrets`, + Short: "Copy an instance's configuration to another instance of the same component", + Example: `mass instance promote ecomm-staging-db --to ecomm-production-db --copy-secrets`, Long: helpdocs.MustRender("instance/copy"), - Args: cobra.ExactArgs(2), + Args: cobra.ExactArgs(1), RunE: runInstanceCopy, } + instanceCopyCmd.Flags().String("to", "", "Destination instance (required). Must be built from the same component as the source.") instanceCopyCmd.Flags().StringP("message", "m", "", "Optional message attached to the plan deployment created on the destination") instanceCopyCmd.Flags().StringP("overrides", "o", "", "Path to a JSON or YAML file of param overrides deep-merged onto the source params") instanceCopyCmd.Flags().Bool("copy-secrets", false, "Copy secrets from the source instance to the destination") instanceCopyCmd.Flags().Bool("copy-remote-references", false, "Copy remote-reference overrides from the source instance to the destination") + _ = instanceCopyCmd.MarkFlagRequired("to") instanceCmd.AddCommand(instanceDeployCmd) instanceCmd.AddCommand(instanceExportCmd) @@ -325,7 +327,10 @@ func runInstanceCopy(cmd *cobra.Command, args []string) error { ctx := context.Background() sourceID := args[0] - destinationID := args[1] + destinationID, err := cmd.Flags().GetString("to") + if err != nil { + return err + } message, err := cmd.Flags().GetString("message") if err != nil { return err diff --git a/docs/generated/mass_instance.md b/docs/generated/mass_instance.md index 31a8575..c00e6dc 100644 --- a/docs/generated/mass_instance.md +++ b/docs/generated/mass_instance.md @@ -30,7 +30,7 @@ Instances are used to: ### SEE ALSO * [mass](/cli/commands/mass) - Massdriver Cloud CLI -* [mass instance copy](/cli/commands/mass_instance_copy) - Copy an instance's configuration into another instance of the same component +* [mass instance copy](/cli/commands/mass_instance_copy) - Copy an instance's configuration to another instance of the same component * [mass instance deploy](/cli/commands/mass_instance_deploy) - Deploy instances * [mass instance destroy](/cli/commands/mass_instance_destroy) - Destroy (decommission) an instance * [mass instance export](/cli/commands/mass_instance_export) - Export instances diff --git a/docs/generated/mass_instance_copy.md b/docs/generated/mass_instance_copy.md index 597ff9a..5599af6 100644 --- a/docs/generated/mass_instance_copy.md +++ b/docs/generated/mass_instance_copy.md @@ -6,13 +6,13 @@ sidebar_label: Mass Instance Copy --- ## mass instance copy -Copy an instance's configuration into another instance of the same component +Copy an instance's configuration to another instance of the same component ### Synopsis # Copy Instance -Copies one instance's configuration into another instance of the same +Copies one instance's configuration to another instance of the same component. The source's params (minus any fields the bundle marks non-copyable) are written to the destination, optionally deep-merged with `--overrides`. A plan deployment is created on the destination so the @@ -22,26 +22,25 @@ Aliased as `promote` — same command, friendlier shape for the common "promote staging to production" flow: ```bash -mass instance promote ecomm-staging-db ecomm-production-db +mass instance promote ecomm-staging-db --to ecomm-production-db ``` ## Usage ```bash -mass instance copy [flags] -mass instance promote [flags] +mass instance copy --to [flags] +mass instance promote --to [flags] ``` ## Arguments - `source`: full identifier of the instance to copy from (e.g. `ecomm-staging-db`). -- `destination`: full identifier of the instance to copy into - (e.g. `ecomm-production-db`). Must be built from the same component as - the source. ## Flags +- `--to`: destination instance (required). Must be built from the same + component as the source (e.g. `ecomm-production-db`). - `--message, -m`: optional message attached to the plan deployment created on the destination (think: commit message). - `--overrides, -o`: path to a JSON or YAML file of param overrides @@ -54,10 +53,11 @@ mass instance promote [flags] ```bash # Promote staging's config to production (review the plan before applying). -mass instance promote ecomm-staging-db ecomm-production-db -m "Promote DB config" +mass instance promote ecomm-staging-db --to ecomm-production-db -m "Promote DB config" # Promote with a size override and copy secrets. -mass instance copy ecomm-staging-db ecomm-production-db \ +mass instance copy ecomm-staging-db \ + --to ecomm-production-db \ --overrides ./prod-overrides.yaml \ --copy-secrets \ -m "Scale up DB for production" @@ -65,13 +65,13 @@ mass instance copy ecomm-staging-db ecomm-production-db \ ``` -mass instance copy [source] [destination] [flags] +mass instance copy [source] --to [destination] [flags] ``` ### Examples ``` -mass instance copy ecomm-staging-db ecomm-production-db --copy-secrets +mass instance promote ecomm-staging-db --to ecomm-production-db --copy-secrets ``` ### Options @@ -82,6 +82,7 @@ mass instance copy ecomm-staging-db ecomm-production-db --copy-secrets -h, --help help for copy -m, --message string Optional message attached to the plan deployment created on the destination -o, --overrides string Path to a JSON or YAML file of param overrides deep-merged onto the source params + --to string Destination instance (required). Must be built from the same component as the source. ``` ### SEE ALSO diff --git a/docs/helpdocs/instance/copy.md b/docs/helpdocs/instance/copy.md index 9d38d2d..1ffa0d7 100644 --- a/docs/helpdocs/instance/copy.md +++ b/docs/helpdocs/instance/copy.md @@ -1,6 +1,6 @@ # Copy Instance -Copies one instance's configuration into another instance of the same +Copies one instance's configuration to another instance of the same component. The source's params (minus any fields the bundle marks non-copyable) are written to the destination, optionally deep-merged with `--overrides`. A plan deployment is created on the destination so the @@ -10,26 +10,25 @@ Aliased as `promote` — same command, friendlier shape for the common "promote staging to production" flow: ```bash -mass instance promote ecomm-staging-db ecomm-production-db +mass instance promote ecomm-staging-db --to ecomm-production-db ``` ## Usage ```bash -mass instance copy [flags] -mass instance promote [flags] +mass instance copy --to [flags] +mass instance promote --to [flags] ``` ## Arguments - `source`: full identifier of the instance to copy from (e.g. `ecomm-staging-db`). -- `destination`: full identifier of the instance to copy into - (e.g. `ecomm-production-db`). Must be built from the same component as - the source. ## Flags +- `--to`: destination instance (required). Must be built from the same + component as the source (e.g. `ecomm-production-db`). - `--message, -m`: optional message attached to the plan deployment created on the destination (think: commit message). - `--overrides, -o`: path to a JSON or YAML file of param overrides @@ -42,10 +41,11 @@ mass instance promote [flags] ```bash # Promote staging's config to production (review the plan before applying). -mass instance promote ecomm-staging-db ecomm-production-db -m "Promote DB config" +mass instance promote ecomm-staging-db --to ecomm-production-db -m "Promote DB config" # Promote with a size override and copy secrets. -mass instance copy ecomm-staging-db ecomm-production-db \ +mass instance copy ecomm-staging-db \ + --to ecomm-production-db \ --overrides ./prod-overrides.yaml \ --copy-secrets \ -m "Scale up DB for production" From 959a8a3170274c53dffc8637b172867621c648eb Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Fri, 15 May 2026 15:47:07 -0700 Subject: [PATCH 4/6] Drop --message flag from `instance copy` / `promote` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CopyInstanceInput.Message was removed from the V2 schema — copy is pure config staging, not deployment. SDK v0.2.2 dropped Message from the public CopyInput type. Drop the corresponding CLI flag and update the helpdoc to point users at `mass instance deploy` for the follow-up deployment. --- cmd/instance.go | 8 +------- docs/generated/mass_instance_copy.md | 16 +++++++--------- docs/helpdocs/instance/copy.md | 15 +++++++-------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/cmd/instance.go b/cmd/instance.go index b9eff73..d9940f7 100644 --- a/cmd/instance.go +++ b/cmd/instance.go @@ -127,7 +127,6 @@ func NewCmdInstance() *cobra.Command { RunE: runInstanceCopy, } instanceCopyCmd.Flags().String("to", "", "Destination instance (required). Must be built from the same component as the source.") - instanceCopyCmd.Flags().StringP("message", "m", "", "Optional message attached to the plan deployment created on the destination") instanceCopyCmd.Flags().StringP("overrides", "o", "", "Path to a JSON or YAML file of param overrides deep-merged onto the source params") instanceCopyCmd.Flags().Bool("copy-secrets", false, "Copy secrets from the source instance to the destination") instanceCopyCmd.Flags().Bool("copy-remote-references", false, "Copy remote-reference overrides from the source instance to the destination") @@ -331,10 +330,6 @@ func runInstanceCopy(cmd *cobra.Command, args []string) error { if err != nil { return err } - message, err := cmd.Flags().GetString("message") - if err != nil { - return err - } overridesPath, err := cmd.Flags().GetString("overrides") if err != nil { return err @@ -367,7 +362,6 @@ func runInstanceCopy(cmd *cobra.Command, args []string) error { Overrides: overrides, CopySecrets: copySecrets, CopyRemoteReferences: copyRefs, - Message: message, } inst, err := mdClient.Instances.Copy(ctx, sourceID, destinationID, input) @@ -375,7 +369,7 @@ func runInstanceCopy(cmd *cobra.Command, args []string) error { return err } - fmt.Printf("✅ Instance `%s` copied to `%s` — plan deployment created on the destination\n", sourceID, destinationID) + fmt.Printf("✅ Instance `%s` configuration copied to `%s`\n", sourceID, destinationID) fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).InstanceURL(inst.ID)) return nil } diff --git a/docs/generated/mass_instance_copy.md b/docs/generated/mass_instance_copy.md index 5599af6..261a2cd 100644 --- a/docs/generated/mass_instance_copy.md +++ b/docs/generated/mass_instance_copy.md @@ -15,14 +15,15 @@ Copy an instance's configuration to another instance of the same component Copies one instance's configuration to another instance of the same component. The source's params (minus any fields the bundle marks non-copyable) are written to the destination, optionally deep-merged with -`--overrides`. A plan deployment is created on the destination so the -changes can be reviewed before applying. +`--overrides`. Deployment is a separate action — run `mass instance +deploy ` when you're ready to apply. Aliased as `promote` — same command, friendlier shape for the common "promote staging to production" flow: ```bash mass instance promote ecomm-staging-db --to ecomm-production-db +mass instance deploy ecomm-production-db ``` ## Usage @@ -41,8 +42,6 @@ mass instance promote --to [flags] - `--to`: destination instance (required). Must be built from the same component as the source (e.g. `ecomm-production-db`). -- `--message, -m`: optional message attached to the plan deployment created - on the destination (think: commit message). - `--overrides, -o`: path to a JSON or YAML file of param overrides deep-merged onto the source params before writing. - `--copy-secrets`: also copy the source's secret values to the destination. @@ -52,15 +51,15 @@ mass instance promote --to [flags] ## Examples ```bash -# Promote staging's config to production (review the plan before applying). -mass instance promote ecomm-staging-db --to ecomm-production-db -m "Promote DB config" +# Promote staging's config to production. +mass instance promote ecomm-staging-db --to ecomm-production-db +mass instance deploy ecomm-production-db # Promote with a size override and copy secrets. mass instance copy ecomm-staging-db \ --to ecomm-production-db \ --overrides ./prod-overrides.yaml \ - --copy-secrets \ - -m "Scale up DB for production" + --copy-secrets ``` @@ -80,7 +79,6 @@ mass instance promote ecomm-staging-db --to ecomm-production-db --copy-secrets --copy-remote-references Copy remote-reference overrides from the source instance to the destination --copy-secrets Copy secrets from the source instance to the destination -h, --help help for copy - -m, --message string Optional message attached to the plan deployment created on the destination -o, --overrides string Path to a JSON or YAML file of param overrides deep-merged onto the source params --to string Destination instance (required). Must be built from the same component as the source. ``` diff --git a/docs/helpdocs/instance/copy.md b/docs/helpdocs/instance/copy.md index 1ffa0d7..658aafa 100644 --- a/docs/helpdocs/instance/copy.md +++ b/docs/helpdocs/instance/copy.md @@ -3,14 +3,15 @@ Copies one instance's configuration to another instance of the same component. The source's params (minus any fields the bundle marks non-copyable) are written to the destination, optionally deep-merged with -`--overrides`. A plan deployment is created on the destination so the -changes can be reviewed before applying. +`--overrides`. Deployment is a separate action — run `mass instance +deploy ` when you're ready to apply. Aliased as `promote` — same command, friendlier shape for the common "promote staging to production" flow: ```bash mass instance promote ecomm-staging-db --to ecomm-production-db +mass instance deploy ecomm-production-db ``` ## Usage @@ -29,8 +30,6 @@ mass instance promote --to [flags] - `--to`: destination instance (required). Must be built from the same component as the source (e.g. `ecomm-production-db`). -- `--message, -m`: optional message attached to the plan deployment created - on the destination (think: commit message). - `--overrides, -o`: path to a JSON or YAML file of param overrides deep-merged onto the source params before writing. - `--copy-secrets`: also copy the source's secret values to the destination. @@ -40,13 +39,13 @@ mass instance promote --to [flags] ## Examples ```bash -# Promote staging's config to production (review the plan before applying). -mass instance promote ecomm-staging-db --to ecomm-production-db -m "Promote DB config" +# Promote staging's config to production. +mass instance promote ecomm-staging-db --to ecomm-production-db +mass instance deploy ecomm-production-db # Promote with a size override and copy secrets. mass instance copy ecomm-staging-db \ --to ecomm-production-db \ --overrides ./prod-overrides.yaml \ - --copy-secrets \ - -m "Scale up DB for production" + --copy-secrets ``` From dc2c395c78c963dc67b9c358f406a208c71d596d Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Fri, 15 May 2026 17:09:59 -0700 Subject: [PATCH 5/6] Extract new subcommand builders to satisfy funlen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NewCmdEnvironment was 110 lines and NewCmdInstance was 111 lines after the new commands landed (preview/fork/deploy on environment; copy on instance) — over golangci-lint's 100-line funlen cap. Each new command's construction now lives in its own `new*Cmd` builder; the parents just AddCommand them. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/environment.go | 63 ++++++++++++++++++++++++++-------------------- cmd/instance.go | 35 ++++++++++++++------------ 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/cmd/environment.go b/cmd/environment.go index 2cb362b..3fc6cbb 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -87,20 +87,37 @@ func NewCmdEnvironment() *cobra.Command { RunE: runEnvironmentDefault, } - environmentPreviewCmd := &cobra.Command{ + environmentCmd.AddCommand(environmentExportCmd) + environmentCmd.AddCommand(environmentGetCmd) + environmentCmd.AddCommand(environmentListCmd) + environmentCmd.AddCommand(environmentCreateCmd) + environmentCmd.AddCommand(environmentUpdateCmd) + environmentCmd.AddCommand(environmentDefaultCmd) + environmentCmd.AddCommand(newEnvironmentPreviewCmd()) + environmentCmd.AddCommand(newEnvironmentForkCmd()) + environmentCmd.AddCommand(newEnvironmentDeployCmd()) + + return environmentCmd +} + +func newEnvironmentPreviewCmd() *cobra.Command { + c := &cobra.Command{ Use: "preview [ID]", Short: "Converge a preview environment from a YAML config", Long: helpdocs.MustRender("environment/preview"), Args: cobra.ExactArgs(1), RunE: runEnvironmentPreview, } - environmentPreviewCmd.Flags().StringP("file", "f", "preview.yaml", "Path to the preview config YAML") - environmentPreviewCmd.Flags().StringP("name", "n", "", "Environment name (defaults to ID if not provided)") - environmentPreviewCmd.Flags().StringP("description", "d", "", "Optional environment description") - environmentPreviewCmd.Flags().StringToStringP("attributes", "a", nil, "Custom attributes for ABAC (e.g. -a environment=preview,region=uswest). Overrides `attributes:` in the config file.") - environmentPreviewCmd.Flags().Bool("follow", false, "Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.") + c.Flags().StringP("file", "f", "preview.yaml", "Path to the preview config YAML") + c.Flags().StringP("name", "n", "", "Environment name (defaults to ID if not provided)") + c.Flags().StringP("description", "d", "", "Optional environment description") + c.Flags().StringToStringP("attributes", "a", nil, "Custom attributes for ABAC (e.g. -a environment=preview,region=uswest). Overrides `attributes:` in the config file.") + c.Flags().Bool("follow", false, "Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.") + return c +} - environmentForkCmd := &cobra.Command{ +func newEnvironmentForkCmd() *cobra.Command { + c := &cobra.Command{ Use: "fork [parent-environment] [new-ID]", Short: "Fork an existing environment", Example: `mass environment fork ecomm-production staging`, @@ -108,14 +125,17 @@ func NewCmdEnvironment() *cobra.Command { Args: cobra.ExactArgs(2), RunE: runEnvironmentFork, } - environmentForkCmd.Flags().StringP("name", "n", "", "Environment name (defaults to new-ID if not provided)") - environmentForkCmd.Flags().StringP("description", "d", "", "Optional environment description") - environmentForkCmd.Flags().StringToStringP("attributes", "a", nil, "Custom attributes for ABAC (e.g. -a region=uswest)") - environmentForkCmd.Flags().Bool("copy-environment-defaults", false, "Copy the parent's default resource connections into the fork") - environmentForkCmd.Flags().Bool("copy-secrets", false, "Copy every instance's secrets from the parent into the fork") - environmentForkCmd.Flags().Bool("copy-remote-references", false, "Copy every instance's remote references from the parent into the fork") + c.Flags().StringP("name", "n", "", "Environment name (defaults to new-ID if not provided)") + c.Flags().StringP("description", "d", "", "Optional environment description") + c.Flags().StringToStringP("attributes", "a", nil, "Custom attributes for ABAC (e.g. -a region=uswest)") + c.Flags().Bool("copy-environment-defaults", false, "Copy the parent's default resource connections into the fork") + c.Flags().Bool("copy-secrets", false, "Copy every instance's secrets from the parent into the fork") + c.Flags().Bool("copy-remote-references", false, "Copy every instance's remote references from the parent into the fork") + return c +} - environmentDeployCmd := &cobra.Command{ +func newEnvironmentDeployCmd() *cobra.Command { + c := &cobra.Command{ Use: "deploy [environment]", Short: "Deploy every instance in an environment, in dependency order", Example: `mass environment deploy ecomm-staging --follow`, @@ -123,19 +143,8 @@ func NewCmdEnvironment() *cobra.Command { Args: cobra.ExactArgs(1), RunE: runEnvironmentDeploy, } - environmentDeployCmd.Flags().Bool("follow", false, "Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.") - - environmentCmd.AddCommand(environmentExportCmd) - environmentCmd.AddCommand(environmentGetCmd) - environmentCmd.AddCommand(environmentListCmd) - environmentCmd.AddCommand(environmentCreateCmd) - environmentCmd.AddCommand(environmentUpdateCmd) - environmentCmd.AddCommand(environmentDefaultCmd) - environmentCmd.AddCommand(environmentPreviewCmd) - environmentCmd.AddCommand(environmentForkCmd) - environmentCmd.AddCommand(environmentDeployCmd) - - return environmentCmd + c.Flags().Bool("follow", false, "Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.") + return c } func runEnvironmentExport(cmd *cobra.Command, args []string) error { diff --git a/cmd/instance.go b/cmd/instance.go index d9940f7..04dbdd3 100644 --- a/cmd/instance.go +++ b/cmd/instance.go @@ -117,21 +117,6 @@ func NewCmdInstance() *cobra.Command { instanceOrphanCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt") instanceOrphanCmd.Flags().Bool("delete-state", false, "Also delete the remote Terraform/OpenTofu state files (irreversible)") - instanceCopyCmd := &cobra.Command{ - Use: `copy [source] --to [destination]`, - Aliases: []string{"promote"}, - Short: "Copy an instance's configuration to another instance of the same component", - Example: `mass instance promote ecomm-staging-db --to ecomm-production-db --copy-secrets`, - Long: helpdocs.MustRender("instance/copy"), - Args: cobra.ExactArgs(1), - RunE: runInstanceCopy, - } - instanceCopyCmd.Flags().String("to", "", "Destination instance (required). Must be built from the same component as the source.") - instanceCopyCmd.Flags().StringP("overrides", "o", "", "Path to a JSON or YAML file of param overrides deep-merged onto the source params") - instanceCopyCmd.Flags().Bool("copy-secrets", false, "Copy secrets from the source instance to the destination") - instanceCopyCmd.Flags().Bool("copy-remote-references", false, "Copy remote-reference overrides from the source instance to the destination") - _ = instanceCopyCmd.MarkFlagRequired("to") - instanceCmd.AddCommand(instanceDeployCmd) instanceCmd.AddCommand(instanceExportCmd) instanceCmd.AddCommand(instanceGetCmd) @@ -139,11 +124,29 @@ func NewCmdInstance() *cobra.Command { instanceCmd.AddCommand(instanceVersionCmd) instanceCmd.AddCommand(instanceDestroyCmd) instanceCmd.AddCommand(instanceOrphanCmd) - instanceCmd.AddCommand(instanceCopyCmd) + instanceCmd.AddCommand(newInstanceCopyCmd()) return instanceCmd } +func newInstanceCopyCmd() *cobra.Command { + c := &cobra.Command{ + Use: `copy [source] --to [destination]`, + Aliases: []string{"promote"}, + Short: "Copy an instance's configuration to another instance of the same component", + Example: `mass instance promote ecomm-staging-db --to ecomm-production-db --copy-secrets`, + Long: helpdocs.MustRender("instance/copy"), + Args: cobra.ExactArgs(1), + RunE: runInstanceCopy, + } + c.Flags().String("to", "", "Destination instance (required). Must be built from the same component as the source.") + c.Flags().StringP("overrides", "o", "", "Path to a JSON or YAML file of param overrides deep-merged onto the source params") + c.Flags().Bool("copy-secrets", false, "Copy secrets from the source instance to the destination") + c.Flags().Bool("copy-remote-references", false, "Copy remote-reference overrides from the source instance to the destination") + _ = c.MarkFlagRequired("to") + return c +} + func runInstanceGet(cmd *cobra.Command, args []string) error { ctx := context.Background() From 4160ecb3fac9ae8e0813fbe72a1e091ce44aa6aa Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Mon, 18 May 2026 11:45:20 -0700 Subject: [PATCH 6/6] Add `mass environment decommission` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the new V2 `decommissionEnvironment` mutation (platform PR #3259) through `Environments.Decommission` (SDK v0.2.4) so a fan-out teardown is a single CLI call. Mirrors `environment deploy`: - Reverse-dependency-order tear-down across every instance. - `--follow` streams interleaved per-instance logs. - Async — returns once the wave is enqueued. The environment shell stays put; run `mass environment delete` after to remove the empty environment. Decommissioning is rejected when `decommissionProtection` is on; disable via `updateEnvironment` first. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/environment.go | 44 ++++++++++ docs/generated/mass_environment.md | 1 + .../mass_environment_decommission.md | 80 +++++++++++++++++++ docs/helpdocs/environment/decommission.md | 46 +++++++++++ go.mod | 2 +- go.sum | 2 + 6 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 docs/generated/mass_environment_decommission.md create mode 100644 docs/helpdocs/environment/decommission.md diff --git a/cmd/environment.go b/cmd/environment.go index 3fc6cbb..17dddc8 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -96,6 +96,7 @@ func NewCmdEnvironment() *cobra.Command { environmentCmd.AddCommand(newEnvironmentPreviewCmd()) environmentCmd.AddCommand(newEnvironmentForkCmd()) environmentCmd.AddCommand(newEnvironmentDeployCmd()) + environmentCmd.AddCommand(newEnvironmentDecommissionCmd()) return environmentCmd } @@ -147,6 +148,19 @@ func newEnvironmentDeployCmd() *cobra.Command { return c } +func newEnvironmentDecommissionCmd() *cobra.Command { + c := &cobra.Command{ + Use: "decommission [environment]", + Short: "Decommission every instance in an environment, in reverse dependency order", + Example: `mass environment decommission ecomm-pr42 --follow`, + Long: helpdocs.MustRender("environment/decommission"), + Args: cobra.ExactArgs(1), + RunE: runEnvironmentDecommission, + } + c.Flags().Bool("follow", false, "Stream every decommission deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.") + return c +} + func runEnvironmentExport(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -570,3 +584,33 @@ func runEnvironmentDeploy(cmd *cobra.Command, args []string) error { } return nil } + +func runEnvironmentDecommission(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + environmentID := args[0] + follow, err := cmd.Flags().GetBool("follow") + if err != nil { + return err + } + + cmd.SilenceUsage = true + + mdClient, mdClientErr := massdriver.NewClient() + if mdClientErr != nil { + return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) + } + + env, err := mdClient.Environments.Decommission(ctx, environmentID) + if err != nil { + return err + } + + fmt.Printf("🔻 Decommissioning environment `%s` — instances tear down in reverse dependency order asynchronously\n", env.ID) + fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).EnvironmentURL(env.ID)) + + if follow { + return environment.FollowEnvironment(ctx, environment.NewFollowAPI(mdClient), env.ID, os.Stdout) + } + return nil +} diff --git a/docs/generated/mass_environment.md b/docs/generated/mass_environment.md index fe68ac3..d6a9837 100644 --- a/docs/generated/mass_environment.md +++ b/docs/generated/mass_environment.md @@ -27,6 +27,7 @@ Environments can be modeled by application stage (production, staging, developme * [mass](/cli/commands/mass) - Massdriver Cloud CLI * [mass environment create](/cli/commands/mass_environment_create) - Create an environment +* [mass environment decommission](/cli/commands/mass_environment_decommission) - Decommission every instance in an environment, in reverse dependency order * [mass environment default](/cli/commands/mass_environment_default) - Set an environment default connection * [mass environment deploy](/cli/commands/mass_environment_deploy) - Deploy every instance in an environment, in dependency order * [mass environment export](/cli/commands/mass_environment_export) - Export an environment from Massdriver diff --git a/docs/generated/mass_environment_decommission.md b/docs/generated/mass_environment_decommission.md new file mode 100644 index 0000000..642e85a --- /dev/null +++ b/docs/generated/mass_environment_decommission.md @@ -0,0 +1,80 @@ +--- +id: mass_environment_decommission.md +slug: /cli/commands/mass_environment_decommission +title: Mass Environment Decommission +sidebar_label: Mass Environment Decommission +--- +## mass environment decommission + +Decommission every instance in an environment, in reverse dependency order + +### Synopsis + +# Decommission Environment + +Tears down every instance in an environment in reverse dependency order. +The environment shell stays in place so it can be redeployed; run +`mass environment delete` afterwards to remove the empty environment. + +Any in-flight environment deployment is cancelled and replaced. The +command returns as soon as the decommission wave is enqueued; instances +are torn down asynchronously. + +Decommissioning is blocked when the environment has +`decommissionProtection: true`. Disable it first with +`mass environment update --decommission-protection=false` (or via the +UI / API) before retrying. + +## Usage + +```bash +mass environment decommission +``` + +## Arguments + +- `environment`: full identifier of the environment to decommission + (e.g. `ecomm-pr42`). + +## Flags + +- `--follow`: stream every decommission deployment's logs to stdout + until the rollout completes. Each line is prefixed with the instance + id so the interleaved output stays grep-friendly when multiple + decommissions run in parallel. + +## Examples + +```bash +# Tear down every instance in a preview env. +mass environment decommission ecomm-pr42 + +# Tear it down and watch the logs. +mass environment decommission ecomm-pr42 --follow + +# Full preview-env teardown: decommission instances, then delete the shell. +mass environment decommission ecomm-pr42 --follow +mass environment delete ecomm-pr42 +``` + + +``` +mass environment decommission [environment] [flags] +``` + +### Examples + +``` +mass environment decommission ecomm-pr42 --follow +``` + +### Options + +``` + --follow Stream every decommission deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id. + -h, --help help for decommission +``` + +### SEE ALSO + +* [mass environment](/cli/commands/mass_environment) - Environment management diff --git a/docs/helpdocs/environment/decommission.md b/docs/helpdocs/environment/decommission.md new file mode 100644 index 0000000..c313d08 --- /dev/null +++ b/docs/helpdocs/environment/decommission.md @@ -0,0 +1,46 @@ +# Decommission Environment + +Tears down every instance in an environment in reverse dependency order. +The environment shell stays in place so it can be redeployed; run +`mass environment delete` afterwards to remove the empty environment. + +Any in-flight environment deployment is cancelled and replaced. The +command returns as soon as the decommission wave is enqueued; instances +are torn down asynchronously. + +Decommissioning is blocked when the environment has +`decommissionProtection: true`. Disable it first with +`mass environment update --decommission-protection=false` (or via the +UI / API) before retrying. + +## Usage + +```bash +mass environment decommission +``` + +## Arguments + +- `environment`: full identifier of the environment to decommission + (e.g. `ecomm-pr42`). + +## Flags + +- `--follow`: stream every decommission deployment's logs to stdout + until the rollout completes. Each line is prefixed with the instance + id so the interleaved output stays grep-friendly when multiple + decommissions run in parallel. + +## Examples + +```bash +# Tear down every instance in a preview env. +mass environment decommission ecomm-pr42 + +# Tear it down and watch the logs. +mass environment decommission ecomm-pr42 --follow + +# Full preview-env teardown: decommission instances, then delete the shell. +mass environment decommission ecomm-pr42 --follow +mass environment delete ecomm-pr42 +``` diff --git a/go.mod b/go.mod index a6aeb39..a62e962 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/itchyny/gojq v0.12.16 github.com/manifoldco/promptui v0.9.0 github.com/massdriver-cloud/airlock v0.0.9 - github.com/massdriver-cloud/massdriver-sdk-go v0.2.3 + github.com/massdriver-cloud/massdriver-sdk-go v0.2.4 github.com/mattn/go-runewidth v0.0.16 github.com/opencontainers/image-spec v1.1.1 github.com/osteele/liquid v1.7.0 diff --git a/go.sum b/go.sum index 780e02a..7594b09 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/massdriver-cloud/airlock v0.0.9 h1:t+jTY6nZEiPZNTKx0wEgQTPztIxL4u0RFv github.com/massdriver-cloud/airlock v0.0.9/go.mod h1:igJm33JvINiUtbyEspUeKUWyWewG+jYyxO1UDHqLp9Q= github.com/massdriver-cloud/massdriver-sdk-go v0.2.3 h1:gKRiSbJPI1uWVBRmoexPNbi9IJEYZ2upciMTjxRHb5I= github.com/massdriver-cloud/massdriver-sdk-go v0.2.3/go.mod h1:6NrSP+wfGQvUOAggsz10/Wkln8CKmk3VBnD+OJzZgFY= +github.com/massdriver-cloud/massdriver-sdk-go v0.2.4 h1:5UqNA0vBkzvqNa3H94SoVlDmxiJiRPltklO/j9/H+F0= +github.com/massdriver-cloud/massdriver-sdk-go v0.2.4/go.mod h1:6NrSP+wfGQvUOAggsz10/Wkln8CKmk3VBnD+OJzZgFY= github.com/massdriver-cloud/terraform-config-inspect v0.0.1 h1:eLtKFRaklHIxcPvUtZmNacl28n4QIHr29pJzw/u/FKU= github.com/massdriver-cloud/terraform-config-inspect v0.0.1/go.mod h1:3AbDpWxIRMdMAg7FDmTJuVBhCGNwdm49cBIOmUHjqRg= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=