Skip to content

feat: add deployment mode selection to parameter collection#2135

Open
lionello wants to merge 6 commits into
mainfrom
lio/pulumi-config
Open

feat: add deployment mode selection to parameter collection#2135
lionello wants to merge 6 commits into
mainfrom
lio/pulumi-config

Conversation

@lionello
Copy link
Copy Markdown
Member

@lionello lionello commented May 29, 2026

Description

  • add new defang recipe … commands, with list, show, activate, deactivate
  • before spawning CD job, look up recipe using new GetRecipe gRPC and attach PulumiConfig (if any) to the ProjectUpdate

Linked Issues

This needs https://github.com/DefangLabs/defang-mvp/pull/3027

Part of https://github.com/DefangLabs/defang-global/issues/75

Checklist

  • I have performed a self-review of my code
  • I have added appropriate tests
  • I have updated the Defang CLI docs and/or README to reflect my changes, if necessary

Summary by CodeRabbit

  • New Features

    • New recipe CLI: list, show, activate, deactivate recipes.
    • Deployment modes converted to "recipes" and selectable during stack setup and configuration.
    • Recipe values are included in deployments and previews; estimates reflect selected recipe.
    • Improved provider UX: better Azure detection and defaulting.
  • Tests

    • Tests updated to exercise recipe-based flows.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Recipe type and migrates mode handling from Mode to Recipe across stacks, compose/deploy, estimate/validation, client APIs, CLI commands, and tests; adds recipe CRUD client methods and CLI commands; includes Recipe in BYOC deploy payloads and persisted deployment records.

Changes

Recipe type and workflow migration

