Files
3e53630854 feat: respect cache backend type for registry cache import/export, add cache support to build subcommand (#595)
* fix: respect cache backend type for registry cache import/export

* docs: update local development build instructions

- Update local image building to use the provided project task.
- Document the workflow for building with a local registry.
- Add examples for configuring buildctl with caching and debugging flags.

Generated-by: aiautocommit

* refactor(buildkit): clean up code comments and formatting

- Remove redundant comments and improve internal documentation.
- Fix minor formatting inconsistencies in code blocks and comment descriptions.

Generated-by: aiautocommit

* feat: add ParseBool utility to handle common boolean string variants

Generated-by: aiautocommit

* feat: integrate local registry and buildkit configuration

- Add buildkitd configuration to enable HTTP/insecure access to the local registry.
- Introduce a local registry service container on port 7890.
- Update the buildkit setup to mount the new registry configuration.
- Add tasks for building the frontend image and running project builds using the local registry and buildctl.

Generated-by: aiautocommit

* docs: update CLAUDE.md with architecture and style guidance

Generated-by: aiautocommit

* feat: add support for multiple cache-from and cache-to build flags

- Allow users to configure external cache sources and destinations via the `build` subcommand.
- Update cache parsing logic to support multiple entries matching the Docker buildx CLI format.
- Remove hardcoded default cache type to provide better control over BuildKit cache configurations.

Generated-by: aiautocommit

* test: add integration tests for cache-import parsing

Generated-by: aiautocommit

* refactor(buildkit): extract cache entry parsing logic

- Centralize cache configuration logic by moving loop-based parsing into a reusable `cacheEntriesFromFlags` helper.
- Improve code maintainability and readability by reducing duplication in `BuildWithBuildkitClient`.
- Add comprehensive unit tests for cache entry processing, including edge cases for flag parsing and empty inputs.

Generated-by: aiautocommit

* fix(test): remove unused buildkit cache flags from integration tests

* Remove unused flag variables that were meant for remote BuildKit caching.
* Ensure integration tests avoid remote cache (e.g., type=gha) to prevent exceeding GitHub Actions cache limits and excessive eviction.

Generated-by: aiautocommit

* test(buildkit): consolidate cache import tests

- Move TestParseCacheImports from its own file to frontend_test.go to consolidate related buildkit frontend tests.

Generated-by: aiautocommit

* style: fix indentation in build test helper

Generated-by: aiautocommit

* docs: document support for BuildKit cache backends

Generated-by: aiautocommit

* docs(developing-locally): add information on Docker, BuildKit, and caching

- Add missing setup prerequisites.
- Include debugging and configuration guidance for BuildKit/Docker.
- Clarify cache export and import implementation expectations in Railpack.

Generated-by: aiautocommit

* docs: clarify cache flag parsing and pnpm version logic

Generated-by: aiautocommit

---------

Co-authored-by: Matt Van Horn <[email protected]>

Co-authored-by: Michael Bianco <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-13 11:15:16 -06:00

196 lines
4.8 KiB
Go

package buildkit
import (
"maps"
"path/filepath"
"testing"
"github.com/moby/buildkit/client"
"github.com/stretchr/testify/require"
)
func TestGetImageName(t *testing.T) {
tests := []struct {
name string
appDir string
expected string
}{
{
name: "lowercase directory name",
appDir: "/path/to/myapp",
expected: "myapp",
},
{
name: "uppercase directory name is lowercased",
appDir: "/path/to/SteelMC",
expected: "steelmc",
},
{
name: "mixed case directory name is lowercased",
appDir: "/path/to/MyApp",
expected: "myapp",
},
{
name: "directory name with hyphens",
appDir: "/path/to/my-app",
expected: "my-app",
},
{
name: "empty path returns fallback",
appDir: "",
expected: "railpack-app",
},
{
name: "path ending with separator",
appDir: "/path/to/myapp" + string(filepath.Separator),
expected: "railpack-app",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getImageName(tt.appDir)
if got != tt.expected {
t.Errorf("getImageName(%q) = %q, want %q", tt.appDir, got, tt.expected)
}
})
}
}
func TestExtractCacheType(t *testing.T) {
tests := []struct {
name string
input string
expectedType string
expectedAttrs map[string]string
}{
{
name: "registry cache type is extracted",
input: "type=registry,ref=localhost:5555/cache",
expectedType: "registry",
expectedAttrs: map[string]string{
"ref": "localhost:5555/cache",
},
},
{
name: "mode max kept in attrs",
input: "type=registry,ref=host.docker.internal:7890/node-bun:cache,mode=max",
expectedType: "registry",
expectedAttrs: map[string]string{
"ref": "host.docker.internal:7890/node-bun:cache",
"mode": "max",
},
},
{
name: "gha shape",
input: "type=gha,scope=my-cache",
expectedType: "gha",
expectedAttrs: map[string]string{
"scope": "my-cache",
},
},
{
name: "type only",
input: "type=registry",
expectedType: "registry",
expectedAttrs: map[string]string{},
},
{
name: "spaces around keys and values",
input: " type = registry , ref = myapp:cache ",
expectedType: "registry",
expectedAttrs: map[string]string{
"ref": "myapp:cache",
},
},
{
name: "missing type is not defaulted",
input: "scope=my-cache,mode=max",
expectedType: "",
expectedAttrs: map[string]string{
"scope": "my-cache",
"mode": "max",
},
},
{
name: "empty attrs",
input: "",
expectedType: "",
expectedAttrs: map[string]string{},
},
{
name: "skips segments without equals",
input: "type=registry,notavalue,ref=x",
expectedType: "registry",
expectedAttrs: map[string]string{
"ref": "x",
},
},
{
name: "value may contain equals",
input: "type=registry,ref=image:tag=latest",
expectedType: "registry",
expectedAttrs: map[string]string{
"ref": "image:tag=latest",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotType, gotAttrs := extractCacheType(parseKeyValue(tt.input))
if gotType != tt.expectedType {
t.Errorf("extractCacheType(%q) type = %q, want %q", tt.input, gotType, tt.expectedType)
}
if !maps.Equal(gotAttrs, tt.expectedAttrs) {
t.Errorf("extractCacheType(%q) attrs = %v, want %v", tt.input, gotAttrs, tt.expectedAttrs)
}
})
}
}
func TestCacheEntriesFromFlags(t *testing.T) {
t.Run("nil slice", func(t *testing.T) {
require.Nil(t, cacheEntriesFromFlags(nil))
})
t.Run("empty slice", func(t *testing.T) {
require.Nil(t, cacheEntriesFromFlags([]string{}))
})
t.Run("skips empty strings", func(t *testing.T) {
got := cacheEntriesFromFlags([]string{"", "type=registry,ref=x", ""})
require.Equal(t, []client.CacheOptionsEntry{
{Type: "registry", Attrs: map[string]string{"ref": "x"}},
}, got)
})
t.Run("multiple registry entries", func(t *testing.T) {
got := cacheEntriesFromFlags([]string{
"type=registry,ref=myapp:cache-main",
"type=registry,ref=myapp:cache-branch",
})
require.Equal(t, []client.CacheOptionsEntry{
{Type: "registry", Attrs: map[string]string{"ref": "myapp:cache-main"}},
{Type: "registry", Attrs: map[string]string{"ref": "myapp:cache-branch"}},
}, got)
})
t.Run("mixed registry and gha with mode", func(t *testing.T) {
got := cacheEntriesFromFlags([]string{
"type=registry,ref=host.docker.internal:7890/node-bun:cache,mode=max",
"type=gha,scope=ci",
})
require.Equal(t, []client.CacheOptionsEntry{
{
Type: "registry",
Attrs: map[string]string{
"ref": "host.docker.internal:7890/node-bun:cache",
"mode": "max",
},
},
{Type: "gha", Attrs: map[string]string{"scope": "ci"}},
}, got)
})
}