From 899d7460c341c85776f82d55e16657b6641dc72e Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Sat, 17 Jan 2026 12:18:28 -0700 Subject: [PATCH 1/3] docs: minor docs + comment improvements --- buildkit/frontend.go | 1 + core/plan/dockerignore.go | 5 ++++- .../content/docs/guides/running-railpack-in-production.mdx | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/buildkit/frontend.go b/buildkit/frontend.go index 40d0a082..3111297a 100644 --- a/buildkit/frontend.go +++ b/buildkit/frontend.go @@ -1,3 +1,4 @@ +// for platforms: used by `ghcr.io/railwayapp/railpack-frontend` to accept railpack plans via BuildKit package buildkit import ( diff --git a/core/plan/dockerignore.go b/core/plan/dockerignore.go index f5c31b5c..20fc3866 100644 --- a/core/plan/dockerignore.go +++ b/core/plan/dockerignore.go @@ -3,8 +3,11 @@ package plan import ( "strings" - "github.com/moby/patternmatcher/ignorefile" "github.com/railwayapp/railpack/core/app" + + // this is the native dockerignore parser used by buildkit + // https://github.com/moby/buildkit/blob/master/frontend/dockerfile/dockerignore/dockerignore_deprecated.go + "github.com/moby/patternmatcher/ignorefile" ) // checks if a .dockerignore file exists in the app directory and parses it diff --git a/docs/src/content/docs/guides/running-railpack-in-production.mdx b/docs/src/content/docs/guides/running-railpack-in-production.mdx index b6866f30..31f86512 100644 --- a/docs/src/content/docs/guides/running-railpack-in-production.mdx +++ b/docs/src/content/docs/guides/running-railpack-in-production.mdx @@ -3,8 +3,9 @@ title: Running Railpack in Production description: Learn how to run Railpack in production as a platform --- -This guide will walk you through running Railpack in production as a platform -(like Railway). +This guide walks through running Railpack in production as a platform +(like Railway). This guide is not intended for end users using Railpack to +containerize their applications. ## CLI vs Frontend From cce01249846f6eba9420153ed2b44b5df5bb0c53 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Sat, 17 Jan 2026 15:38:56 -0700 Subject: [PATCH 2/3] refactor: simplify dockerignore parsing --- core/generate/context.go | 24 +++++++++--------- core/plan/dockerignore.go | 52 ++++++++++++++------------------------- 2 files changed, 30 insertions(+), 46 deletions(-) diff --git a/core/generate/context.go b/core/generate/context.go index 20f63064..ae346dd5 100644 --- a/core/generate/context.go +++ b/core/generate/context.go @@ -70,15 +70,16 @@ func NewGenerateContext(app *a.App, env *a.Environment, config *config.Config, l return nil, err } - dockerignoreCtx := plan.NewDockerignoreContext(app) - excludes, includes, err := dockerignoreCtx.ParseWithLogging(logger) + dockerignoreCtx, err := plan.NewDockerignoreContext(app) if err != nil { return nil, fmt.Errorf("failed to parse .dockerignore: %w", err) } - - if excludes != nil || includes != nil { - log.Debugf("Dockerignore exclude patterns: %v", excludes) - log.Debugf("Dockerignore include patterns: %v", includes) + + if dockerignoreCtx.HasFile { + logger.LogInfo("Found .dockerignore file, applying filters") + + log.Debugf("Dockerignore exclude patterns: %v", dockerignoreCtx.Excludes) + log.Debugf("Dockerignore include patterns: %v", dockerignoreCtx.Includes) } ctx := &GenerateContext{ @@ -97,7 +98,7 @@ func NewGenerateContext(app *a.App, env *a.Environment, config *config.Config, l ctx.applyPackagesFromConfig() - if excludes != nil || includes != nil { + if dockerignoreCtx.HasFile { ctx.Metadata.SetBool("dockerIgnore", true) } @@ -268,12 +269,11 @@ func (c *GenerateContext) applyConfig() { func (c *GenerateContext) NewLocalLayer() plan.Layer { layer := plan.NewLocalLayer() - excludes, includes, _ := c.dockerignoreCtx.Parse() - if len(includes) > 0 { - layer.Filter.Include = utils.RemoveDuplicates(append(layer.Filter.Include, includes...)) + if len(c.dockerignoreCtx.Includes) > 0 { + layer.Filter.Include = append(layer.Filter.Include, c.dockerignoreCtx.Includes...) } - if len(excludes) > 0 { - layer.Filter.Exclude = utils.RemoveDuplicates(append(layer.Filter.Exclude, excludes...)) + if len(c.dockerignoreCtx.Excludes) > 0 { + layer.Filter.Exclude = append(layer.Filter.Exclude, c.dockerignoreCtx.Excludes...) } return layer diff --git a/core/plan/dockerignore.go b/core/plan/dockerignore.go index 20fc3866..f73982d5 100644 --- a/core/plan/dockerignore.go +++ b/core/plan/dockerignore.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/railwayapp/railpack/core/app" + "github.com/railwayapp/railpack/internal/utils" // this is the native dockerignore parser used by buildkit // https://github.com/moby/buildkit/blob/master/frontend/dockerfile/dockerignore/dockerignore_deprecated.go @@ -56,43 +57,26 @@ func separatePatterns(patterns []string) (excludes []string, includes []string) } type DockerignoreContext struct { - parsed bool - excludes []string - includes []string - app *app.App + Excludes []string + Includes []string + HasFile bool } -func NewDockerignoreContext(app *app.App) *DockerignoreContext { - return &DockerignoreContext{ - app: app, - } -} - -// Parse parses the .dockerignore file and caches the results -func (d *DockerignoreContext) Parse() ([]string, []string, error) { - if !d.parsed { - excludes, includes, err := CheckAndParseDockerignore(d.app) - if err != nil { - return nil, nil, err - } - - d.excludes = excludes - d.includes = includes - d.parsed = true - } - - return d.excludes, d.includes, nil -} - -func (d *DockerignoreContext) ParseWithLogging(logger interface{ LogInfo(string, ...interface{}) }) ([]string, []string, error) { - excludes, includes, err := d.Parse() +func NewDockerignoreContext(app *app.App) (*DockerignoreContext, error) { + hasFile := app.HasFile(".dockerignore") + excludes, includes, err := CheckAndParseDockerignore(app) if err != nil { - return nil, nil, err + return nil, err } - - if excludes != nil || includes != nil { - logger.LogInfo("Found .dockerignore file, applying filters") + if excludes != nil { + excludes = utils.RemoveDuplicates(excludes) } - - return excludes, includes, nil + if includes != nil { + includes = utils.RemoveDuplicates(includes) + } + return &DockerignoreContext{ + Excludes: excludes, + Includes: includes, + HasFile: hasFile, + }, nil } From 7b25997971e5324ae9aec887464d0ff0cfc1045f Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Sat, 17 Jan 2026 15:40:39 -0700 Subject: [PATCH 3/3] test: update tests to work with new dockerignore structure --- core/generate/context_test.go | 5 +- core/plan/dockerignore_test.go | 130 +++++++++++++++++---------------- 2 files changed, 70 insertions(+), 65 deletions(-) diff --git a/core/generate/context_test.go b/core/generate/context_test.go index c6de62c8..8ddaf194 100644 --- a/core/generate/context_test.go +++ b/core/generate/context_test.go @@ -154,9 +154,8 @@ func TestGenerateContextDockerignore(t *testing.T) { require.NotNil(t, ctx.dockerignoreCtx) // Verify parsing works with no file present - excludes, includes, _ := ctx.dockerignoreCtx.Parse() - require.Nil(t, excludes) - require.Nil(t, includes) + require.Nil(t, ctx.dockerignoreCtx.Excludes) + require.Nil(t, ctx.dockerignoreCtx.Includes) }) t.Run("context creation fails with invalid dockerignore", func(t *testing.T) { diff --git a/core/plan/dockerignore_test.go b/core/plan/dockerignore_test.go index 596ae7fe..a7fb278e 100644 --- a/core/plan/dockerignore_test.go +++ b/core/plan/dockerignore_test.go @@ -137,55 +137,26 @@ func TestDockerignoreContext(t *testing.T) { testApp, err := app.NewApp(tempDir) require.NoError(t, err) - ctx := NewDockerignoreContext(testApp) - require.NotNil(t, ctx) - require.Equal(t, testApp, ctx.app) - require.False(t, ctx.parsed) - require.Nil(t, ctx.excludes) - require.Nil(t, ctx.includes) - }) - - t.Run("parse caching", func(t *testing.T) { - examplePath := filepath.Join("..", "..", "examples", "dockerignore") - testApp, err := app.NewApp(examplePath) + ctx, err := NewDockerignoreContext(testApp) require.NoError(t, err) - - ctx := NewDockerignoreContext(testApp) - - // First parse - excludes1, includes1, err1 := ctx.Parse() - require.NoError(t, err1) - require.True(t, ctx.parsed) - - // Second parse should return cached results - excludes2, includes2, err2 := ctx.Parse() - require.NoError(t, err2) - require.Equal(t, excludes1, excludes2) - require.Equal(t, includes1, includes2) + require.NotNil(t, ctx) + require.False(t, ctx.HasFile) + require.Nil(t, ctx.Excludes) + require.Nil(t, ctx.Includes) }) - t.Run("parse with logging", func(t *testing.T) { + t.Run("context with dockerignore file", func(t *testing.T) { examplePath := filepath.Join("..", "..", "examples", "dockerignore") testApp, err := app.NewApp(examplePath) require.NoError(t, err) - ctx := NewDockerignoreContext(testApp) - - // Mock logger that captures calls - logCalls := []string{} - mockLogger := &mockLogger{logFunc: func(format string, args ...interface{}) { - logCalls = append(logCalls, format) - }} - - excludes, includes, err := ctx.ParseWithLogging(mockLogger) + ctx, err := NewDockerignoreContext(testApp) require.NoError(t, err) - require.NotNil(t, excludes) - require.NotNil(t, includes) - require.Contains(t, includes, "negation_test/should_exist.txt") - require.Contains(t, includes, "negation_test/existing_folder") - - // Should have logged that dockerignore was found - require.Contains(t, logCalls, "Found .dockerignore file, applying filters") + require.True(t, ctx.HasFile) + require.NotNil(t, ctx.Excludes) + require.NotNil(t, ctx.Includes) + require.Contains(t, ctx.Includes, "negation_test/should_exist.txt") + require.Contains(t, ctx.Includes, "negation_test/existing_folder") }) t.Run("parse nonexistent file", func(t *testing.T) { @@ -196,13 +167,11 @@ func TestDockerignoreContext(t *testing.T) { testApp, err := app.NewApp(tempDir) require.NoError(t, err) - ctx := NewDockerignoreContext(testApp) - - excludes, includes, err := ctx.Parse() + ctx, err := NewDockerignoreContext(testApp) require.NoError(t, err) - require.Nil(t, excludes) - require.Nil(t, includes) - require.True(t, ctx.parsed) // Should still mark as parsed + require.False(t, ctx.HasFile) + require.Nil(t, ctx.Excludes) + require.Nil(t, ctx.Includes) }) t.Run("parse error handling", func(t *testing.T) { @@ -223,25 +192,62 @@ func TestDockerignoreContext(t *testing.T) { testApp, err := app.NewApp(tempDir) require.NoError(t, err) - ctx := NewDockerignoreContext(testApp) - excludes, includes, err := ctx.Parse() - + ctx, err := NewDockerignoreContext(testApp) require.Error(t, err) - require.Nil(t, excludes) - require.Nil(t, includes) - require.False(t, ctx.parsed) // Should not mark as parsed on error + require.Nil(t, ctx) }) } -// Mock logger for testing -type mockLogger struct { - logFunc func(string, ...interface{}) -} +func TestDockerignoreDuplicatePatterns(t *testing.T) { + t.Run("duplicate patterns removed", func(t *testing.T) { + tempDir, err := os.MkdirTemp("", "dockerignore-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // Create test files + err = os.WriteFile(filepath.Join(tempDir, "keep.txt"), []byte("exists"), 0644) + require.NoError(t, err) + + // Create .dockerignore with duplicate patterns + dockerignoreContent := `*.log +*.log +node_modules +!keep.txt +!keep.txt +` + err = os.WriteFile(filepath.Join(tempDir, ".dockerignore"), []byte(dockerignoreContent), 0644) + require.NoError(t, err) -func (m *mockLogger) LogInfo(format string, args ...interface{}) { - if m.logFunc != nil { - m.logFunc(format, args...) - } + testApp, err := app.NewApp(tempDir) + require.NoError(t, err) + + ctx, err := NewDockerignoreContext(testApp) + require.NoError(t, err) + + // Count occurrences of each pattern + logCount := 0 + nodeModulesCount := 0 + for _, pattern := range ctx.Excludes { + if pattern == "*.log" { + logCount++ + } + if pattern == "node_modules" { + nodeModulesCount++ + } + } + + keepCount := 0 + for _, pattern := range ctx.Includes { + if pattern == "keep.txt" { + keepCount++ + } + } + + // Verify no duplicates exist + require.LessOrEqual(t, logCount, 1, "*.log pattern should appear at most once") + require.LessOrEqual(t, nodeModulesCount, 1, "node_modules pattern should appear at most once") + require.LessOrEqual(t, keepCount, 1, "keep.txt pattern should appear at most once") + }) } func TestCheckAndParseDockerignoreWithNegation(t *testing.T) { @@ -259,7 +265,7 @@ func TestCheckAndParseDockerignoreWithNegation(t *testing.T) { require.NoError(t, err) // Create .dockerignore with mixed negation cases - dockerignoreContent := ` + dockerignoreContent := ` negation_test/* !negation_test/should_exist.txt !negation_test/should_not_exist.txt