Layer / File(s) Summary
Recipe type definition and Mode deprecation
src/pkg/modes/recipe.go, src/pkg/modes/mode.go, src/pkg/modes/mode_test.go
Introduces Recipe with parsing, Set, String, Type, Mode, FromMode; marks Mode deprecated and updates mode tests.
Stacks Parameters and serialization
src/pkg/stacks/stacks.go, src/pkg/stacks/stacks_test.go, src/pkg/stacks/manager.go
Changes Parameters/ListItem to use modes.Recipe; updates ToMap/paramsFromMap to parse/emit recipe strings and adjusts manager conversions.
Session loader and manager integration
src/pkg/session/session.go, src/pkg/stacks/manager_test.go, src/pkg/stacks/stacks_test.go
Uses RecipeUnspecified as sentinel for overrides; derives params from remote mode via FromMode; updates related tests.
Stack wizard and selector prompting
src/pkg/stacks/wizard.go, src/pkg/stacks/selector_test.go, src/pkg/stacks/wizard_test.go
Elicits deployment recipe in wizard for non-Defang providers (RequestEnum: mode) and updates selector/wizard tests and mocks to expect it.
Recipe Fabric client APIs and mocks
src/pkg/cli/client/client.go, src/pkg/cli/client/grpc.go, src/pkg/cli/client/mock.go, src/pkg/cli/client/provider.go, src/pkg/cli/common.go
Adds GetRecipe, ListRecipes, PutRecipe to FabricClient and GrpcClient; extends mock client; adds Recipe to DeployRequest and deployment params.
Recipe CLI command group
src/cmd/cli/command/commands.go, src/cmd/cli/command/recipe.go
Registers recipe command with list, show, activate, deactivate subcommands wired to cli helpers and requiring auth.
Recipe CLI helpers
src/pkg/cli/recipeShow.go, src/pkg/cli/recipeList.go, src/pkg/cli/recipeActivate.go
Implements RecipeShow, RecipeList, and RecipeActivate to get/list/modify recipes via fabric client and render terminal output.
CLI globals and estimate defaults
src/cmd/cli/command/globals.go, src/cmd/cli/command/estimate.go, src/cmd/cli/command/globals_test.go, src/cmd/cli/command/estimate_test.go
Changes default sentinel to RecipeUnspecified, default stack mode to RecipeAffordable in estimate flow, adds Azure provider detection/description, and updates tests.
Validation and estimate flows
src/pkg/cli/compose/validation.go, src/pkg/cli/compose/validation_test.go, src/pkg/cli/estimate.go
Migrates ValidateProject/validateService to accept modes.Recipe, updates HA checks, and changes RunEstimate/GeneratePreview/PrintEstimate to use Recipe.
ComposeUp deployment threading
src/pkg/cli/composeUp.go, src/cmd/cli/command/compose.go, src/pkg/cli/composeUp_test.go, src/pkg/cli/composeUp_dockerfile_test.go
Uses ComposeUpParams.Mode as Recipe, threads recipe through validation and compatibility checks, fetches recipe, and sets DeployRequest.Mode and DeployRequest.Recipe; updates tests.
BYOC provider deployment payloads
src/pkg/cli/client/byoc/aws/byoc.go, src/pkg/cli/client/byoc/azure/byoc.go, src/pkg/cli/client/byoc/do/byoc.go, src/pkg/cli/client/byoc/gcp/byoc.go
Adds Recipe field to marshaled defangv1.ProjectUpdate payloads sent to CD jobs.
Agent stack creation tools
src/pkg/agent/tools/create_aws_stack.go, src/pkg/agent/tools/create_azure_stack.go, src/pkg/agent/tools/create_gcp_stack.go
Use modes.ParseRecipe directly when building new stacks (removes per-tool parse error early returns).
Agent estimate interfaces and tests
src/pkg/agent/tools/default_tool_cli.go, src/pkg/agent/tools/interfaces.go, src/pkg/agent/tools/estimate.go, src/pkg/agent/tools/*_test.go
Switches RunEstimate/PrintEstimate signatures to accept modes.Recipe; updates mocks and tests; HandleEstimateTool parses deployment_mode via ParseRecipe.
Tests and small fixes
src/cmd/cli/command/stack.go, src/pkg/cli/tail_test.go, many *_test.go updates
Adds explicit nil return in prompt helper, scanner.Err() check in tail test, and updates numerous tests to use modes.Recipe* constants and adjusted expectations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • jordanstephens
  • edwardrf

"I'm a rabbit with a code-filled hat,
Recipes now guide the deploy-splat,
Modes retired, new enums in sight,
Stacks and CLI sing recipes right,
Hooray — a tidy deploy delight!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add deployment mode selection to parameter collection' directly and clearly summarizes the main change: adding deployment mode (recipe) selection to the parameter collection wizard flow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lio/pulumi-config

Comment @coderabbitai help to get the list of available commands and usage tips.

@lionello lionello requested a review from edwardrf June 2, 2026 15:27
@lionello lionello marked this pull request as ready for review June 2, 2026 23:05
@lionello lionello requested a review from jordanstephens as a code owner June 2, 2026 23:05
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/cmd/cli/command/estimate.go (1)

88-100: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the duplicate Azure case.

case pkg.AzureInEnv() != "" already exists at Lines 92-93, so the second occurrence at Lines 98-99 is unreachable dead code. Either drop it or correct it if a different provider was intended.

🧹 Proposed cleanup
 	case pkg.GcpInEnv() != "":
 		defaultOption = client.ProviderGCP.String()
-	case pkg.AzureInEnv() != "":
-		defaultOption = client.ProviderAzure.String()
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/cli/command/estimate.go` around lines 88 - 100, The switch in
estimate.go contains a duplicate Azure branch, making the later case
unreachable. Remove the repeated pkg.AzureInEnv() check from the default option
selection, or replace it with the intended provider case if another environment
was meant; verify the logic in the provider selection block around defaultOption
so each provider is handled only once.
src/pkg/agent/tools/create_aws_stack.go (1)

20-31: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate params.Mode instead of silently accepting unknown values
src/pkg/agent/tools/create_aws_stack.go uses modes.ParseRecipe(params.Mode) inline (no error handling). In src/pkg/modes/recipe.go, ParseRecipe(str string) Recipe has no error return; it uppercases the input, maps only a few legacy aliases, and otherwise falls through to return Recipe(upper) for unrecognized strings. This removes the prior “invalid mode” error behavior from modes.Parse. Restore explicit validation/error handling (or ensure unknown recipes are rejected downstream) before persisting/using the stack.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/agent/tools/create_aws_stack.go` around lines 20 - 31,
HandleCreateAWSStackTool currently calls modes.ParseRecipe(params.Mode) with no
validation; restore explicit validation by parsing the mode then checking it
against the known/allowed Recipe values (e.g., via a new or existing validator
like modes.IsValid/knownRecipes or by updating modes.ParseRecipe to return an
error). If the parsed Recipe is not one of the recognized recipes, return a
descriptive error from HandleCreateAWSStackTool instead of persisting the stack;
otherwise proceed to build newStack (referencing newStack and modes.ParseRecipe)
and call createAndLoadStack as before.
src/pkg/cli/common.go (1)

56-124: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Drop putDeploymentParams.Recipe in putDeploymentAndStack (common.go)

  • src/pkg/cli/common.go defines putDeploymentParams.Recipe, but putDeploymentAndStack never reads it and never sets Stack.Recipe / Deployment.Recipe in the PutStackRequest and PutDeploymentRequest.
  • src/pkg/cli/composeUp.go does populate Recipe: deployRequest.Recipe, and the protobufs (io.defang.v1.Stack / io.defang.v1.Deployment) include a recipe field (with mode marked deprecated), so wiring req.Recipe through would prevent losing recipe metadata in the stored deployment/stack.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/cli/common.go` around lines 56 - 124, putDeploymentAndStack currently
ignores putDeploymentParams.Recipe so recipe metadata is never saved; update the
function to pass req.Recipe into both the Stack and Deployment in the
PutStackRequest and PutDeploymentRequest (set Stack.Recipe = req.Recipe and
Deployment.Recipe = req.Recipe), ensuring the same recipe populated by
composeUp.go (deployRequest.Recipe) is persisted; locate these fields in
putDeploymentAndStack and add the Recipe assignments to the created
defangv1.Stack and defangv1.Deployment objects.
🧹 Nitpick comments (3)
src/cmd/cli/command/recipe.go (2)

76-93: 💤 Low value

Rename variable for consistency.

The variable is named recipeUnarchiveCmd but represents the activate command. Consider renaming to recipeActivateCmd for consistency with the function name and command semantics.

♻️ Proposed change
-	var recipeUnarchiveCmd = &cobra.Command{
+	var recipeActivateCmd = &cobra.Command{
 		Use:         "activate [RECIPE_NAME...]",
 		Aliases:     []string{"restore", "enable", "undelete", "unarchive"},
 		Annotations: authNeededAlways,
 		Args:        cobra.MinimumNArgs(1),
 		Short:       "Activates a recipe in the current workspace",
 		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx := cmd.Context()
 			var errs []error
 			for _, name := range args {
 				errs = append(errs, cli.RecipeActivate(ctx, global.Client, name, true))
 			}
 			return errors.Join(errs...)
 		},
 	}
-	return recipeUnarchiveCmd
+	return recipeActivateCmd
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/cli/command/recipe.go` around lines 76 - 93, The local variable
recipeUnarchiveCmd in makeRecipeActivateCmd should be renamed to
recipeActivateCmd to match the function/command semantics; update its
declaration and the final return statement to use recipeActivateCmd, and ensure
any internal references (the cobra.Command literal and its RunE closure) remain
unchanged so only the variable identifier is renamed.

58-73: 💤 Low value

Rename variable for consistency.

The variable is named recipeArchiveCmd but represents the deactivate command. Consider renaming to recipeDeactivateCmd for consistency with the function name and command semantics.

♻️ Proposed change
-	var recipeArchiveCmd = &cobra.Command{
+	var recipeDeactivateCmd = &cobra.Command{
 		Use:         "deactivate [RECIPE_NAME...]",
 		Aliases:     []string{"remove", "rm", "delete", "del", "disable", "archive"},
 		Annotations: authNeededAlways,
 		Args:        cobra.MinimumNArgs(1),
 		Short:       "Deactivates a recipe in the current workspace",
 		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx := cmd.Context()
 			var errs []error
 			for _, name := range args {
 				errs = append(errs, cli.RecipeActivate(ctx, global.Client, name, false))
 			}
 			return errors.Join(errs...)
 		},
 	}
-	return recipeArchiveCmd
+	return recipeDeactivateCmd
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/cli/command/recipe.go` around lines 58 - 73, The variable
recipeArchiveCmd is misnamed for the deactivate command; rename it to
recipeDeactivateCmd and update its declaration, any references (including the
return statement) to use recipeDeactivateCmd so the symbol matches the command
semantics and the function name; ensure the variable name change is applied
consistently within this function block (the cobra.Command variable and the
final return).
src/pkg/cli/composeUp.go (1)

138-140: 💤 Low value

Clarify the recipe override and simplify mode conversion.

After the compatibility check, if newMode is not ModeUnspecified, you override the user-provided recipe with a new one derived from the mode via modes.FromMode(newMode.Value()). This pattern has two concerns:

  1. Redundant conversion: newMode is already a modes.Mode, so calling .Value() to get defangv1.DeploymentMode and then passing it back to FromMode is a round-trip. Consider storing a direct Mode-to-Recipe mapping or checking if modes.FromMode(newMode) is valid (without the .Value() step).

  2. Potential information loss: If the original recipe carries any custom configuration beyond just the mode name, this override discards it. The subsequent GetRecipe call fetches backend config by name, but clarify in a comment whether user-provided recipe details (if any exist) are intentionally replaced by the compatibility-derived mode.

♻️ Optional: simplify or document the override

Document the intent:

 if newMode != modes.ModeUnspecified {
+    // Replace recipe with mode-based one to enforce compatibility;
+    // backend GetRecipe will supply full config.
     recipe = modes.FromMode(newMode.Value())
 }

Or refactor if a direct conversion exists:

-recipe = modes.FromMode(newMode.Value())
+recipe = modes.RecipeFromMode(newMode) // if such a helper exists
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/cli/composeUp.go` around lines 138 - 140, The current override
replaces the entire user-provided recipe by calling recipe =
modes.FromMode(newMode.Value()), which both does a redundant .Value() round-trip
and discards any custom recipe fields; change this to directly convert the
modes.Mode without .Value() (e.g., call modes.FromMode(newMode) or use a small
Mode->Recipe mapping) and when applying the result only override the recipe's
mode/name (not the whole recipe) so user-specified configuration is preserved;
also add a short comment near newMode, recipe, modes.FromMode and GetRecipe
clarifying that only the mode/name is being canonicalized for compatibility, not
wholesale replacement of user recipe data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cmd/cli/command/recipe.go`:
- Line 11: Rename the incorrectly named variable stackCmd to recipeCmd so the
command variable reflects its purpose; find the declaration "var stackCmd =
&cobra.Command{", rename the identifier to "recipeCmd" everywhere it is
referenced (including any uses in init/register functions) and ensure any
related help/usage text remains unchanged.

In `@src/pkg/cli/composeUp.go`:
- Around line 143-155: The deploy request mixes the local recipe.Mode() and the
backend-fetched rresp.Recipe, which can produce conflicting Mode values; after
calling fabric.GetRecipe (GetRecipe) check that rresp.Recipe is non-nil and that
rresp.Recipe.Mode equals recipe.Mode().Value(), and if they differ return an
error indicating the mismatch, or else set deployRequest.DeployRequest.Mode from
the fetched rresp.Recipe.Mode so both deployRequest.Mode and Recipe
(rresp.Recipe) use the same source of truth; update the code paths that build
the deployRequest (the DeployRequest struct and any early return) accordingly.

In `@src/pkg/cli/recipeShow.go`:
- Around line 13-19: resp.Recipe may be nil after fabric.GetRecipe; before
accessing resp.Recipe.PulumiConfig in recipeShow.go, add a nil-guard: check that
resp != nil && resp.Recipe != nil and handle the nil case (return a clear error
from the surrounding function or print a “recipe not found” message) instead of
dereferencing; update the code around the GetRecipe call and the subsequent
term.Println usage to avoid a panic and return the appropriate error when Recipe
is nil.

In `@src/pkg/modes/recipe.go`:
- Around line 22-39: The Set method currently accepts any string and ParseRecipe
returns Recipe(upper) for unknown inputs, letting typos become ModeUnspecified
downstream; update ParseRecipe to validate against known constants
(RecipeAffordable, RecipeBalanced, RecipeHighAvailability, etc.) and have
Recipe.Set call ParseRecipe and return a descriptive error when the input is
unrecognized (instead of nil), or alternatively provide a separate
ParseRecipeAllowFreeForm used only by the agent; ensure Recipe.Mode() still maps
properly but that CLI pflag.Value parsing and DEFANG_MODE reading fail fast on
invalid values.

In `@src/pkg/stacks/stacks.go`:
- Around line 72-80: modes.ParseRecipe currently uppercases and maps aliases but
returns Recipe(upper) for unknown inputs, allowing invalid modes to slip
through; update ParseRecipe to validate the uppercased value against known
recipes and return modes.RecipeUnspecified for any unrecognized string (while
preserving case-insensitive matching and legacy alias mapping), add a unit test
that calls modes.ParseRecipe with an invalid value and asserts
RecipeUnspecified, and ensure callers (e.g., the Parameters construction in
stacks.go where recipe is set) continue to treat RecipeUnspecified as the
sentinel for unspecified/invalid mode.

---

Outside diff comments:
In `@src/cmd/cli/command/estimate.go`:
- Around line 88-100: The switch in estimate.go contains a duplicate Azure
branch, making the later case unreachable. Remove the repeated pkg.AzureInEnv()
check from the default option selection, or replace it with the intended
provider case if another environment was meant; verify the logic in the provider
selection block around defaultOption so each provider is handled only once.

In `@src/pkg/agent/tools/create_aws_stack.go`:
- Around line 20-31: HandleCreateAWSStackTool currently calls
modes.ParseRecipe(params.Mode) with no validation; restore explicit validation
by parsing the mode then checking it against the known/allowed Recipe values
(e.g., via a new or existing validator like modes.IsValid/knownRecipes or by
updating modes.ParseRecipe to return an error). If the parsed Recipe is not one
of the recognized recipes, return a descriptive error from
HandleCreateAWSStackTool instead of persisting the stack; otherwise proceed to
build newStack (referencing newStack and modes.ParseRecipe) and call
createAndLoadStack as before.

In `@src/pkg/cli/common.go`:
- Around line 56-124: putDeploymentAndStack currently ignores
putDeploymentParams.Recipe so recipe metadata is never saved; update the
function to pass req.Recipe into both the Stack and Deployment in the
PutStackRequest and PutDeploymentRequest (set Stack.Recipe = req.Recipe and
Deployment.Recipe = req.Recipe), ensuring the same recipe populated by
composeUp.go (deployRequest.Recipe) is persisted; locate these fields in
putDeploymentAndStack and add the Recipe assignments to the created
defangv1.Stack and defangv1.Deployment objects.

---

Nitpick comments:
In `@src/cmd/cli/command/recipe.go`:
- Around line 76-93: The local variable recipeUnarchiveCmd in
makeRecipeActivateCmd should be renamed to recipeActivateCmd to match the
function/command semantics; update its declaration and the final return
statement to use recipeActivateCmd, and ensure any internal references (the
cobra.Command literal and its RunE closure) remain unchanged so only the
variable identifier is renamed.
- Around line 58-73: The variable recipeArchiveCmd is misnamed for the
deactivate command; rename it to recipeDeactivateCmd and update its declaration,
any references (including the return statement) to use recipeDeactivateCmd so
the symbol matches the command semantics and the function name; ensure the
variable name change is applied consistently within this function block (the
cobra.Command variable and the final return).

In `@src/pkg/cli/composeUp.go`:
- Around line 138-140: The current override replaces the entire user-provided
recipe by calling recipe = modes.FromMode(newMode.Value()), which both does a
redundant .Value() round-trip and discards any custom recipe fields; change this
to directly convert the modes.Mode without .Value() (e.g., call
modes.FromMode(newMode) or use a small Mode->Recipe mapping) and when applying
the result only override the recipe's mode/name (not the whole recipe) so
user-specified configuration is preserved; also add a short comment near
newMode, recipe, modes.FromMode and GetRecipe clarifying that only the mode/name
is being canonicalized for compatibility, not wholesale replacement of user
recipe data.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b363f9dc-d3d7-47e2-b66a-eeefa9d62564

📥 Commits

Reviewing files that changed from the base of the PR and between 3dfee36 and 10f9f4c.

📒 Files selected for processing (54)
  • src/cmd/cli/command/commands.go
  • src/cmd/cli/command/compose.go
  • src/cmd/cli/command/estimate.go
  • src/cmd/cli/command/estimate_test.go
  • src/cmd/cli/command/globals.go
  • src/cmd/cli/command/globals_test.go
  • src/cmd/cli/command/recipe.go
  • src/cmd/cli/command/stack.go
  • src/cmd/cli/command/stack_test.go
  • src/pkg/agent/tools/create_aws_stack.go
  • src/pkg/agent/tools/create_azure_stack.go
  • src/pkg/agent/tools/create_gcp_stack.go
  • src/pkg/agent/tools/current_stack_test.go
  • src/pkg/agent/tools/default_tool_cli.go
  • src/pkg/agent/tools/deploy.go
  • src/pkg/agent/tools/estimate.go
  • src/pkg/agent/tools/estimate_test.go
  • src/pkg/agent/tools/interfaces.go
  • src/pkg/agent/tools/select_stack_test.go
  • src/pkg/agent/tools/services_test.go
  • src/pkg/cli/client/byoc/aws/byoc.go
  • src/pkg/cli/client/byoc/azure/byoc.go
  • src/pkg/cli/client/byoc/do/byoc.go
  • src/pkg/cli/client/byoc/gcp/byoc.go
  • src/pkg/cli/client/client.go
  • src/pkg/cli/client/grpc.go
  • src/pkg/cli/client/mock.go
  • src/pkg/cli/client/provider.go
  • src/pkg/cli/common.go
  • src/pkg/cli/compose/validation.go
  • src/pkg/cli/compose/validation_test.go
  • src/pkg/cli/composeUp.go
  • src/pkg/cli/composeUp_dockerfile_test.go
  • src/pkg/cli/composeUp_test.go
  • src/pkg/cli/estimate.go
  • src/pkg/cli/preview_test.go
  • src/pkg/cli/recipeActivate.go
  • src/pkg/cli/recipeList.go
  • src/pkg/cli/recipeShow.go
  • src/pkg/cli/stacks.go
  • src/pkg/cli/stacks_test.go
  • src/pkg/cli/tail_test.go
  • src/pkg/modes/mode.go
  • src/pkg/modes/mode_test.go
  • src/pkg/modes/recipe.go
  • src/pkg/session/session.go
  • src/pkg/session/session_test.go
  • src/pkg/stacks/manager.go
  • src/pkg/stacks/manager_test.go
  • src/pkg/stacks/selector_test.go
  • src/pkg/stacks/stacks.go
  • src/pkg/stacks/stacks_test.go
  • src/pkg/stacks/wizard.go
  • src/pkg/stacks/wizard_test.go
💤 Files with no reviewable changes (1)
  • src/cmd/cli/command/stack.go

)

func makeRecipeCmd() *cobra.Command {
var stackCmd = &cobra.Command{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the variable name.

The variable is named stackCmd but should be recipeCmd to match its actual purpose.

📝 Proposed fix
-	var stackCmd = &cobra.Command{
+	var recipeCmd = &cobra.Command{
 		Use:     "recipe",
 		Aliases: []string{"recipes", "modes", "mode"},
 		Short:   "Manage workspace recipes (deployment modes)",
 	}
 	recipeListCmd := makeRecipeListCmd()
-	stackCmd.AddCommand(recipeListCmd)
+	recipeCmd.AddCommand(recipeListCmd)
 	recipeShowCmd := makeRecipeShowCmd()
-	stackCmd.AddCommand(recipeShowCmd)
+	recipeCmd.AddCommand(recipeShowCmd)
 	recipeDeactivateCmd := makeRecipeDeactivateCmd()
-	stackCmd.AddCommand(recipeDeactivateCmd)
+	recipeCmd.AddCommand(recipeDeactivateCmd)
 	recipeActivateCmd := makeRecipeActivateCmd()
-	stackCmd.AddCommand(recipeActivateCmd)
-	return stackCmd
+	recipeCmd.AddCommand(recipeActivateCmd)
+	return recipeCmd
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var stackCmd = &cobra.Command{
var recipeCmd = &cobra.Command{
Use: "recipe",
Aliases: []string{"recipes", "modes", "mode"},
Short: "Manage workspace recipes (deployment modes)",
}
recipeListCmd := makeRecipeListCmd()
recipeCmd.AddCommand(recipeListCmd)
recipeShowCmd := makeRecipeShowCmd()
recipeCmd.AddCommand(recipeShowCmd)
recipeDeactivateCmd := makeRecipeDeactivateCmd()
recipeCmd.AddCommand(recipeDeactivateCmd)
recipeActivateCmd := makeRecipeActivateCmd()
recipeCmd.AddCommand(recipeActivateCmd)
return recipeCmd
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/cli/command/recipe.go` at line 11, Rename the incorrectly named
variable stackCmd to recipeCmd so the command variable reflects its purpose;
find the declaration "var stackCmd = &cobra.Command{", rename the identifier to
"recipeCmd" everywhere it is referenced (including any uses in init/register
functions) and ensure any related help/usage text remains unchanged.

Comment thread src/pkg/cli/composeUp.go
Comment on lines +143 to +155
rresp, err := fabric.GetRecipe(ctx, &defangv1.GetRecipeRequest{Name: recipe.String()})
if err != nil {
return nil, project, fmt.Errorf("failed to get recipe for deployment mode %q: %w", recipe, err)
}

deployRequest := &client.DeployRequest{
DeployRequest: defangv1.DeployRequest{
Mode: mode.Value(),
Mode: recipe.Mode().Value(),
Project: project.Name,
Compose: composeYaml,
DelegateDomain: delegateDomain.Zone,
},
Recipe: rresp.Recipe,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Verify Mode and Recipe consistency after fetching from backend.

Lines 150 and 155 populate Mode from the local recipe.Mode().Value() and Recipe from the backend rresp.Recipe. If the backend recipe's mode differs from the local recipe's mode (due to misconfiguration, drift, or backend logic), the deploy request will carry inconsistent mode signals to the CD task.

Example scenario:

// Local recipe after compatibility check
recipe.Mode() == modes.ModeBalanced

// Backend returns a different mode
rresp.Recipe.Mode == defangv1.DeploymentMode_DEPLOYMENT_MODE_HIGH_AVAILABILITY

// Deploy request has conflicting fields:
deployRequest.Mode           = BALANCED          // from local
deployRequest.Recipe.Mode    = HIGH_AVAILABILITY // from backend

Recommendation: After fetching the recipe, validate consistency or use the fetched recipe as the source of truth for both fields:

rresp, err := fabric.GetRecipe(ctx, &defangv1.GetRecipeRequest{Name: recipe.String()})
if err != nil {
    return nil, project, fmt.Errorf("failed to get recipe for deployment mode %q: %w", recipe, err)
}
// Validate mode consistency
if rresp.Recipe != nil && rresp.Recipe.Mode != recipe.Mode().Value() {
    return nil, project, fmt.Errorf("backend recipe mode %s does not match expected mode %s", 
        modes.Mode(rresp.Recipe.Mode), recipe.Mode())
}

deployRequest := &client.DeployRequest{
    DeployRequest: defangv1.DeployRequest{
        Mode:           rresp.Recipe.Mode,  // use fetched recipe as source of truth
        ...
    },
    Recipe: rresp.Recipe,
}

Or validate and continue using local recipe, but confirm they match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/cli/composeUp.go` around lines 143 - 155, The deploy request mixes
the local recipe.Mode() and the backend-fetched rresp.Recipe, which can produce
conflicting Mode values; after calling fabric.GetRecipe (GetRecipe) check that
rresp.Recipe is non-nil and that rresp.Recipe.Mode equals recipe.Mode().Value(),
and if they differ return an error indicating the mismatch, or else set
deployRequest.DeployRequest.Mode from the fetched rresp.Recipe.Mode so both
deployRequest.Mode and Recipe (rresp.Recipe) use the same source of truth;
update the code paths that build the deployRequest (the DeployRequest struct and
any early return) accordingly.

Comment thread src/pkg/cli/recipeShow.go
Comment on lines +13 to +19
resp, err := fabric.GetRecipe(ctx, &defangv1.GetRecipeRequest{Name: recipeName})
if err != nil {
return fmt.Errorf("failed to get recipe: %w", err)
}

_, err = term.Println(resp.Recipe.PulumiConfig)
return err
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Possible nil-pointer dereference on resp.Recipe.

GetRecipe may return a successful response with a nil Recipe (e.g. an empty/not-found result). resp.Recipe.PulumiConfig would then panic. Guard against a nil Recipe before dereferencing.

🛡️ Proposed nil guard
 	resp, err := fabric.GetRecipe(ctx, &defangv1.GetRecipeRequest{Name: recipeName})
 	if err != nil {
 		return fmt.Errorf("failed to get recipe: %w", err)
 	}
+	if resp.Recipe == nil {
+		return fmt.Errorf("recipe %q not found", recipeName)
+	}
 
 	_, err = term.Println(resp.Recipe.PulumiConfig)
 	return err
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resp, err := fabric.GetRecipe(ctx, &defangv1.GetRecipeRequest{Name: recipeName})
if err != nil {
return fmt.Errorf("failed to get recipe: %w", err)
}
_, err = term.Println(resp.Recipe.PulumiConfig)
return err
resp, err := fabric.GetRecipe(ctx, &defangv1.GetRecipeRequest{Name: recipeName})
if err != nil {
return fmt.Errorf("failed to get recipe: %w", err)
}
if resp.Recipe == nil {
return fmt.Errorf("recipe %q not found", recipeName)
}
_, err = term.Println(resp.Recipe.PulumiConfig)
return err
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/cli/recipeShow.go` around lines 13 - 19, resp.Recipe may be nil after
fabric.GetRecipe; before accessing resp.Recipe.PulumiConfig in recipeShow.go,
add a nil-guard: check that resp != nil && resp.Recipe != nil and handle the nil
case (return a clear error from the surrounding function or print a “recipe not
found” message) instead of dereferencing; update the code around the GetRecipe
call and the subsequent term.Println usage to avoid a panic and return the
appropriate error when Recipe is nil.

Comment thread src/pkg/modes/recipe.go
Comment on lines +22 to +39
func (r *Recipe) Set(s string) error {
*r = ParseRecipe(s)
return nil
}

func ParseRecipe(str string) Recipe {
upper := strings.ToUpper(str)
// Handle legacy aliases
switch upper {
case "CHEAP", "DEVELOPMENT":
return RecipeAffordable
case "STAGING":
return RecipeBalanced
case "HA", "HIGH-AVAILABILITY", "PRODUCTION":
return RecipeHighAvailability
}
return Recipe(upper)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find flag bindings using Recipe as a pflag.Value and any callers of ParseRecipe/Set
rg -nP --type=go -C3 '\bVar\s*\(\s*&[A-Za-z0-9_.]*[Rr]ecipe\b' 
rg -nP --type=go -C2 '\bParseRecipe\s*\('
ast-grep --pattern 'func ($R *Recipe) Set($_ string) error { $$$ }'

Repository: DefangLabs/defang

Length of output: 2688


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect recipe.go around Set/ParseRecipe/Type/Mode
sed -n '1,120p' src/pkg/modes/recipe.go
echo "----"

# Find any pflag/cobra usage that mentions "recipe" type or calls Set/ParseRecipe for CLI flags
rg -n --type=go --glob '!**/*_test.go' '\b("recipe"|modes\.Recipe|ParseRecipe|DeploymentMode|DEFANG_MODE)\b' src cmd || true

