cli/pkg/release/publishers/docker_test.go
Snider f2bc912ebe feat: infrastructure packages and lint cleanup (#281)
* ci: consolidate duplicate workflows and merge CodeQL configs

Remove 17 duplicate workflow files that were split copies of the
combined originals. Each family (CI, CodeQL, Coverage, PR Build,
Alpha Release) had the same job duplicated across separate
push/pull_request/schedule/manual trigger files.

Merge codeql.yml and codescan.yml into a single codeql.yml with
a language matrix covering go, javascript-typescript, python,
and actions — matching the previous default setup coverage.

Remaining workflows (one per family):
- ci.yml (push + PR + manual)
- codeql.yml (push + PR + schedule, all languages)
- coverage.yml (push + PR + manual)
- alpha-release.yml (push + manual)
- pr-build.yml (PR + manual)
- release.yml (tag push)
- agent-verify.yml, auto-label.yml, auto-project.yml

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add collect, config, crypt, plugin packages and fix all lint issues

Add four new infrastructure packages with CLI commands:
- pkg/config: layered configuration (defaults → file → env → flags)
- pkg/crypt: crypto primitives (Argon2id, AES-GCM, ChaCha20, HMAC, checksums)
- pkg/plugin: plugin system with GitHub-based install/update/remove
- pkg/collect: collection subsystem (GitHub, BitcoinTalk, market, papers, excavate)

Fix all golangci-lint issues across the entire codebase (~100 errcheck,
staticcheck SA1012/SA1019/ST1005, unused, ineffassign fixes) so that
`core go qa` passes with 0 issues.

Closes #167, #168, #170, #250, #251, #252, #253, #254, #255, #256

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:34:43 +00:00

798 lines
22 KiB
Go

package publishers
import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDockerPublisher_Name_Good(t *testing.T) {
t.Run("returns docker", func(t *testing.T) {
p := NewDockerPublisher()
assert.Equal(t, "docker", p.Name())
})
}
func TestDockerPublisher_ParseConfig_Good(t *testing.T) {
p := NewDockerPublisher()
t.Run("uses defaults when no extended config", func(t *testing.T) {
pubCfg := PublisherConfig{Type: "docker"}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
cfg := p.parseConfig(pubCfg, relCfg, "/project")
assert.Equal(t, "ghcr.io", cfg.Registry)
assert.Equal(t, "owner/repo", cfg.Image)
assert.Equal(t, "/project/Dockerfile", cfg.Dockerfile)
assert.Equal(t, []string{"linux/amd64", "linux/arm64"}, cfg.Platforms)
assert.Equal(t, []string{"latest", "{{.Version}}"}, cfg.Tags)
})
t.Run("parses extended config", func(t *testing.T) {
pubCfg := PublisherConfig{
Type: "docker",
Extended: map[string]any{
"registry": "docker.io",
"image": "myorg/myimage",
"dockerfile": "docker/Dockerfile.prod",
"platforms": []any{"linux/amd64"},
"tags": []any{"latest", "stable", "{{.Version}}"},
"build_args": map[string]any{
"GO_VERSION": "1.21",
},
},
}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
cfg := p.parseConfig(pubCfg, relCfg, "/project")
assert.Equal(t, "docker.io", cfg.Registry)
assert.Equal(t, "myorg/myimage", cfg.Image)
assert.Equal(t, "/project/docker/Dockerfile.prod", cfg.Dockerfile)
assert.Equal(t, []string{"linux/amd64"}, cfg.Platforms)
assert.Equal(t, []string{"latest", "stable", "{{.Version}}"}, cfg.Tags)
assert.Equal(t, "1.21", cfg.BuildArgs["GO_VERSION"])
})
t.Run("handles absolute dockerfile path", func(t *testing.T) {
pubCfg := PublisherConfig{
Type: "docker",
Extended: map[string]any{
"dockerfile": "/absolute/path/Dockerfile",
},
}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
cfg := p.parseConfig(pubCfg, relCfg, "/project")
assert.Equal(t, "/absolute/path/Dockerfile", cfg.Dockerfile)
})
}
func TestDockerPublisher_ResolveTags_Good(t *testing.T) {
p := NewDockerPublisher()
t.Run("resolves version template", func(t *testing.T) {
tags := p.resolveTags([]string{"latest", "{{.Version}}", "stable"}, "v1.2.3")
assert.Equal(t, []string{"latest", "v1.2.3", "stable"}, tags)
})
t.Run("handles simple version syntax", func(t *testing.T) {
tags := p.resolveTags([]string{"{{Version}}"}, "v1.0.0")
assert.Equal(t, []string{"v1.0.0"}, tags)
})
t.Run("handles no templates", func(t *testing.T) {
tags := p.resolveTags([]string{"latest", "stable"}, "v1.2.3")
assert.Equal(t, []string{"latest", "stable"}, tags)
})
}
func TestDockerPublisher_BuildFullTag_Good(t *testing.T) {
p := NewDockerPublisher()
tests := []struct {
name string
registry string
image string
tag string
expected string
}{
{
name: "with registry",
registry: "ghcr.io",
image: "owner/repo",
tag: "v1.0.0",
expected: "ghcr.io/owner/repo:v1.0.0",
},
{
name: "without registry",
registry: "",
image: "myimage",
tag: "latest",
expected: "myimage:latest",
},
{
name: "docker hub",
registry: "docker.io",
image: "library/nginx",
tag: "alpine",
expected: "docker.io/library/nginx:alpine",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tag := p.buildFullTag(tc.registry, tc.image, tc.tag)
assert.Equal(t, tc.expected, tag)
})
}
}
func TestDockerPublisher_BuildBuildxArgs_Good(t *testing.T) {
p := NewDockerPublisher()
t.Run("builds basic args", func(t *testing.T) {
cfg := DockerConfig{
Registry: "ghcr.io",
Image: "owner/repo",
Dockerfile: "/project/Dockerfile",
Platforms: []string{"linux/amd64", "linux/arm64"},
BuildArgs: make(map[string]string),
}
tags := []string{"latest", "v1.0.0"}
args := p.buildBuildxArgs(cfg, tags, "v1.0.0")
assert.Contains(t, args, "buildx")
assert.Contains(t, args, "build")
assert.Contains(t, args, "--platform")
assert.Contains(t, args, "linux/amd64,linux/arm64")
assert.Contains(t, args, "-t")
assert.Contains(t, args, "ghcr.io/owner/repo:latest")
assert.Contains(t, args, "ghcr.io/owner/repo:v1.0.0")
assert.Contains(t, args, "-f")
assert.Contains(t, args, "/project/Dockerfile")
assert.Contains(t, args, "--push")
assert.Contains(t, args, ".")
})
t.Run("includes build args", func(t *testing.T) {
cfg := DockerConfig{
Registry: "ghcr.io",
Image: "owner/repo",
Dockerfile: "/project/Dockerfile",
Platforms: []string{"linux/amd64"},
BuildArgs: map[string]string{
"GO_VERSION": "1.21",
"APP_NAME": "myapp",
},
}
tags := []string{"latest"}
args := p.buildBuildxArgs(cfg, tags, "v1.0.0")
assert.Contains(t, args, "--build-arg")
// Check that build args are present (order may vary)
foundGoVersion := false
foundAppName := false
foundVersion := false
for i, arg := range args {
if arg == "--build-arg" && i+1 < len(args) {
if args[i+1] == "GO_VERSION=1.21" {
foundGoVersion = true
}
if args[i+1] == "APP_NAME=myapp" {
foundAppName = true
}
if args[i+1] == "VERSION=v1.0.0" {
foundVersion = true
}
}
}
assert.True(t, foundGoVersion, "GO_VERSION build arg not found")
assert.True(t, foundAppName, "APP_NAME build arg not found")
assert.True(t, foundVersion, "VERSION build arg not found")
})
t.Run("expands version in build args", func(t *testing.T) {
cfg := DockerConfig{
Registry: "ghcr.io",
Image: "owner/repo",
Dockerfile: "/project/Dockerfile",
Platforms: []string{"linux/amd64"},
BuildArgs: map[string]string{
"APP_VERSION": "{{.Version}}",
},
}
tags := []string{"latest"}
args := p.buildBuildxArgs(cfg, tags, "v2.0.0")
foundExpandedVersion := false
for i, arg := range args {
if arg == "--build-arg" && i+1 < len(args) {
if args[i+1] == "APP_VERSION=v2.0.0" {
foundExpandedVersion = true
}
}
}
assert.True(t, foundExpandedVersion, "APP_VERSION should be expanded to v2.0.0")
})
}
func TestDockerPublisher_Publish_Bad(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
p := NewDockerPublisher()
t.Run("fails when dockerfile not found", func(t *testing.T) {
release := &Release{
Version: "v1.0.0",
ProjectDir: "/nonexistent",
}
pubCfg := PublisherConfig{
Type: "docker",
Extended: map[string]any{
"dockerfile": "/nonexistent/Dockerfile",
},
}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
err := p.Publish(context.TODO(), release, pubCfg, relCfg, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Dockerfile not found")
})
}
func TestDockerConfig_Defaults_Good(t *testing.T) {
t.Run("has sensible defaults", func(t *testing.T) {
p := NewDockerPublisher()
pubCfg := PublisherConfig{Type: "docker"}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
cfg := p.parseConfig(pubCfg, relCfg, "/project")
// Verify defaults
assert.Equal(t, "ghcr.io", cfg.Registry)
assert.Equal(t, "owner/repo", cfg.Image)
assert.Len(t, cfg.Platforms, 2)
assert.Contains(t, cfg.Platforms, "linux/amd64")
assert.Contains(t, cfg.Platforms, "linux/arm64")
assert.Contains(t, cfg.Tags, "latest")
})
}
func TestDockerPublisher_DryRunPublish_Good(t *testing.T) {
p := NewDockerPublisher()
t.Run("outputs expected dry run information", func(t *testing.T) {
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
release := &Release{
Version: "v1.0.0",
ProjectDir: "/project",
}
cfg := DockerConfig{
Registry: "ghcr.io",
Image: "owner/repo",
Dockerfile: "/project/Dockerfile",
Platforms: []string{"linux/amd64", "linux/arm64"},
Tags: []string{"latest", "{{.Version}}"},
BuildArgs: make(map[string]string),
}
err := p.dryRunPublish(release, cfg)
_ = w.Close()
var buf bytes.Buffer
_, _ = buf.ReadFrom(r)
os.Stdout = oldStdout
require.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "DRY RUN: Docker Build & Push")
assert.Contains(t, output, "Version: v1.0.0")
assert.Contains(t, output, "Registry: ghcr.io")
assert.Contains(t, output, "Image: owner/repo")
assert.Contains(t, output, "Dockerfile: /project/Dockerfile")
assert.Contains(t, output, "Platforms: linux/amd64, linux/arm64")
assert.Contains(t, output, "Tags to be applied:")
assert.Contains(t, output, "ghcr.io/owner/repo:latest")
assert.Contains(t, output, "ghcr.io/owner/repo:v1.0.0")
assert.Contains(t, output, "Would execute command:")
assert.Contains(t, output, "docker buildx build")
assert.Contains(t, output, "END DRY RUN")
})
t.Run("shows build args when present", func(t *testing.T) {
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
release := &Release{
Version: "v1.0.0",
ProjectDir: "/project",
}
cfg := DockerConfig{
Registry: "docker.io",
Image: "myorg/myapp",
Dockerfile: "/project/Dockerfile",
Platforms: []string{"linux/amd64"},
Tags: []string{"latest"},
BuildArgs: map[string]string{
"GO_VERSION": "1.21",
"APP_NAME": "myapp",
},
}
err := p.dryRunPublish(release, cfg)
_ = w.Close()
var buf bytes.Buffer
_, _ = buf.ReadFrom(r)
os.Stdout = oldStdout
require.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Build arguments:")
assert.Contains(t, output, "GO_VERSION=1.21")
assert.Contains(t, output, "APP_NAME=myapp")
})
t.Run("handles single platform", func(t *testing.T) {
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
release := &Release{
Version: "v2.0.0",
ProjectDir: "/project",
}
cfg := DockerConfig{
Registry: "ghcr.io",
Image: "owner/repo",
Dockerfile: "/project/Dockerfile.prod",
Platforms: []string{"linux/amd64"},
Tags: []string{"stable"},
BuildArgs: make(map[string]string),
}
err := p.dryRunPublish(release, cfg)
_ = w.Close()
var buf bytes.Buffer
_, _ = buf.ReadFrom(r)
os.Stdout = oldStdout
require.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Platforms: linux/amd64")
assert.Contains(t, output, "ghcr.io/owner/repo:stable")
})
}
func TestDockerPublisher_ParseConfig_EdgeCases_Good(t *testing.T) {
p := NewDockerPublisher()
t.Run("handles nil release config", func(t *testing.T) {
pubCfg := PublisherConfig{
Type: "docker",
Extended: map[string]any{
"image": "custom/image",
},
}
cfg := p.parseConfig(pubCfg, nil, "/project")
assert.Equal(t, "custom/image", cfg.Image)
assert.Equal(t, "ghcr.io", cfg.Registry)
})
t.Run("handles empty repository in release config", func(t *testing.T) {
pubCfg := PublisherConfig{
Type: "docker",
Extended: map[string]any{
"image": "fallback/image",
},
}
relCfg := &mockReleaseConfig{repository: ""}
cfg := p.parseConfig(pubCfg, relCfg, "/project")
assert.Equal(t, "fallback/image", cfg.Image)
})
t.Run("extended config overrides repository image", func(t *testing.T) {
pubCfg := PublisherConfig{
Type: "docker",
Extended: map[string]any{
"image": "override/image",
},
}
relCfg := &mockReleaseConfig{repository: "original/repo"}
cfg := p.parseConfig(pubCfg, relCfg, "/project")
assert.Equal(t, "override/image", cfg.Image)
})
t.Run("handles mixed build args types", func(t *testing.T) {
pubCfg := PublisherConfig{
Type: "docker",
Extended: map[string]any{
"build_args": map[string]any{
"STRING_ARG": "value",
"INT_ARG": 123, // Non-string value should be skipped
},
},
}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
cfg := p.parseConfig(pubCfg, relCfg, "/project")
assert.Equal(t, "value", cfg.BuildArgs["STRING_ARG"])
_, exists := cfg.BuildArgs["INT_ARG"]
assert.False(t, exists, "non-string build arg should not be included")
})
}
func TestDockerPublisher_ResolveTags_EdgeCases_Good(t *testing.T) {
p := NewDockerPublisher()
t.Run("handles empty tags", func(t *testing.T) {
tags := p.resolveTags([]string{}, "v1.0.0")
assert.Empty(t, tags)
})
t.Run("handles multiple version placeholders", func(t *testing.T) {
tags := p.resolveTags([]string{"{{.Version}}", "prefix-{{.Version}}", "{{.Version}}-suffix"}, "v1.2.3")
assert.Equal(t, []string{"v1.2.3", "prefix-v1.2.3", "v1.2.3-suffix"}, tags)
})
t.Run("handles mixed template formats", func(t *testing.T) {
tags := p.resolveTags([]string{"{{.Version}}", "{{Version}}", "latest"}, "v3.0.0")
assert.Equal(t, []string{"v3.0.0", "v3.0.0", "latest"}, tags)
})
}
func TestDockerPublisher_BuildBuildxArgs_EdgeCases_Good(t *testing.T) {
p := NewDockerPublisher()
t.Run("handles empty platforms", func(t *testing.T) {
cfg := DockerConfig{
Registry: "ghcr.io",
Image: "owner/repo",
Dockerfile: "/project/Dockerfile",
Platforms: []string{},
BuildArgs: make(map[string]string),
}
args := p.buildBuildxArgs(cfg, []string{"latest"}, "v1.0.0")
assert.Contains(t, args, "buildx")
assert.Contains(t, args, "build")
// Should not have --platform if empty
foundPlatform := false
for i, arg := range args {
if arg == "--platform" {
foundPlatform = true
// Check the next arg exists (it shouldn't be empty)
if i+1 < len(args) && args[i+1] == "" {
t.Error("platform argument should not be empty string")
}
}
}
assert.False(t, foundPlatform, "should not include --platform when platforms is empty")
})
t.Run("handles version expansion in build args", func(t *testing.T) {
cfg := DockerConfig{
Registry: "ghcr.io",
Image: "owner/repo",
Dockerfile: "/Dockerfile",
Platforms: []string{"linux/amd64"},
BuildArgs: map[string]string{
"VERSION": "{{.Version}}",
"SIMPLE_VER": "{{Version}}",
"STATIC_VALUE": "static",
},
}
args := p.buildBuildxArgs(cfg, []string{"latest"}, "v2.5.0")
foundVersionArg := false
foundSimpleArg := false
foundStaticArg := false
foundAutoVersion := false
for i, arg := range args {
if arg == "--build-arg" && i+1 < len(args) {
switch args[i+1] {
case "VERSION=v2.5.0":
foundVersionArg = true
case "SIMPLE_VER=v2.5.0":
foundSimpleArg = true
case "STATIC_VALUE=static":
foundStaticArg = true
}
// Auto-added VERSION build arg
if args[i+1] == "VERSION=v2.5.0" {
foundAutoVersion = true
}
}
}
// Note: VERSION is both in BuildArgs and auto-added, so we just check it exists
assert.True(t, foundVersionArg || foundAutoVersion, "VERSION build arg not found")
assert.True(t, foundSimpleArg, "SIMPLE_VER build arg not expanded")
assert.True(t, foundStaticArg, "STATIC_VALUE build arg not found")
})
t.Run("handles empty registry", func(t *testing.T) {
cfg := DockerConfig{
Registry: "",
Image: "localimage",
Dockerfile: "/Dockerfile",
Platforms: []string{"linux/amd64"},
BuildArgs: make(map[string]string),
}
args := p.buildBuildxArgs(cfg, []string{"latest"}, "v1.0.0")
assert.Contains(t, args, "-t")
assert.Contains(t, args, "localimage:latest")
})
}
func TestDockerPublisher_Publish_DryRun_Good(t *testing.T) {
// Skip if docker CLI is not available - dry run still validates docker is installed
if err := validateDockerCli(); err != nil {
t.Skip("skipping test: docker CLI not available")
}
p := NewDockerPublisher()
t.Run("dry run succeeds with valid Dockerfile", func(t *testing.T) {
// Create temp directory with Dockerfile
tmpDir, err := os.MkdirTemp("", "docker-test")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
err = os.WriteFile(dockerfilePath, []byte("FROM alpine:latest\n"), 0644)
require.NoError(t, err)
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
release := &Release{
Version: "v1.0.0",
ProjectDir: tmpDir,
}
pubCfg := PublisherConfig{Type: "docker"}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
_ = w.Close()
var buf bytes.Buffer
_, _ = buf.ReadFrom(r)
os.Stdout = oldStdout
require.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "DRY RUN: Docker Build & Push")
})
t.Run("dry run uses custom dockerfile path", func(t *testing.T) {
// Create temp directory with custom Dockerfile
tmpDir, err := os.MkdirTemp("", "docker-test")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
customDir := filepath.Join(tmpDir, "docker")
err = os.MkdirAll(customDir, 0755)
require.NoError(t, err)
dockerfilePath := filepath.Join(customDir, "Dockerfile.prod")
err = os.WriteFile(dockerfilePath, []byte("FROM alpine:latest\n"), 0644)
require.NoError(t, err)
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
release := &Release{
Version: "v1.0.0",
ProjectDir: tmpDir,
}
pubCfg := PublisherConfig{
Type: "docker",
Extended: map[string]any{
"dockerfile": "docker/Dockerfile.prod",
},
}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
_ = w.Close()
var buf bytes.Buffer
_, _ = buf.ReadFrom(r)
os.Stdout = oldStdout
require.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Dockerfile.prod")
})
}
func TestDockerPublisher_Publish_Validation_Bad(t *testing.T) {
p := NewDockerPublisher()
t.Run("fails when Dockerfile not found with docker installed", func(t *testing.T) {
if err := validateDockerCli(); err != nil {
t.Skip("skipping test: docker CLI not available")
}
release := &Release{
Version: "v1.0.0",
ProjectDir: "/nonexistent/path",
}
pubCfg := PublisherConfig{Type: "docker"}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
err := p.Publish(context.TODO(), release, pubCfg, relCfg, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Dockerfile not found")
})
t.Run("fails when docker CLI not available", func(t *testing.T) {
if err := validateDockerCli(); err == nil {
t.Skip("skipping test: docker CLI is available")
}
release := &Release{
Version: "v1.0.0",
ProjectDir: "/tmp",
}
pubCfg := PublisherConfig{Type: "docker"}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
err := p.Publish(context.TODO(), release, pubCfg, relCfg, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "docker CLI not found")
})
}
func TestValidateDockerCli_Good(t *testing.T) {
t.Run("returns nil when docker is installed", func(t *testing.T) {
err := validateDockerCli()
if err != nil {
// Docker is not installed, which is fine for this test
assert.Contains(t, err.Error(), "docker CLI not found")
}
// If err is nil, docker is installed - that's OK
})
}
func TestDockerPublisher_Publish_WithCLI_Good(t *testing.T) {
// These tests run only when docker CLI is available
if err := validateDockerCli(); err != nil {
t.Skip("skipping test: docker CLI not available")
}
p := NewDockerPublisher()
t.Run("dry run succeeds with all config options", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "docker-test")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
err = os.WriteFile(dockerfilePath, []byte("FROM alpine:latest\n"), 0644)
require.NoError(t, err)
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
release := &Release{
Version: "v1.0.0",
ProjectDir: tmpDir,
}
pubCfg := PublisherConfig{
Type: "docker",
Extended: map[string]any{
"registry": "docker.io",
"image": "myorg/myapp",
"platforms": []any{"linux/amd64", "linux/arm64"},
"tags": []any{"latest", "{{.Version}}", "stable"},
"build_args": map[string]any{"GO_VERSION": "1.21"},
},
}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
_ = w.Close()
var buf bytes.Buffer
_, _ = buf.ReadFrom(r)
os.Stdout = oldStdout
require.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "DRY RUN: Docker Build & Push")
assert.Contains(t, output, "docker.io")
assert.Contains(t, output, "myorg/myapp")
})
t.Run("dry run with nil relCfg uses extended image", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "docker-test")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
err = os.WriteFile(dockerfilePath, []byte("FROM alpine:latest\n"), 0644)
require.NoError(t, err)
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
release := &Release{
Version: "v1.0.0",
ProjectDir: tmpDir,
}
pubCfg := PublisherConfig{
Type: "docker",
Extended: map[string]any{
"image": "standalone/image",
},
}
err = p.Publish(context.TODO(), release, pubCfg, nil, true) // nil relCfg
_ = w.Close()
var buf bytes.Buffer
_, _ = buf.ReadFrom(r)
os.Stdout = oldStdout
require.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "standalone/image")
})
t.Run("fails with non-existent Dockerfile in non-dry-run", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "docker-test")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
// Don't create a Dockerfile
release := &Release{
Version: "v1.0.0",
ProjectDir: tmpDir,
}
pubCfg := PublisherConfig{Type: "docker"}
relCfg := &mockReleaseConfig{repository: "owner/repo"}
err = p.Publish(context.TODO(), release, pubCfg, relCfg, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Dockerfile not found")
})
}