Files
Michael BiancoandGitHub 392d3fef96 feat: respect dockerignore on initial source load (#373)
* docs: add claude ref to agents

* refactor: simplify DockerignoreContext passing and make it required

* refactor: another take at fixing the initial docker ignore load stuff

* style: whitespace formatting tweaks

I'm not sure how it got into this state :/

* fix: apply dockerignore rules when copying local app context, not later in the build process

* fix: merge dockerignore excludes with top-level railpack.json excludes/includes

* test: top-level railpack.json exclude/include

* docs: update excluding files docs

* test: add negation to an existing dockerignore to expand test coverage

* fix: hack to determine if I'm understanding the llb exclude functionality right

* refactor: remove Includes and instead use negation operators and Excludes

* test: respect dockerignore on initial source load

- Apply exclude and include filters during the initial buildkit context load to improve local build performance
- Support top-level exclude and include configurations in railpack.json

Generated-by: aiautocommit

* feat: respect dockerignore on initial source load

- Load local source layers using the plan package directly to ensure .dockerignore patterns are respected at the buildkit level.
- Prevent unnecessary loading of ignored files like node_modules and virtual environments during local builds.

Generated-by: aiautocommit

* style: formatting updates

* docs(config): update documentation for dockerignore and exclude pattern handling

Generated-by: aiautocommit

* feat(plan): respect dockerignore on initial source load

Generated-by: aiautocommit

* refactor(core): access include rules directly from layer struct

Generated-by: aiautocommit

* feat(buildkit): respect exclude patterns on initial source load

- Apply exclude patterns at the buildkit level during local context loading to prevent transferring unnecessary large directories like node_modules or virtual environments.

Generated-by: aiautocommit

* feat: respect dockerignore on initial source load

Generated-by: aiautocommit

* feat(core): respect dockerignore on initial source load

- Expose the parsed dockerignore context via the build generate context
- Ensure build results preserve the correct dockerignore metadata and excludes on initial load

Generated-by: aiautocommit

* style(docs): fix trailing whitespace in CLAUDE.md

Generated-by: aiautocommit

* feat(core): respect dockerignore on initial source load

- Apply exclude and include filters during buildkit context setup to avoid loading large ignored directories locally.
- Support top-level exclude and include configuration in railpack.json.

Generated-by: aiautocommit

* test(plan): assert all expected empty dockerignore examples are found

Generated-by: aiautocommit

* docs(config): fix typo in excluding-files guide

Generated-by: aiautocommit

* feat(dockerignore): respect dockerignore on initial source load

- Apply exclude and include filters during Buildkit context establishment to drastically improve local build performance.
- Support top-level `exclude` and `include` rules directly in `railpack.json`.

Generated-by: aiautocommit

* docs(config): clarify directory matching behavior in excluding-files guide

Generated-by: aiautocommit

* refactor(core): remove dead dockerignore context fields

- Remove unused dockerignore context properties from build plans and options
- Clean up obsolete metadata checks in core tests

Generated-by: aiautocommit

* feat: add top-level exclude option to railpack config

Generated-by: aiautocommit
2026-07-31 07:41:35 -06:00

380 lines
7.9 KiB
Go

package config
import (
"encoding/json"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)
func TestEmptyConfig(t *testing.T) {
config := EmptyConfig()
require.NotNil(t, config)
require.Empty(t, config.Caches)
require.Empty(t, config.Packages)
require.Empty(t, config.BuildAptPackages)
require.Empty(t, config.Steps)
require.Nil(t, config.Provider)
require.NotNil(t, config.Deploy)
require.Nil(t, config.Deploy.Inputs)
require.Nil(t, config.Deploy.Paths)
}
func TestMergeConfigSmall(t *testing.T) {
config1JSON := `{
"baseImage": "ubuntu:20.04",
"packages": {
"python": "latest",
"node": "22"
},
"aptPackages": ["git"],
"steps": {
"install": {
"dependsOn": ["packages"],
"commands": [
"echo first"
],
"variables": {
"HELLO": "world"
},
"caches": ["pip"]
}
},
"deploy": {
"variables": {
"SHARED": "one",
"HELLO": "world"
}
}
}`
config2JSON := `{
"baseImage": "secondd",
"packages": {
"node": "23",
"bun": "latest"
},
"steps": {
"install": {
"variables": {
"another": "boop"
}
}
},
"deploy": {
"variables": {
"SHARED": "two",
"FOO": "bar"
}
}
}`
expectedJSON := `{
"baseImage": "secondd",
"packages": {
"python": "latest",
"node": "23",
"bun": "latest"
},
"aptPackages": ["git"],
"steps": {
"install": {
"dependsOn": ["packages"],
"commands": [
"echo first"
],
"variables": {
"HELLO": "world",
"another": "boop"
},
"caches": ["pip"]
}
},
"deploy": {
"variables": {
"HELLO": "world",
"SHARED": "two",
"FOO": "bar"
}
},
"caches": {}
}`
var config1, config2, expected Config
require.NoError(t, json.Unmarshal([]byte(config1JSON), &config1))
require.NoError(t, json.Unmarshal([]byte(config2JSON), &config2))
require.NoError(t, json.Unmarshal([]byte(expectedJSON), &expected))
result := Merge(&config1, &config2)
if diff := cmp.Diff(expected, *result); diff != "" {
t.Errorf("configs mismatch (-want +got):\n%s", diff)
}
}
func TestMergeConfig(t *testing.T) {
config1JSON := `{
"baseImage": "ubuntu:20.04",
"packages": {
"python": "latest",
"node": "22"
},
"aptPackages": ["git"],
"caches": {
"npm": {
"directory": "/root/.npm",
"type": "locked"
},
"pip": {
"directory": "/root/.cache/pip"
}
},
"secrets": ["SECRET_1", "API_KEY"],
"steps": {
"install": {
"name": "install",
"secrets": ["*"],
"assets": {
"package.json": "content1",
"requirements.txt": "content2"
},
"commands": [
{"type": "exec", "cmd": "npm install", "caches": ["npm"], "customName": "Install NPM deps"},
{"type": "path", "path": "/usr/local/bin"},
{"type": "variable", "name": "NODE_ENV", "value": "production"},
{"type": "copy", "src": "/src", "dest": "/app", "image": "alpine:latest"},
{"type": "file", "path": "/app", "name": "config.json", "mode": 384, "customName": "Write config"}
]
},
"build": {
"name": "build",
"commands": [
{"type": "exec", "cmd": "config 1 a"},
{"type": "exec", "cmd": "config 1 b"}
]
}
},
"deploy": {
"startCommand": "python app.py"
}
}`
config2JSON := `{
"providers": ["node"],
"baseImage": "ubuntu:22.04",
"packages": {
"node": "23"
},
"aptPackages": ["curl"],
"caches": {
"npm": {
"directory": "/root/.npm-new",
"type": "shared"
},
"go": {
"directory": "/root/.cache/go-build"
}
},
"secrets": ["SECRET_2"],
"steps": {
"install": {
"name": "install",
"secrets": ["*"],
"assets": {
"package.json": "content3"
}
},
"build": {
"name": "build",
"secrets": [],
"commands": [
{"type": "exec", "cmd": "config 2 a"},
{"type": "exec", "cmd": "config 2 b"}
]
}
},
"deploy": {
"aptPackages": ["curl"],
"startCommand": "node server.js",
"paths": ["/usr/local/bin", "/app/bin"]
}
}`
expectedJSON := `{
"providers": ["node"],
"baseImage": "ubuntu:22.04",
"packages": {
"python": "latest",
"node": "23"
},
"aptPackages": ["curl"],
"caches": {
"npm": {
"directory": "/root/.npm-new",
"type": "shared"
},
"go": {
"directory": "/root/.cache/go-build"
},
"pip": {
"directory": "/root/.cache/pip"
}
},
"secrets": ["SECRET_2"],
"steps": {
"install": {
"name": "install",
"secrets": ["*"],
"assets": {
"package.json": "content3",
"requirements.txt": "content2"
},
"commands": [
{"type": "exec", "cmd": "npm install", "caches": ["npm"], "customName": "Install NPM deps"},
{"type": "path", "path": "/usr/local/bin"},
{"type": "variable", "name": "NODE_ENV", "value": "production"},
{"type": "copy", "src": "/src", "dest": "/app", "image": "alpine:latest"},
{"type": "file", "path": "/app", "name": "config.json", "mode": 384, "customName": "Write config"}
]
},
"build": {
"name": "build",
"secrets": [],
"commands": [
{"type": "exec", "cmd": "config 2 a"},
{"type": "exec", "cmd": "config 2 b"}
]
}
},
"deploy": {
"aptPackages": ["curl"],
"startCommand": "node server.js",
"paths": ["/usr/local/bin", "/app/bin"]
}
}`
var config1, config2, expected Config
require.NoError(t, json.Unmarshal([]byte(config1JSON), &config1))
require.NoError(t, json.Unmarshal([]byte(config2JSON), &config2))
require.NoError(t, json.Unmarshal([]byte(expectedJSON), &expected))
result := Merge(&config1, &config2)
if diff := cmp.Diff(expected, *result); diff != "" {
t.Errorf("configs mismatch (-want +got):\n%s", diff)
}
}
func TestMergeConfigStart(t *testing.T) {
config1JSON := `{
"deploy": {
"startCommand": "python app.py"
}
}`
config2JSON := `{
"packages": {
"node": "23"
}
}`
expectedJSON := `{
"packages": {
"node": "23"
},
"deploy": {
"startCommand": "python app.py"
},
"steps": {},
"caches": {}
}`
var config1, config2, expected Config
require.NoError(t, json.Unmarshal([]byte(config1JSON), &config1))
require.NoError(t, json.Unmarshal([]byte(config2JSON), &config2))
require.NoError(t, json.Unmarshal([]byte(expectedJSON), &expected))
result := Merge(&config1, &config2)
if diff := cmp.Diff(expected, *result); diff != "" {
t.Errorf("configs mismatch (-want +got):\n%s", diff)
}
}
func TestGetJsonSchema(t *testing.T) {
schema := GetJsonSchema()
require.NotEmpty(t, schema)
require.NotContains(t, schema.Required, "provider")
providerSchema, ok := schema.Properties.Get("provider")
require.True(t, ok)
require.ElementsMatch(t, []any{
"php",
"golang",
"java",
"rust",
"ruby",
"elixir",
"python",
"deno",
"dotnet",
"node",
"gleam",
"cpp",
"staticfile",
"shell",
}, providerSchema.Enum)
schemaJson, err := json.MarshalIndent(schema, "", " ")
require.NoError(t, err)
require.NotEmpty(t, schemaJson)
}
func TestConfigExcludeInclude(t *testing.T) {
configJSON := `{
"exclude": [
"node_modules",
"*.log",
".env",
"!important.log"
]
}`
var config Config
require.NoError(t, json.Unmarshal([]byte(configJSON), &config))
require.Equal(t, []string{"node_modules", "*.log", ".env", "!important.log"}, config.Exclude)
}
func TestMergeConfigExcludeInclude(t *testing.T) {
config1JSON := `{
"exclude": ["node_modules", ".env", "!important.log"]
}`
config2JSON := `{
"exclude": ["*.tmp", "!keep.tmp"]
}`
expectedJSON := `{
"exclude": ["*.tmp", "!keep.tmp"],
"steps": {},
"packages": {},
"caches": {},
"deploy": {}
}`
var config1, config2, expected Config
require.NoError(t, json.Unmarshal([]byte(config1JSON), &config1))
require.NoError(t, json.Unmarshal([]byte(config2JSON), &config2))
require.NoError(t, json.Unmarshal([]byte(expectedJSON), &expected))
result := Merge(&config1, &config2)
if diff := cmp.Diff(expected, *result); diff != "" {
t.Errorf("configs mismatch (-want +got):\n%s", diff)
}
}