# Find flag definitions that set "mode" / deployment mode
rg -n --type=go --glob '!**/*_test.go' '(pflag\.|cobra\.|Flags\(\)|PersistentFlags\(\)|VarP?\(|StringVarP?\(|BoolVarP?\()' src cmd || true

# Specifically look for Var(&..., ...) patterns that pass a Recipe or call Recipe.Set
rg -n --type=go --glob '!**/*_test.go' 'VarP?\s*\(\s*&.*[Rr]ecipe|Var\s*\(\s*&.*[Rr]ecipe|\.Set\(\s*[^,)]+' src cmd || true

Repository: DefangLabs/defang

Length of output: 47302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' src/pkg/cli/compose/validation.go
echo "----"
sed -n '1,120p' src/pkg/modes/mode.go
echo "----"
sed -n '180,260p' src/cmd/cli/command/compose.go
echo "----"
sed -n '1,140p' src/cmd/cli/command/estimate.go
echo "----"
sed -n '1,120p' src/cmd/cli/command/stack.go

Repository: DefangLabs/defang

Length of output: 17474


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Global.Stack.Mode type and env parsing
sed -n '1,240p' src/cmd/cli/command/globals.go
echo "----"
rg -n --type=go 'type .*Stack|Stack struct|var global' src/cmd/cli/command || true

