feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
package builders
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-01 12:40:30 +00:00
|
|
|
"context"
|
|
|
|
|
"os"
|
2026-04-01 13:24:35 +00:00
|
|
|
"runtime"
|
2026-04-01 12:40:30 +00:00
|
|
|
"strings"
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
"testing"
|
|
|
|
|
|
2026-03-26 17:41:53 +00:00
|
|
|
"dappco.re/go/core/build/internal/ax"
|
|
|
|
|
|
2026-03-22 01:53:16 +00:00
|
|
|
"dappco.re/go/core/build/pkg/build"
|
2026-04-01 12:40:30 +00:00
|
|
|
coreio "dappco.re/go/core/io"
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-01 12:40:30 +00:00
|
|
|
func setupFakeDockerToolchain(t *testing.T, binDir string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
script := `#!/bin/sh
|
|
|
|
|
set -eu
|
|
|
|
|
|
2026-04-01 16:02:09 +00:00
|
|
|
log_file="${DOCKER_BUILD_LOG_FILE:-}"
|
|
|
|
|
if [ -n "$log_file" ]; then
|
|
|
|
|
printf '%s\n' "$*" >> "$log_file"
|
|
|
|
|
env | sort >> "$log_file"
|
|
|
|
|
fi
|
2026-04-01 12:40:30 +00:00
|
|
|
|
2026-04-01 16:02:09 +00:00
|
|
|
if [ "${1:-}" = "buildx" ] && [ "${2:-}" = "build" ]; then
|
2026-04-01 12:40:30 +00:00
|
|
|
dest=""
|
|
|
|
|
while [ $# -gt 0 ]; do
|
|
|
|
|
if [ "$1" = "--output" ]; then
|
|
|
|
|
shift
|
|
|
|
|
dest="$(printf '%s' "$1" | sed -n 's#type=oci,dest=##p')"
|
|
|
|
|
fi
|
|
|
|
|
shift
|
|
|
|
|
done
|
|
|
|
|
if [ -n "$dest" ]; then
|
|
|
|
|
mkdir -p "$(dirname "$dest")"
|
|
|
|
|
printf 'oci archive\n' > "$dest"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
require.NoError(t, ax.WriteFile(ax.Join(binDir, "docker"), []byte(script), 0o755))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 17:41:53 +00:00
|
|
|
func TestDocker_DockerBuilderName_Good(t *testing.T) {
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
assert.Equal(t, "docker", builder.Name())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 17:41:53 +00:00
|
|
|
func TestDocker_DockerBuilderDetect_Good(t *testing.T) {
|
2026-04-01 12:40:30 +00:00
|
|
|
fs := coreio.Local
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
|
|
|
|
|
t.Run("detects Dockerfile", func(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
2026-03-26 17:41:53 +00:00
|
|
|
err := ax.WriteFile(ax.Join(dir, "Dockerfile"), []byte("FROM alpine\n"), 0644)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
detected, err := builder.Detect(fs, dir)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.True(t, detected)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-02 00:11:37 +00:00
|
|
|
t.Run("detects Containerfile", func(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
err := ax.WriteFile(ax.Join(dir, "Containerfile"), []byte("FROM alpine\n"), 0644)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
detected, err := builder.Detect(fs, dir)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.True(t, detected)
|
|
|
|
|
})
|
|
|
|
|
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
t.Run("returns false for empty directory", func(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
detected, err := builder.Detect(fs, dir)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.False(t, detected)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("returns false for non-Docker project", func(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
// Create a Go project instead
|
2026-03-26 17:41:53 +00:00
|
|
|
err := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0644)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
detected, err := builder.Detect(fs, dir)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.False(t, detected)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("does not match docker-compose.yml", func(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
2026-03-26 17:41:53 +00:00
|
|
|
err := ax.WriteFile(ax.Join(dir, "docker-compose.yml"), []byte("version: '3'\n"), 0644)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
detected, err := builder.Detect(fs, dir)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.False(t, detected)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("does not match Dockerfile in subdirectory", func(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
2026-03-26 17:41:53 +00:00
|
|
|
subDir := ax.Join(dir, "subdir")
|
|
|
|
|
require.NoError(t, ax.MkdirAll(subDir, 0755))
|
|
|
|
|
err := ax.WriteFile(ax.Join(subDir, "Dockerfile"), []byte("FROM alpine\n"), 0644)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
detected, err := builder.Detect(fs, dir)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.False(t, detected)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 17:41:53 +00:00
|
|
|
func TestDocker_DockerBuilderInterface_Good(t *testing.T) {
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
// Verify DockerBuilder implements Builder interface
|
|
|
|
|
var _ build.Builder = (*DockerBuilder)(nil)
|
|
|
|
|
var _ build.Builder = NewDockerBuilder()
|
|
|
|
|
}
|
2026-03-30 01:19:19 +00:00
|
|
|
|
|
|
|
|
func TestDocker_DockerBuilderResolveDockerCli_Good(t *testing.T) {
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
fallbackDir := t.TempDir()
|
|
|
|
|
fallbackPath := ax.Join(fallbackDir, "docker")
|
|
|
|
|
require.NoError(t, ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755))
|
2026-03-30 01:28:57 +00:00
|
|
|
t.Setenv("PATH", "")
|
2026-03-30 01:19:19 +00:00
|
|
|
|
|
|
|
|
command, err := builder.resolveDockerCli(fallbackPath)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, fallbackPath, command)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDocker_DockerBuilderResolveDockerCli_Bad(t *testing.T) {
|
|
|
|
|
builder := NewDockerBuilder()
|
2026-03-30 01:28:57 +00:00
|
|
|
t.Setenv("PATH", "")
|
2026-03-30 01:19:19 +00:00
|
|
|
|
|
|
|
|
_, err := builder.resolveDockerCli(ax.Join(t.TempDir(), "missing-docker"))
|
|
|
|
|
require.Error(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "docker CLI not found")
|
|
|
|
|
}
|
2026-04-01 12:40:30 +00:00
|
|
|
|
|
|
|
|
func TestDocker_DockerBuilderBuild_Good(t *testing.T) {
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
t.Skip("skipping integration test in short mode")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
binDir := t.TempDir()
|
|
|
|
|
setupFakeDockerToolchain(t, binDir)
|
|
|
|
|
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
|
|
|
|
|
|
projectDir := t.TempDir()
|
2026-04-02 00:11:37 +00:00
|
|
|
require.NoError(t, ax.WriteFile(ax.Join(projectDir, "Containerfile"), []byte("FROM alpine:latest\n"), 0o644))
|
2026-04-01 12:40:30 +00:00
|
|
|
|
|
|
|
|
outputDir := t.TempDir()
|
|
|
|
|
logDir := t.TempDir()
|
|
|
|
|
logPath := ax.Join(logDir, "docker.log")
|
|
|
|
|
t.Setenv("DOCKER_BUILD_LOG_FILE", logPath)
|
|
|
|
|
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
cfg := &build.Config{
|
|
|
|
|
FS: coreio.Local,
|
|
|
|
|
ProjectDir: projectDir,
|
|
|
|
|
OutputDir: outputDir,
|
|
|
|
|
Name: "sample-app",
|
|
|
|
|
Image: "owner/repo",
|
2026-04-01 16:02:09 +00:00
|
|
|
Env: []string{"FOO=bar"},
|
2026-04-01 12:40:30 +00:00
|
|
|
}
|
|
|
|
|
targets := []build.Target{
|
|
|
|
|
{OS: "linux", Arch: "amd64"},
|
|
|
|
|
{OS: "linux", Arch: "arm64"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
artifacts, err := builder.Build(context.Background(), cfg, targets)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, artifacts, 1)
|
|
|
|
|
|
|
|
|
|
expectedPath := ax.Join(outputDir, "owner_repo.tar")
|
|
|
|
|
assert.Equal(t, expectedPath, artifacts[0].Path)
|
|
|
|
|
assert.Equal(t, "linux", artifacts[0].OS)
|
|
|
|
|
assert.Equal(t, "amd64", artifacts[0].Arch)
|
|
|
|
|
assert.FileExists(t, expectedPath)
|
|
|
|
|
|
|
|
|
|
logContent, err := ax.ReadFile(logPath)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
log := string(logContent)
|
|
|
|
|
assert.Equal(t, 1, strings.Count(log, "buildx build"))
|
|
|
|
|
assert.Contains(t, log, "--platform")
|
|
|
|
|
assert.Contains(t, log, "linux/amd64,linux/arm64")
|
|
|
|
|
assert.Contains(t, log, "--output")
|
|
|
|
|
assert.Contains(t, log, "type=oci,dest="+expectedPath)
|
|
|
|
|
|
2026-04-01 16:02:09 +00:00
|
|
|
assert.Contains(t, log, "FOO=bar")
|
|
|
|
|
|
2026-04-01 12:40:30 +00:00
|
|
|
artifacts, err = builder.Build(context.Background(), cfg, nil)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, artifacts, 1)
|
2026-04-01 13:24:35 +00:00
|
|
|
assert.Equal(t, runtime.GOOS, artifacts[0].OS)
|
|
|
|
|
assert.Equal(t, runtime.GOARCH, artifacts[0].Arch)
|
2026-04-01 12:40:30 +00:00
|
|
|
}
|
2026-04-01 15:24:33 +00:00
|
|
|
|
2026-04-02 02:30:31 +00:00
|
|
|
func TestDocker_DockerBuilderBuild_ResolvesRelativeDockerfile_Good(t *testing.T) {
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
t.Skip("skipping integration test in short mode")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
binDir := t.TempDir()
|
|
|
|
|
setupFakeDockerToolchain(t, binDir)
|
|
|
|
|
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
|
|
|
|
|
|
projectDir := t.TempDir()
|
|
|
|
|
dockerfilePath := ax.Join(projectDir, "dockerfiles", "Dockerfile.app")
|
|
|
|
|
require.NoError(t, ax.MkdirAll(ax.Dir(dockerfilePath), 0o755))
|
|
|
|
|
require.NoError(t, ax.WriteFile(dockerfilePath, []byte("FROM alpine:latest\n"), 0o644))
|
|
|
|
|
|
|
|
|
|
outputDir := t.TempDir()
|
|
|
|
|
logDir := t.TempDir()
|
|
|
|
|
logPath := ax.Join(logDir, "docker.log")
|
|
|
|
|
t.Setenv("DOCKER_BUILD_LOG_FILE", logPath)
|
|
|
|
|
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
cfg := &build.Config{
|
|
|
|
|
FS: coreio.Local,
|
|
|
|
|
ProjectDir: projectDir,
|
|
|
|
|
OutputDir: outputDir,
|
|
|
|
|
Dockerfile: "dockerfiles/Dockerfile.app",
|
|
|
|
|
Image: "owner/repo",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
artifacts, err := builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, artifacts, 1)
|
|
|
|
|
assert.FileExists(t, ax.Join(outputDir, "owner_repo.tar"))
|
|
|
|
|
|
|
|
|
|
logContent, err := ax.ReadFile(logPath)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
log := string(logContent)
|
|
|
|
|
|
|
|
|
|
assert.Contains(t, log, "-f")
|
|
|
|
|
assert.Contains(t, log, dockerfilePath)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 00:11:37 +00:00
|
|
|
func TestDocker_DockerBuilderBuild_Containerfile_Good(t *testing.T) {
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
t.Skip("skipping integration test in short mode")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
binDir := t.TempDir()
|
|
|
|
|
setupFakeDockerToolchain(t, binDir)
|
|
|
|
|
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
|
|
|
|
|
|
projectDir := t.TempDir()
|
|
|
|
|
require.NoError(t, ax.WriteFile(ax.Join(projectDir, "Containerfile"), []byte("FROM alpine:latest\n"), 0o644))
|
|
|
|
|
|
|
|
|
|
outputDir := t.TempDir()
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
cfg := &build.Config{
|
|
|
|
|
FS: coreio.Local,
|
|
|
|
|
ProjectDir: projectDir,
|
|
|
|
|
OutputDir: outputDir,
|
|
|
|
|
Image: "owner/repo",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
artifacts, err := builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, artifacts, 1)
|
|
|
|
|
assert.FileExists(t, ax.Join(outputDir, "owner_repo.tar"))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 15:24:33 +00:00
|
|
|
func TestDocker_DockerBuilderBuild_Load_Good(t *testing.T) {
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
t.Skip("skipping integration test in short mode")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
binDir := t.TempDir()
|
|
|
|
|
setupFakeDockerToolchain(t, binDir)
|
|
|
|
|
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
|
|
|
|
|
|
projectDir := t.TempDir()
|
|
|
|
|
require.NoError(t, ax.WriteFile(ax.Join(projectDir, "Dockerfile"), []byte("FROM alpine:latest\n"), 0o644))
|
|
|
|
|
|
|
|
|
|
outputDir := t.TempDir()
|
|
|
|
|
logDir := t.TempDir()
|
|
|
|
|
logPath := ax.Join(logDir, "docker.log")
|
|
|
|
|
t.Setenv("DOCKER_BUILD_LOG_FILE", logPath)
|
|
|
|
|
|
|
|
|
|
builder := NewDockerBuilder()
|
|
|
|
|
cfg := &build.Config{
|
|
|
|
|
FS: coreio.Local,
|
|
|
|
|
ProjectDir: projectDir,
|
|
|
|
|
OutputDir: outputDir,
|
|
|
|
|
Image: "owner/repo",
|
|
|
|
|
Load: true,
|
2026-04-01 16:02:09 +00:00
|
|
|
Env: []string{"FOO=bar"},
|
2026-04-01 15:24:33 +00:00
|
|
|
}
|
|
|
|
|
targets := []build.Target{
|
|
|
|
|
{OS: "linux", Arch: "amd64"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
artifacts, err := builder.Build(context.Background(), cfg, targets)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, artifacts, 1)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, "ghcr.io/owner/repo:latest", artifacts[0].Path)
|
|
|
|
|
assert.Equal(t, "linux", artifacts[0].OS)
|
|
|
|
|
assert.Equal(t, "amd64", artifacts[0].Arch)
|
|
|
|
|
assert.DirExists(t, outputDir)
|
|
|
|
|
|
|
|
|
|
logContent, err := ax.ReadFile(logPath)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
log := string(logContent)
|
|
|
|
|
assert.Contains(t, log, "buildx build")
|
|
|
|
|
assert.Contains(t, log, "--load")
|
|
|
|
|
assert.NotContains(t, log, "--output")
|
|
|
|
|
}
|