diff --git a/core/generate/context.go b/core/generate/context.go index 3b66ee49f..94727ba2c 100644 --- a/core/generate/context.go +++ b/core/generate/context.go @@ -244,11 +244,14 @@ func (c *GenerateContext) applyConfig() { maps.Copy(commandStepBuilder.Assets, configStep.Assets) // Convert the deploy outputs into layers that will be added to the deploy. - // Skip if the path is already covered by existing inputs from this step - // (e.g. provider already added "." so we don't duplicate it from --build-cmd). - outputFilters := []plan.Filter{plan.NewIncludeFilter([]string{"."})} + // Skip default outputs if the user explicitly provided deploy.inputs + // (they control deploy composition directly), or if the path is already + // covered by existing inputs from this step. + var outputFilters []plan.Filter if configStep.DeployOutputs != nil { outputFilters = configStep.DeployOutputs + } else if c.Config.Deploy == nil || c.Config.Deploy.Inputs == nil { + outputFilters = []plan.Filter{plan.NewIncludeFilter([]string{"."})} } for _, filter := range outputFilters { alreadyCovered := false diff --git a/core/generate/context_test.go b/core/generate/context_test.go index 8ddaf1943..345ad61bb 100644 --- a/core/generate/context_test.go +++ b/core/generate/context_test.go @@ -11,6 +11,7 @@ import ( "github.com/railwayapp/railpack/core/config" "github.com/railwayapp/railpack/core/logger" "github.com/railwayapp/railpack/core/plan" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -187,3 +188,132 @@ func TestGenerateContextDockerignore(t *testing.T) { require.Nil(t, ctx) }) } + +func TestGenerateContextDeployInputs(t *testing.T) { + t.Run("deploy inputs with specific include prevents default output", func(t *testing.T) { + ctx := CreateTestContext(t, "../../examples/node-npm") + provider := &TestProvider{} + require.NoError(t, provider.Plan(ctx)) + + configJSON := `{ + "steps": { + "install": { + "commands": ["echo installing"] + }, + "build": { + "commands": ["echo building"] + } + }, + "deploy": { + "startCommand": "echo hello", + "inputs": [{"step": "build", "include": ["dist"]}] + } + }` + + var cfg config.Config + require.NoError(t, json.Unmarshal([]byte(configJSON), &cfg)) + ctx.Config = &cfg + + buildPlan, _, err := ctx.Generate() + require.NoError(t, err) + + for _, input := range buildPlan.Deploy.Inputs { + if input.Step == "build" { + for _, inc := range input.Include { + assert.NotEqual(t, ".", inc, + "deploy inputs should not contain default '.' when user specified deploy.inputs") + } + } + assert.NotEqual(t, "install", input.Step, + "install step should not have a default deploy output when user specified deploy.inputs") + } + + found := false + for _, input := range buildPlan.Deploy.Inputs { + if input.Step == "build" && len(input.Include) == 1 && input.Include[0] == "dist" { + found = true + break + } + } + require.True(t, found, "user's deploy input with include 'dist' should be present") + }) + + t.Run("no deploy inputs preserves default behavior", func(t *testing.T) { + ctx := CreateTestContext(t, "../../examples/node-npm") + provider := &TestProvider{} + require.NoError(t, provider.Plan(ctx)) + + configJSON := `{ + "steps": { + "build": { + "commands": ["echo building"] + } + }, + "deploy": { + "startCommand": "echo hello" + } + }` + + var cfg config.Config + require.NoError(t, json.Unmarshal([]byte(configJSON), &cfg)) + ctx.Config = &cfg + + buildPlan, _, err := ctx.Generate() + require.NoError(t, err) + + found := false + for _, input := range buildPlan.Deploy.Inputs { + if input.Step == "build" && len(input.Include) == 1 && input.Include[0] == "." { + found = true + break + } + } + require.True(t, found, "default '.' deploy output should be present when no deploy.inputs specified") + }) + + t.Run("deploy inputs with deployOutputs on step", func(t *testing.T) { + ctx := CreateTestContext(t, "../../examples/node-npm") + provider := &TestProvider{} + require.NoError(t, provider.Plan(ctx)) + + configJSON := `{ + "steps": { + "build": { + "commands": ["echo building"], + "deployOutputs": [{"include": ["dist"]}] + } + }, + "deploy": { + "startCommand": "echo hello", + "inputs": [{"step": "build", "include": ["other"]}] + } + }` + + var cfg config.Config + require.NoError(t, json.Unmarshal([]byte(configJSON), &cfg)) + ctx.Config = &cfg + + buildPlan, _, err := ctx.Generate() + require.NoError(t, err) + + // The user's deploy.inputs layer should be present + foundUserInput := false + for _, input := range buildPlan.Deploy.Inputs { + if input.Step == "build" && len(input.Include) == 1 && input.Include[0] == "other" { + foundUserInput = true + break + } + } + require.True(t, foundUserInput, "user's deploy input should be present") + + // The step's deployOutputs should also be present + foundDeployOutput := false + for _, input := range buildPlan.Deploy.Inputs { + if input.Step == "build" && len(input.Include) == 1 && input.Include[0] == "dist" { + foundDeployOutput = true + break + } + } + require.True(t, foundDeployOutput, "step's deployOutputs should still be honored") + }) +}