echo "----"

# How flags are wired into global.Stack.Mode (mode flags)
rg -n --type=go 'Flags\(\)\.VarP\(&global\.Stack\.Mode,\s*\"mode\"' -S src/cmd/cli/command || true

echo "----"

# Where Recipe is converted to protobuf / used in requests
sed -n '1,220p' src/pkg/agent/tools/default_tool_cli.go
echo "----"
sed -n '1,120p' src/pkg/agent/tools/estimate.go
echo "----"
sed -n '1,120p' src/pkg/cli/estimate.go
echo "----"

sed -n '1,120p' src/pkg/agent/tools/create_aws_stack.go
echo "----"
sed -n '1,120p' src/pkg/agent/tools/create_gcp_stack.go
echo "----"
sed -n '1,120p' src/pkg/agent/tools/create_azure_stack.go

echo "----"

# Any explicit use of Recipe.Mode() (mapping to ModeUnspecified)
rg -n --type=go '\.Mode\(\)' src/pkg src/cmd | head -n 200
echo "----"

# Any handling of RecipeUnspecified after parsing
rg -n --type=go 'RecipeUnspecified' src | head -n 200

Repository: DefangLabs/defang

Length of output: 23674


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' src/pkg/stacks/manager.go
echo "----"
sed -n '1,240p' src/pkg/stacks/wizard.go
echo "----"
sed -n '1,120p' src/pkg/stacks/stacks.go
echo "----"
sed -n '1,120p' src/pkg/cli/stacks.go
echo "----"
sed -n '500,620p' src/cmd/cli/command/compose.go
echo "----"
sed -n '80,210p' src/cmd/cli/command/globals.go

