Skip to content

Commit ecc673e

Browse files
committed
Handle explicit --config flag in determineBaseAppConfig
When --config is explicitly specified (e.g., by the deployer passing --config fly.api-server.toml), treat it as copy-config to prevent prompting and source scanning fallback. Add comprehensive tests for determineBaseAppConfig behavior and ensure LoadAppConfigIfPresent is called in runGenerate so custom config paths are available in context.
1 parent 89937cb commit ecc673e

File tree

3 files changed

+102
-2
lines changed

3 files changed

+102
-2
lines changed

internal/command/launch/plan_builder.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,9 +465,13 @@ func determineBaseAppConfig(ctx context.Context) (*appconfig.Config, bool, error
465465

466466
// if --attach is specified, we should return the config as the base config
467467
attach := flag.GetBool(ctx, "attach")
468-
copyConfig := flag.GetBool(ctx, "copy-config") || attach
468+
// An explicit --config flag means the caller deliberately chose the file
469+
// (e.g. the deployer passing --config fly.api-server.toml). Treat it as
470+
// copy-config so we never prompt and never fall back to source scanning.
471+
explicitConfig := flag.IsSpecified(ctx, "config")
472+
copyConfig := flag.GetBool(ctx, "copy-config") || attach || explicitConfig
469473

470-
if !flag.IsSpecified(ctx, "copy-config") && !attach && !flag.GetYes(ctx) {
474+
if !flag.IsSpecified(ctx, "copy-config") && !attach && !explicitConfig && !flag.GetYes(ctx) {
471475
var err error
472476
copyConfig, err = prompt.Confirm(ctx, colorize.Yellow("Would you like to use this fly.toml configuration for this app?"))
473477
fmt.Fprintln(io.Out)

internal/command/launch/plan_builder_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import (
88
"github.com/stretchr/testify/assert"
99
"github.com/stretchr/testify/require"
1010
fly "github.com/superfly/fly-go"
11+
"github.com/superfly/flyctl/internal/appconfig"
1112
"github.com/superfly/flyctl/internal/flag/flagctx"
1213
"github.com/superfly/flyctl/internal/flyutil"
1314
"github.com/superfly/flyctl/internal/mock"
15+
"github.com/superfly/flyctl/iostreams"
1416
)
1517

1618
func newDetermineOrgCtx(t *testing.T, orgFlag string) context.Context {
@@ -112,3 +114,80 @@ func TestDetermineOrg(t *testing.T) {
112114
assert.Equal(t, "personal", org.Slug)
113115
})
114116
}
117+
118+
// newDetermineBaseAppConfigCtx builds a context wired with the flags that
119+
// determineBaseAppConfig reads. Pass configPath="" to leave --config unset.
120+
func newDetermineBaseAppConfigCtx(t *testing.T, copyConfigFlag, explicitConfigPath bool) context.Context {
121+
t.Helper()
122+
123+
ctx := context.Background()
124+
ctx = iostreams.NewContext(ctx, iostreams.System())
125+
126+
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
127+
flagSet.String("config", "", "")
128+
flagSet.Bool("copy-config", false, "")
129+
flagSet.Bool("attach", false, "")
130+
flagSet.Bool("yes", false, "")
131+
132+
if copyConfigFlag {
133+
require.NoError(t, flagSet.Set("copy-config", "true"))
134+
}
135+
if explicitConfigPath {
136+
require.NoError(t, flagSet.Set("config", "fly.custom.toml"))
137+
}
138+
139+
return flagctx.NewContext(ctx, flagSet)
140+
}
141+
142+
func TestDetermineBaseAppConfig(t *testing.T) {
143+
// existingCfg simulates what LoadAppConfigIfPresent puts in context when
144+
// the customer has a custom fly.toml with a non-default dockerfile.
145+
existingCfg := appconfig.NewConfig()
146+
existingCfg.Build = &appconfig.Build{
147+
Dockerfile: "docker.ui-server.dockerfile",
148+
}
149+
150+
t.Run("no flags and no existing config returns blank config", func(t *testing.T) {
151+
ctx := newDetermineBaseAppConfigCtx(t, false, false)
152+
// No config in context — simulates no fly.toml present.
153+
154+
cfg, copied, err := determineBaseAppConfig(ctx)
155+
require.NoError(t, err)
156+
assert.False(t, copied)
157+
assert.Nil(t, cfg.Build)
158+
})
159+
160+
t.Run("--copy-config adopts existing config without prompting", func(t *testing.T) {
161+
ctx := newDetermineBaseAppConfigCtx(t, true, false)
162+
ctx = appconfig.WithConfig(ctx, existingCfg)
163+
164+
cfg, copied, err := determineBaseAppConfig(ctx)
165+
require.NoError(t, err)
166+
assert.True(t, copied)
167+
assert.Equal(t, "docker.ui-server.dockerfile", cfg.Build.Dockerfile)
168+
})
169+
170+
t.Run("explicit --config adopts existing config without prompting", func(t *testing.T) {
171+
// This is the deployer scenario: --config fly.custom.toml is passed but
172+
// --copy-config is not. The explicit path signals intent, so we must
173+
// not fall through to source scanning with an empty config.
174+
ctx := newDetermineBaseAppConfigCtx(t, false, true)
175+
ctx = appconfig.WithConfig(ctx, existingCfg)
176+
177+
cfg, copied, err := determineBaseAppConfig(ctx)
178+
require.NoError(t, err)
179+
assert.True(t, copied)
180+
assert.Equal(t, "docker.ui-server.dockerfile", cfg.Build.Dockerfile)
181+
})
182+
183+
t.Run("no flags in non-interactive mode returns error", func(t *testing.T) {
184+
ctx := newDetermineBaseAppConfigCtx(t, false, false)
185+
ctx = appconfig.WithConfig(ctx, existingCfg)
186+
// Non-interactive iostreams → prompt.Confirm returns ErrNonInteractive.
187+
ios, _, _, _ := iostreams.Test()
188+
ctx = iostreams.NewContext(ctx, ios)
189+
190+
_, _, err := determineBaseAppConfig(ctx)
191+
assert.Error(t, err)
192+
})
193+
}

internal/command/launch/plan_commands.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ func newGenerate() *cobra.Command {
184184
flag.Region(),
185185
flag.Org(),
186186
flag.AppConfig(),
187+
flag.Yes(),
188+
flag.Bool{
189+
Name: "copy-config",
190+
Description: "Use the configuration file if present without prompting",
191+
Default: false,
192+
},
187193
flag.Bool{
188194
Name: "no-deploy",
189195
Description: "Don't deploy the app",
@@ -269,5 +275,16 @@ func runTigris(ctx context.Context) error {
269275
func runGenerate(ctx context.Context) error {
270276
flag.SetString(ctx, "from-manifest", flag.FirstArg(ctx))
271277

278+
// LoadAppConfigIfPresent is registered on the parent "plan" command, but
279+
// because that command has no Run func cobra never executes its RunE and the
280+
// preparer is never called. Load the config here explicitly so that a custom
281+
// path supplied via --config (e.g. fly.api-server.toml) is in context before
282+
// buildManifest → determineBaseAppConfig reads it.
283+
var err error
284+
ctx, err = command.LoadAppConfigIfPresent(ctx)
285+
if err != nil {
286+
return err
287+
}
288+
272289
return RunPlan(ctx, "generate")
273290
}

0 commit comments

Comments
 (0)