fix(build): emit docker tarball artifacts

This commit is contained in:
Virgil 2026-04-01 12:40:30 +00:00
parent e66220f493
commit ebebfd2155
2 changed files with 113 additions and 19 deletions

View file

@ -3,6 +3,7 @@ package builders
import (
"context"
"strings"
"dappco.re/go/core"
"dappco.re/go/core/build/internal/ax"
@ -82,6 +83,10 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
}
// If no targets specified, use current platform
buildTargets := targets
if len(buildTargets) == 0 {
buildTargets = []build.Target{{OS: "linux", Arch: "amd64"}}
}
if len(platforms) == 0 {
platforms = []string{"linux/amd64"}
}
@ -141,18 +146,15 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
args = append(args, "--build-arg", core.Sprintf("VERSION=%s", cfg.Version))
}
safeImageName := strings.ReplaceAll(imageName, "/", "_")
// Output to local docker images or push
if cfg.Push {
args = append(args, "--push")
} else {
// For multi-platform builds without push, we need to load or output somewhere
if len(platforms) == 1 {
args = append(args, "--load")
} else {
// Multi-platform builds can't use --load, output to tarball
outputPath := ax.Join(cfg.OutputDir, core.Sprintf("%s.tar", imageName))
args = append(args, "--output", core.Sprintf("type=oci,dest=%s", outputPath))
}
// Local Docker builds emit an OCI archive so the build output is a file.
outputPath := ax.Join(cfg.OutputDir, core.Sprintf("%s.tar", safeImageName))
args = append(args, "--output", core.Sprintf("type=oci,dest=%s", outputPath))
}
// Build context (project directory)
@ -167,21 +169,23 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
core.Print(nil, " Platforms: %s", core.Join(", ", platforms...))
core.Print(nil, " Tags: %s", core.Join(", ", imageRefs...))
// Build once for the full platform set. Docker buildx produces a single
// multi-arch image or OCI archive from the combined platform list.
if err := ax.ExecDir(ctx, cfg.ProjectDir, dockerCommand, args...); err != nil {
return nil, coreerr.E("DockerBuilder.Build", "buildx build failed", err)
}
// Create artifacts for each platform
var artifacts []build.Artifact
for _, t := range targets {
artifacts = append(artifacts, build.Artifact{
Path: imageRefs[0], // Primary image reference
OS: t.OS,
Arch: t.Arch,
})
artifactPath := imageRefs[0]
if !cfg.Push {
artifactPath = ax.Join(cfg.OutputDir, core.Sprintf("%s.tar", safeImageName))
}
return artifacts, nil
primaryTarget := buildTargets[0]
return []build.Artifact{{
Path: artifactPath,
OS: primaryTarget.OS,
Arch: primaryTarget.Arch,
}}, nil
}
// resolveDockerCli returns the executable path for the docker CLI.

View file

@ -1,23 +1,56 @@
package builders
import (
"context"
"os"
"strings"
"testing"
"dappco.re/go/core/build/internal/ax"
"dappco.re/go/core/build/pkg/build"
"dappco.re/go/core/io"
coreio "dappco.re/go/core/io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupFakeDockerToolchain(t *testing.T, binDir string) {
t.Helper()
script := `#!/bin/sh
set -eu
log_file="${DOCKER_BUILD_LOG_FILE:-}"
if [ -n "$log_file" ]; then
printf '%s\n' "$*" >> "$log_file"
fi
if [ "${1:-}" = "buildx" ] && [ "${2:-}" = "build" ]; then
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))
}
func TestDocker_DockerBuilderName_Good(t *testing.T) {
builder := NewDockerBuilder()
assert.Equal(t, "docker", builder.Name())
}
func TestDocker_DockerBuilderDetect_Good(t *testing.T) {
fs := io.Local
fs := coreio.Local
t.Run("detects Dockerfile", func(t *testing.T) {
dir := t.TempDir()
@ -102,3 +135,60 @@ func TestDocker_DockerBuilderResolveDockerCli_Bad(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "docker CLI not found")
}
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()
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,
Name: "sample-app",
Image: "owner/repo",
}
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)
artifacts, err = builder.Build(context.Background(), cfg, nil)
require.NoError(t, err)
require.Len(t, artifacts, 1)
assert.Equal(t, "linux", artifacts[0].OS)
assert.Equal(t, "amd64", artifacts[0].Arch)
}