Repository: DefangLabs/defang

Length of output: 25940


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where deployment mode is validated/checked in compose up flow
rg -n --type=go 'checkDeploymentMode|ModeUnspecified|RecipeUnspecified|DeploymentMode' src/pkg/cli/composeUp.go src/pkg/cli -S

# Show the relevant sections
sed -n '1,260p' src/pkg/cli/composeUp.go
echo "----"
sed -n '260,420p' src/pkg/cli/composeUp.go
echo "----"

# How stacks parameters are validated/used when uploading/creating
sed -n '1,140p' src/pkg/stacks/manager.go
echo "----"
sed -n '140,260p' src/pkg/stacks/manager.go
echo "----"

# How recipe/mode is used when composing stacks deployment requests
sed -n '1,120p' src/pkg/cli/stacks.go
echo "----"
rg -n --type=go 'RecipeUnspecified|ModeUnspecified|recipe\.Mode\(\)' src/pkg/cli/stacks.go src/pkg/cli/composeUp.go src/pkg/cli/preview.go -S || true

Repository: DefangLabs/defang

Length of output: 23423


Fix validation for unknown Recipe values in Recipe.Set/ParseRecipe.

Recipe.Set always returns nil, and ParseRecipe returns Recipe(upper) for any unrecognized string; when Recipe is used as a pflag.Value for the CLI --mode flags (e.g., compose/estimate/stack commands) and for DEFANG_MODE env parsing, typos won’t fail fast. Unknown values map to ModeUnspecified via Recipe.Mode(), which can silently alter behavior downstream (e.g., compose can fall back to the previous deployment mode; estimate/preview sends MODE_UNSPECIFIED). Add validation in Set (or keep a dedicated “allow free-form” parse path for agent use if that’s truly intended).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/modes/recipe.go` around lines 22 - 39, The Set method currently
accepts any string and ParseRecipe returns Recipe(upper) for unknown inputs,
letting typos become ModeUnspecified downstream; update ParseRecipe to validate
against known constants (RecipeAffordable, RecipeBalanced,
RecipeHighAvailability, etc.) and have Recipe.Set call ParseRecipe and return a
descriptive error when the input is unrecognized (instead of nil), or
alternatively provide a separate ParseRecipeAllowFreeForm used only by the
agent; ensure Recipe.Mode() still maps properly but that CLI pflag.Value parsing
and DEFANG_MODE reading fail fast on invalid values.

Comment thread src/pkg/stacks/stacks.go
Comment on lines +72 to +80
var recipe modes.Recipe
if val, ok := variables["DEFANG_MODE"]; ok {
err := mode.Set(val)
if err != nil {
return nil, fmt.Errorf("invalid DEFANG_MODE value: %w", err)
}
recipe = modes.ParseRecipe(val)
}
return &Parameters{
Variables: variables,
Provider: provider,
Region: region,
Mode: mode,
Mode: recipe,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify ParseRecipe handles invalid/case-mismatched input gracefully

# Check ParseRecipe implementation for case-insensitivity and invalid input handling
ast-grep --pattern $'func ParseRecipe($$$) $_ {
  $$$
}'

# Check for any error handling or validation in ParseRecipe
rg -nP -A10 'func ParseRecipe' --type=go

Repository: DefangLabs/defang

Length of output: 1313


Adjust/cover invalid DEFANG_MODE parsing behavior in modes.ParseRecipe

modes.ParseRecipe uppercases the input, maps legacy aliases, and otherwise returns Recipe(upper) with no validation/error path (so an unknown DEFANG_MODE value will propagate as a non-RecipeUnspecified mode rather than being coerced to the sentinel). Add an explicit policy (e.g., map unknowns to RecipeUnspecified) and a test for an invalid value, while keeping the case-insensitive behavior.
[src/pkg/modes/recipe.go:27-38], [src/pkg/stacks/stacks.go:72-80]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/stacks/stacks.go` around lines 72 - 80, modes.ParseRecipe currently
uppercases and maps aliases but returns Recipe(upper) for unknown inputs,
allowing invalid modes to slip through; update ParseRecipe to validate the
uppercased value against known recipes and return modes.RecipeUnspecified for
any unrecognized string (while preserving case-insensitive matching and legacy
alias mapping), add a unit test that calls modes.ParseRecipe with an invalid
value and asserts RecipeUnspecified, and ensure callers (e.g., the Parameters
construction in stacks.go where recipe is set) continue to treat
RecipeUnspecified as the sentinel for unspecified/invalid mode.

Comment on lines -21 to +25
mode, err := modes.Parse(params.Mode)
if err != nil {
return "Invalid mode provided", err
}

newStack := stacks.Parameters{
Name: params.Name,
Region: params.Region,
Provider: client.ProviderAWS,
Mode: mode,
Mode: modes.ParseRecipe(params.Mode),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a recipe doesnt exist server side, how does this fail now?

Comment thread src/pkg/cli/estimate.go

term.Debugf("Fixedup project: %s", string(composeData))

// TODO: this will need to read the recipe from Fabric first
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When will we do this?

Comment on lines +18 to +32
err = fabric.PutRecipe(ctx, &defangv1.PutRecipeRequest{
Recipe: &defangv1.Recipe{
Name: resp.Recipe.Name,
PulumiConfig: resp.Recipe.PulumiConfig,
Active: active,
},
})
if err == nil {
state := "active"
if !active {
state = "inactive"
}
term.Info(fmt.Sprintf("Recipe %q is now %s.", name, state))
}
return err
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raised my eyebrows at first glance. A few strange things about this code:

  1. the error handling is inverted from the idiomatic pattern
  2. state is inferred from the err, rather than from the record we update.

Can we avoid introducing a new state variable by instead modifying the recipe record in memory before sending it back and printing the state directly from the recipe?

Suggested change
err = fabric.PutRecipe(ctx, &defangv1.PutRecipeRequest{
Recipe: &defangv1.Recipe{
Name: resp.Recipe.Name,
PulumiConfig: resp.Recipe.PulumiConfig,
Active: active,
},
})
if err == nil {
state := "active"
if !active {
state = "inactive"
}
term.Info(fmt.Sprintf("Recipe %q is now %s.", name, state))
}
return err
recipe := resp.Recipe
recipe.Active = active
err = fabric.PutRecipe(ctx, &defangv1.PutRecipeRequest{
Recipe: recipe,
})
if err != nil {
return err
}
term.Info(fmt.Sprintf("Recipe %q is now %s.", name, recipe.Active))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants