feat(release): add LinuxKit and Docker publishers
LinuxKit publisher:
- Build images via linuxkit CLI (iso, qcow2, vmdk, raw)
- Multi-platform support (linux/amd64, linux/arm64)
- Upload artifacts to GitHub release
Docker publisher:
- Multi-arch builds via docker buildx
- Push to registry (default: ghcr.io)
- Tag templates with {{.Version}} expansion
- Build args support
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0f072ad353
commit
d25a86feca
8 changed files with 1134 additions and 5 deletions
|
|
@ -57,12 +57,32 @@ type TargetConfig struct {
|
|||
|
||||
// PublisherConfig holds configuration for a publisher.
|
||||
type PublisherConfig struct {
|
||||
// Type is the publisher type (e.g., "github").
|
||||
// Type is the publisher type (e.g., "github", "linuxkit", "docker").
|
||||
Type string `yaml:"type"`
|
||||
// Prerelease marks the release as a prerelease.
|
||||
Prerelease bool `yaml:"prerelease"`
|
||||
// Draft creates the release as a draft.
|
||||
Draft bool `yaml:"draft"`
|
||||
|
||||
// LinuxKit-specific configuration
|
||||
// Config is the path to the LinuxKit YAML configuration file.
|
||||
Config string `yaml:"config,omitempty"`
|
||||
// Formats are the output formats to build (iso, raw, qcow2, vmdk).
|
||||
Formats []string `yaml:"formats,omitempty"`
|
||||
// Platforms are the target platforms (linux/amd64, linux/arm64).
|
||||
Platforms []string `yaml:"platforms,omitempty"`
|
||||
|
||||
// Docker-specific configuration
|
||||
// Registry is the container registry (default: ghcr.io).
|
||||
Registry string `yaml:"registry,omitempty"`
|
||||
// Image is the image name in owner/repo format.
|
||||
Image string `yaml:"image,omitempty"`
|
||||
// Dockerfile is the path to the Dockerfile (default: Dockerfile).
|
||||
Dockerfile string `yaml:"dockerfile,omitempty"`
|
||||
// Tags are the image tags to apply.
|
||||
Tags []string `yaml:"tags,omitempty"`
|
||||
// BuildArgs are additional Docker build arguments.
|
||||
BuildArgs map[string]string `yaml:"build_args,omitempty"`
|
||||
}
|
||||
|
||||
// ChangelogConfig holds changelog generation settings.
|
||||
|
|
|
|||
278
pkg/release/publishers/docker.go
Normal file
278
pkg/release/publishers/docker.go
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
// Package publishers provides release publishing implementations.
|
||||
package publishers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DockerConfig holds configuration for the Docker publisher.
|
||||
type DockerConfig struct {
|
||||
// Registry is the container registry (default: ghcr.io).
|
||||
Registry string `yaml:"registry"`
|
||||
// Image is the image name in owner/repo format.
|
||||
Image string `yaml:"image"`
|
||||
// Dockerfile is the path to the Dockerfile (default: Dockerfile).
|
||||
Dockerfile string `yaml:"dockerfile"`
|
||||
// Platforms are the target platforms (linux/amd64, linux/arm64).
|
||||
Platforms []string `yaml:"platforms"`
|
||||
// Tags are additional tags to apply (supports {{.Version}} template).
|
||||
Tags []string `yaml:"tags"`
|
||||
// BuildArgs are additional build arguments.
|
||||
BuildArgs map[string]string `yaml:"build_args"`
|
||||
}
|
||||
|
||||
// DockerPublisher builds and publishes Docker images.
|
||||
type DockerPublisher struct{}
|
||||
|
||||
// NewDockerPublisher creates a new Docker publisher.
|
||||
func NewDockerPublisher() *DockerPublisher {
|
||||
return &DockerPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
func (p *DockerPublisher) Name() string {
|
||||
return "docker"
|
||||
}
|
||||
|
||||
// Publish builds and pushes Docker images.
|
||||
func (p *DockerPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
// Validate docker CLI is available
|
||||
if err := validateDockerCli(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse Docker-specific config from publisher config
|
||||
dockerCfg := p.parseConfig(pubCfg, relCfg, release.ProjectDir)
|
||||
|
||||
// Validate Dockerfile exists
|
||||
if _, err := os.Stat(dockerCfg.Dockerfile); err != nil {
|
||||
return fmt.Errorf("docker.Publish: Dockerfile not found: %s", dockerCfg.Dockerfile)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return p.dryRunPublish(release, dockerCfg)
|
||||
}
|
||||
|
||||
return p.executePublish(ctx, release, dockerCfg)
|
||||
}
|
||||
|
||||
// parseConfig extracts Docker-specific configuration.
|
||||
func (p *DockerPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConfig, projectDir string) DockerConfig {
|
||||
cfg := DockerConfig{
|
||||
Registry: "ghcr.io",
|
||||
Image: "",
|
||||
Dockerfile: filepath.Join(projectDir, "Dockerfile"),
|
||||
Platforms: []string{"linux/amd64", "linux/arm64"},
|
||||
Tags: []string{"latest", "{{.Version}}"},
|
||||
BuildArgs: make(map[string]string),
|
||||
}
|
||||
|
||||
// Try to get image from repository config
|
||||
if relCfg != nil && relCfg.GetRepository() != "" {
|
||||
cfg.Image = relCfg.GetRepository()
|
||||
}
|
||||
|
||||
// Override from extended config if present
|
||||
if ext, ok := pubCfg.Extended.(map[string]any); ok {
|
||||
if registry, ok := ext["registry"].(string); ok && registry != "" {
|
||||
cfg.Registry = registry
|
||||
}
|
||||
if image, ok := ext["image"].(string); ok && image != "" {
|
||||
cfg.Image = image
|
||||
}
|
||||
if dockerfile, ok := ext["dockerfile"].(string); ok && dockerfile != "" {
|
||||
if filepath.IsAbs(dockerfile) {
|
||||
cfg.Dockerfile = dockerfile
|
||||
} else {
|
||||
cfg.Dockerfile = filepath.Join(projectDir, dockerfile)
|
||||
}
|
||||
}
|
||||
if platforms, ok := ext["platforms"].([]any); ok && len(platforms) > 0 {
|
||||
cfg.Platforms = make([]string, 0, len(platforms))
|
||||
for _, plat := range platforms {
|
||||
if s, ok := plat.(string); ok {
|
||||
cfg.Platforms = append(cfg.Platforms, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if tags, ok := ext["tags"].([]any); ok && len(tags) > 0 {
|
||||
cfg.Tags = make([]string, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
if s, ok := tag.(string); ok {
|
||||
cfg.Tags = append(cfg.Tags, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if buildArgs, ok := ext["build_args"].(map[string]any); ok {
|
||||
for k, v := range buildArgs {
|
||||
if s, ok := v.(string); ok {
|
||||
cfg.BuildArgs[k] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// dryRunPublish shows what would be done without actually building.
|
||||
func (p *DockerPublisher) dryRunPublish(release *Release, cfg DockerConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: Docker Build & Push ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Version: %s\n", release.Version)
|
||||
fmt.Printf("Registry: %s\n", cfg.Registry)
|
||||
fmt.Printf("Image: %s\n", cfg.Image)
|
||||
fmt.Printf("Dockerfile: %s\n", cfg.Dockerfile)
|
||||
fmt.Printf("Platforms: %s\n", strings.Join(cfg.Platforms, ", "))
|
||||
fmt.Println()
|
||||
|
||||
// Resolve tags
|
||||
tags := p.resolveTags(cfg.Tags, release.Version)
|
||||
fmt.Println("Tags to be applied:")
|
||||
for _, tag := range tags {
|
||||
fullTag := p.buildFullTag(cfg.Registry, cfg.Image, tag)
|
||||
fmt.Printf(" - %s\n", fullTag)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("Would execute command:")
|
||||
args := p.buildBuildxArgs(cfg, tags, release.Version)
|
||||
fmt.Printf(" docker %s\n", strings.Join(args, " "))
|
||||
|
||||
if len(cfg.BuildArgs) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Println("Build arguments:")
|
||||
for k, v := range cfg.BuildArgs {
|
||||
fmt.Printf(" %s=%s\n", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executePublish builds and pushes Docker images.
|
||||
func (p *DockerPublisher) executePublish(ctx context.Context, release *Release, cfg DockerConfig) error {
|
||||
// Ensure buildx is available and builder is set up
|
||||
if err := p.ensureBuildx(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve tags
|
||||
tags := p.resolveTags(cfg.Tags, release.Version)
|
||||
|
||||
// Build the docker buildx command
|
||||
args := p.buildBuildxArgs(cfg, tags, release.Version)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "docker", args...)
|
||||
cmd.Dir = release.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
fmt.Printf("Building and pushing Docker image: %s\n", cfg.Image)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("docker.Publish: buildx build failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveTags expands template variables in tags.
|
||||
func (p *DockerPublisher) resolveTags(tags []string, version string) []string {
|
||||
resolved := make([]string, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
// Replace {{.Version}} with actual version
|
||||
resolvedTag := strings.ReplaceAll(tag, "{{.Version}}", version)
|
||||
// Also support simpler {{Version}} syntax
|
||||
resolvedTag = strings.ReplaceAll(resolvedTag, "{{Version}}", version)
|
||||
resolved = append(resolved, resolvedTag)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
// buildFullTag builds the full image tag including registry.
|
||||
func (p *DockerPublisher) buildFullTag(registry, image, tag string) string {
|
||||
if registry != "" {
|
||||
return fmt.Sprintf("%s/%s:%s", registry, image, tag)
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", image, tag)
|
||||
}
|
||||
|
||||
// buildBuildxArgs builds the arguments for docker buildx build command.
|
||||
func (p *DockerPublisher) buildBuildxArgs(cfg DockerConfig, tags []string, version string) []string {
|
||||
args := []string{"buildx", "build"}
|
||||
|
||||
// Multi-platform support
|
||||
if len(cfg.Platforms) > 0 {
|
||||
args = append(args, "--platform", strings.Join(cfg.Platforms, ","))
|
||||
}
|
||||
|
||||
// Add all tags
|
||||
for _, tag := range tags {
|
||||
fullTag := p.buildFullTag(cfg.Registry, cfg.Image, tag)
|
||||
args = append(args, "-t", fullTag)
|
||||
}
|
||||
|
||||
// Dockerfile path
|
||||
dockerfilePath := cfg.Dockerfile
|
||||
args = append(args, "-f", dockerfilePath)
|
||||
|
||||
// Build arguments
|
||||
for k, v := range cfg.BuildArgs {
|
||||
// Expand version in build args
|
||||
expandedValue := strings.ReplaceAll(v, "{{.Version}}", version)
|
||||
expandedValue = strings.ReplaceAll(expandedValue, "{{Version}}", version)
|
||||
args = append(args, "--build-arg", fmt.Sprintf("%s=%s", k, expandedValue))
|
||||
}
|
||||
|
||||
// Always add VERSION build arg
|
||||
args = append(args, "--build-arg", fmt.Sprintf("VERSION=%s", version))
|
||||
|
||||
// Push the image
|
||||
args = append(args, "--push")
|
||||
|
||||
// Build context (current directory)
|
||||
args = append(args, ".")
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// ensureBuildx ensures docker buildx is available and has a builder.
|
||||
func (p *DockerPublisher) ensureBuildx(ctx context.Context) error {
|
||||
// Check if buildx is available
|
||||
cmd := exec.CommandContext(ctx, "docker", "buildx", "version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("docker: buildx is not available. Install it from https://docs.docker.com/buildx/working-with-buildx/")
|
||||
}
|
||||
|
||||
// Check if we have a builder, create one if not
|
||||
cmd = exec.CommandContext(ctx, "docker", "buildx", "inspect", "--bootstrap")
|
||||
if err := cmd.Run(); err != nil {
|
||||
// Try to create a builder
|
||||
cmd = exec.CommandContext(ctx, "docker", "buildx", "create", "--use", "--bootstrap")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("docker: failed to create buildx builder: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDockerCli checks if the docker CLI is available.
|
||||
func validateDockerCli() error {
|
||||
cmd := exec.Command("docker", "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("docker: docker CLI not found. Install it from https://docs.docker.com/get-docker/")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
267
pkg/release/publishers/docker_test.go
Normal file
267
pkg/release/publishers/docker_test.go
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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(nil, 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")
|
||||
})
|
||||
}
|
||||
|
|
@ -93,10 +93,18 @@ func TestNewRelease_Good(t *testing.T) {
|
|||
|
||||
func TestNewPublisherConfig_Good(t *testing.T) {
|
||||
t.Run("creates config struct", func(t *testing.T) {
|
||||
cfg := NewPublisherConfig("github", true, false)
|
||||
cfg := NewPublisherConfig("github", true, false, nil)
|
||||
assert.Equal(t, "github", cfg.Type)
|
||||
assert.True(t, cfg.Prerelease)
|
||||
assert.False(t, cfg.Draft)
|
||||
assert.Nil(t, cfg.Extended)
|
||||
})
|
||||
|
||||
t.Run("creates config with extended", func(t *testing.T) {
|
||||
ext := map[string]any{"key": "value"}
|
||||
cfg := NewPublisherConfig("docker", false, false, ext)
|
||||
assert.Equal(t, "docker", cfg.Type)
|
||||
assert.Equal(t, ext, cfg.Extended)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
282
pkg/release/publishers/linuxkit.go
Normal file
282
pkg/release/publishers/linuxkit.go
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
// Package publishers provides release publishing implementations.
|
||||
package publishers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LinuxKitConfig holds configuration for the LinuxKit publisher.
|
||||
type LinuxKitConfig struct {
|
||||
// Config is the path to the LinuxKit YAML configuration file.
|
||||
Config string `yaml:"config"`
|
||||
// Formats are the output formats to build (iso, raw, qcow2, vmdk).
|
||||
Formats []string `yaml:"formats"`
|
||||
// Platforms are the target platforms (linux/amd64, linux/arm64).
|
||||
Platforms []string `yaml:"platforms"`
|
||||
}
|
||||
|
||||
// LinuxKitPublisher builds and publishes LinuxKit images.
|
||||
type LinuxKitPublisher struct{}
|
||||
|
||||
// NewLinuxKitPublisher creates a new LinuxKit publisher.
|
||||
func NewLinuxKitPublisher() *LinuxKitPublisher {
|
||||
return &LinuxKitPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
func (p *LinuxKitPublisher) Name() string {
|
||||
return "linuxkit"
|
||||
}
|
||||
|
||||
// Publish builds LinuxKit images and uploads them to the GitHub release.
|
||||
func (p *LinuxKitPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
// Validate linuxkit CLI is available
|
||||
if err := validateLinuxKitCli(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse LinuxKit-specific config from publisher config
|
||||
lkCfg := p.parseConfig(pubCfg, release.ProjectDir)
|
||||
|
||||
// Validate config file exists
|
||||
if _, err := os.Stat(lkCfg.Config); err != nil {
|
||||
return fmt.Errorf("linuxkit.Publish: config file not found: %s", lkCfg.Config)
|
||||
}
|
||||
|
||||
// Determine repository for artifact upload
|
||||
repo := ""
|
||||
if relCfg != nil {
|
||||
repo = relCfg.GetRepository()
|
||||
}
|
||||
if repo == "" {
|
||||
detectedRepo, err := detectRepository(release.ProjectDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("linuxkit.Publish: could not determine repository: %w", err)
|
||||
}
|
||||
repo = detectedRepo
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return p.dryRunPublish(release, lkCfg, repo)
|
||||
}
|
||||
|
||||
return p.executePublish(ctx, release, lkCfg, repo)
|
||||
}
|
||||
|
||||
// parseConfig extracts LinuxKit-specific configuration.
|
||||
func (p *LinuxKitPublisher) parseConfig(pubCfg PublisherConfig, projectDir string) LinuxKitConfig {
|
||||
cfg := LinuxKitConfig{
|
||||
Config: filepath.Join(projectDir, ".core", "linuxkit", "server.yml"),
|
||||
Formats: []string{"iso"},
|
||||
Platforms: []string{"linux/amd64"},
|
||||
}
|
||||
|
||||
// Override from extended config if present
|
||||
if ext, ok := pubCfg.Extended.(map[string]any); ok {
|
||||
if configPath, ok := ext["config"].(string); ok && configPath != "" {
|
||||
if filepath.IsAbs(configPath) {
|
||||
cfg.Config = configPath
|
||||
} else {
|
||||
cfg.Config = filepath.Join(projectDir, configPath)
|
||||
}
|
||||
}
|
||||
if formats, ok := ext["formats"].([]any); ok && len(formats) > 0 {
|
||||
cfg.Formats = make([]string, 0, len(formats))
|
||||
for _, f := range formats {
|
||||
if s, ok := f.(string); ok {
|
||||
cfg.Formats = append(cfg.Formats, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if platforms, ok := ext["platforms"].([]any); ok && len(platforms) > 0 {
|
||||
cfg.Platforms = make([]string, 0, len(platforms))
|
||||
for _, p := range platforms {
|
||||
if s, ok := p.(string); ok {
|
||||
cfg.Platforms = append(cfg.Platforms, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// dryRunPublish shows what would be done without actually building.
|
||||
func (p *LinuxKitPublisher) dryRunPublish(release *Release, cfg LinuxKitConfig, repo string) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: LinuxKit Build & Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Repository: %s\n", repo)
|
||||
fmt.Printf("Version: %s\n", release.Version)
|
||||
fmt.Printf("Config: %s\n", cfg.Config)
|
||||
fmt.Printf("Formats: %s\n", strings.Join(cfg.Formats, ", "))
|
||||
fmt.Printf("Platforms: %s\n", strings.Join(cfg.Platforms, ", "))
|
||||
fmt.Println()
|
||||
|
||||
outputDir := filepath.Join(release.ProjectDir, "dist", "linuxkit")
|
||||
baseName := p.buildBaseName(release.Version)
|
||||
|
||||
fmt.Println("Would execute commands:")
|
||||
for _, platform := range cfg.Platforms {
|
||||
parts := strings.Split(platform, "/")
|
||||
arch := "amd64"
|
||||
if len(parts) == 2 {
|
||||
arch = parts[1]
|
||||
}
|
||||
|
||||
for _, format := range cfg.Formats {
|
||||
outputName := fmt.Sprintf("%s-%s", baseName, arch)
|
||||
args := p.buildLinuxKitArgs(cfg.Config, format, outputName, outputDir, arch)
|
||||
fmt.Printf(" linuxkit %s\n", strings.Join(args, " "))
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("Would upload artifacts to release:")
|
||||
for _, platform := range cfg.Platforms {
|
||||
parts := strings.Split(platform, "/")
|
||||
arch := "amd64"
|
||||
if len(parts) == 2 {
|
||||
arch = parts[1]
|
||||
}
|
||||
|
||||
for _, format := range cfg.Formats {
|
||||
outputName := fmt.Sprintf("%s-%s", baseName, arch)
|
||||
artifactPath := p.getArtifactPath(outputDir, outputName, format)
|
||||
fmt.Printf(" - %s\n", filepath.Base(artifactPath))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executePublish builds LinuxKit images and uploads them.
|
||||
func (p *LinuxKitPublisher) executePublish(ctx context.Context, release *Release, cfg LinuxKitConfig, repo string) error {
|
||||
outputDir := filepath.Join(release.ProjectDir, "dist", "linuxkit")
|
||||
|
||||
// Create output directory
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("linuxkit.Publish: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
baseName := p.buildBaseName(release.Version)
|
||||
var artifacts []string
|
||||
|
||||
// Build for each platform and format
|
||||
for _, platform := range cfg.Platforms {
|
||||
parts := strings.Split(platform, "/")
|
||||
arch := "amd64"
|
||||
if len(parts) == 2 {
|
||||
arch = parts[1]
|
||||
}
|
||||
|
||||
for _, format := range cfg.Formats {
|
||||
outputName := fmt.Sprintf("%s-%s", baseName, arch)
|
||||
|
||||
// Build the image
|
||||
args := p.buildLinuxKitArgs(cfg.Config, format, outputName, outputDir, arch)
|
||||
cmd := exec.CommandContext(ctx, "linuxkit", args...)
|
||||
cmd.Dir = release.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
fmt.Printf("Building LinuxKit image: %s (%s)\n", outputName, format)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("linuxkit.Publish: build failed for %s/%s: %w", platform, format, err)
|
||||
}
|
||||
|
||||
// Track artifact for upload
|
||||
artifactPath := p.getArtifactPath(outputDir, outputName, format)
|
||||
artifacts = append(artifacts, artifactPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Upload artifacts to GitHub release
|
||||
for _, artifactPath := range artifacts {
|
||||
if _, err := os.Stat(artifactPath); err != nil {
|
||||
return fmt.Errorf("linuxkit.Publish: artifact not found after build: %s", artifactPath)
|
||||
}
|
||||
|
||||
if err := UploadArtifact(ctx, repo, release.Version, artifactPath); err != nil {
|
||||
return fmt.Errorf("linuxkit.Publish: failed to upload %s: %w", filepath.Base(artifactPath), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildBaseName creates the base name for output files.
|
||||
func (p *LinuxKitPublisher) buildBaseName(version string) string {
|
||||
// Strip leading 'v' if present for cleaner filenames
|
||||
name := strings.TrimPrefix(version, "v")
|
||||
return fmt.Sprintf("linuxkit-%s", name)
|
||||
}
|
||||
|
||||
// buildLinuxKitArgs builds the arguments for linuxkit build command.
|
||||
func (p *LinuxKitPublisher) buildLinuxKitArgs(configPath, format, outputName, outputDir, arch string) []string {
|
||||
args := []string{"build"}
|
||||
|
||||
// Output format
|
||||
args = append(args, "-format", format)
|
||||
|
||||
// Output name
|
||||
args = append(args, "-name", outputName)
|
||||
|
||||
// Output directory
|
||||
args = append(args, "-dir", outputDir)
|
||||
|
||||
// Architecture (if not amd64)
|
||||
if arch != "amd64" {
|
||||
args = append(args, "-arch", arch)
|
||||
}
|
||||
|
||||
// Config file
|
||||
args = append(args, configPath)
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// getArtifactPath returns the expected path of the built artifact.
|
||||
func (p *LinuxKitPublisher) getArtifactPath(outputDir, outputName, format string) string {
|
||||
ext := p.getFormatExtension(format)
|
||||
return filepath.Join(outputDir, outputName+ext)
|
||||
}
|
||||
|
||||
// getFormatExtension returns the file extension for a LinuxKit output format.
|
||||
func (p *LinuxKitPublisher) getFormatExtension(format string) string {
|
||||
switch format {
|
||||
case "iso":
|
||||
return ".iso"
|
||||
case "raw":
|
||||
return ".raw"
|
||||
case "qcow2":
|
||||
return ".qcow2"
|
||||
case "vmdk":
|
||||
return ".vmdk"
|
||||
case "vhd":
|
||||
return ".vhd"
|
||||
case "gcp":
|
||||
return ".img.tar.gz"
|
||||
case "aws":
|
||||
return ".raw"
|
||||
default:
|
||||
return "." + format
|
||||
}
|
||||
}
|
||||
|
||||
// validateLinuxKitCli checks if the linuxkit CLI is available.
|
||||
func validateLinuxKitCli() error {
|
||||
cmd := exec.Command("linuxkit", "version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("linuxkit: linuxkit CLI not found. Install it from https://github.com/linuxkit/linuxkit")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
217
pkg/release/publishers/linuxkit_test.go
Normal file
217
pkg/release/publishers/linuxkit_test.go
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLinuxKitPublisher_Name_Good(t *testing.T) {
|
||||
t.Run("returns linuxkit", func(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
assert.Equal(t, "linuxkit", p.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_ParseConfig_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||
pubCfg := PublisherConfig{Type: "linuxkit"}
|
||||
cfg := p.parseConfig(pubCfg, "/project")
|
||||
|
||||
assert.Equal(t, "/project/.core/linuxkit/server.yml", cfg.Config)
|
||||
assert.Equal(t, []string{"iso"}, cfg.Formats)
|
||||
assert.Equal(t, []string{"linux/amd64"}, cfg.Platforms)
|
||||
})
|
||||
|
||||
t.Run("parses extended config", func(t *testing.T) {
|
||||
pubCfg := PublisherConfig{
|
||||
Type: "linuxkit",
|
||||
Extended: map[string]any{
|
||||
"config": ".core/linuxkit/custom.yml",
|
||||
"formats": []any{"iso", "qcow2", "vmdk"},
|
||||
"platforms": []any{"linux/amd64", "linux/arm64"},
|
||||
},
|
||||
}
|
||||
cfg := p.parseConfig(pubCfg, "/project")
|
||||
|
||||
assert.Equal(t, "/project/.core/linuxkit/custom.yml", cfg.Config)
|
||||
assert.Equal(t, []string{"iso", "qcow2", "vmdk"}, cfg.Formats)
|
||||
assert.Equal(t, []string{"linux/amd64", "linux/arm64"}, cfg.Platforms)
|
||||
})
|
||||
|
||||
t.Run("handles absolute config path", func(t *testing.T) {
|
||||
pubCfg := PublisherConfig{
|
||||
Type: "linuxkit",
|
||||
Extended: map[string]any{
|
||||
"config": "/absolute/path/to/config.yml",
|
||||
},
|
||||
}
|
||||
cfg := p.parseConfig(pubCfg, "/project")
|
||||
|
||||
assert.Equal(t, "/absolute/path/to/config.yml", cfg.Config)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_BuildLinuxKitArgs_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("builds basic args for amd64", func(t *testing.T) {
|
||||
args := p.buildLinuxKitArgs("/config/server.yml", "iso", "linuxkit-1.0.0-amd64", "/output", "amd64")
|
||||
|
||||
assert.Contains(t, args, "build")
|
||||
assert.Contains(t, args, "-format")
|
||||
assert.Contains(t, args, "iso")
|
||||
assert.Contains(t, args, "-name")
|
||||
assert.Contains(t, args, "linuxkit-1.0.0-amd64")
|
||||
assert.Contains(t, args, "-dir")
|
||||
assert.Contains(t, args, "/output")
|
||||
assert.Contains(t, args, "/config/server.yml")
|
||||
// Should not contain -arch for amd64 (default)
|
||||
assert.NotContains(t, args, "-arch")
|
||||
})
|
||||
|
||||
t.Run("builds args with arch for arm64", func(t *testing.T) {
|
||||
args := p.buildLinuxKitArgs("/config/server.yml", "qcow2", "linuxkit-1.0.0-arm64", "/output", "arm64")
|
||||
|
||||
assert.Contains(t, args, "-arch")
|
||||
assert.Contains(t, args, "arm64")
|
||||
assert.Contains(t, args, "qcow2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_BuildBaseName_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("strips v prefix", func(t *testing.T) {
|
||||
name := p.buildBaseName("v1.2.3")
|
||||
assert.Equal(t, "linuxkit-1.2.3", name)
|
||||
})
|
||||
|
||||
t.Run("handles version without v prefix", func(t *testing.T) {
|
||||
name := p.buildBaseName("1.2.3")
|
||||
assert.Equal(t, "linuxkit-1.2.3", name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_GetArtifactPath_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
outputDir string
|
||||
outputName string
|
||||
format string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "ISO format",
|
||||
outputDir: "/dist/linuxkit",
|
||||
outputName: "linuxkit-1.0.0-amd64",
|
||||
format: "iso",
|
||||
expected: "/dist/linuxkit/linuxkit-1.0.0-amd64.iso",
|
||||
},
|
||||
{
|
||||
name: "raw format",
|
||||
outputDir: "/dist/linuxkit",
|
||||
outputName: "linuxkit-1.0.0-amd64",
|
||||
format: "raw",
|
||||
expected: "/dist/linuxkit/linuxkit-1.0.0-amd64.raw",
|
||||
},
|
||||
{
|
||||
name: "qcow2 format",
|
||||
outputDir: "/dist/linuxkit",
|
||||
outputName: "linuxkit-1.0.0-arm64",
|
||||
format: "qcow2",
|
||||
expected: "/dist/linuxkit/linuxkit-1.0.0-arm64.qcow2",
|
||||
},
|
||||
{
|
||||
name: "vmdk format",
|
||||
outputDir: "/dist/linuxkit",
|
||||
outputName: "linuxkit-1.0.0-amd64",
|
||||
format: "vmdk",
|
||||
expected: "/dist/linuxkit/linuxkit-1.0.0-amd64.vmdk",
|
||||
},
|
||||
{
|
||||
name: "gcp format",
|
||||
outputDir: "/dist/linuxkit",
|
||||
outputName: "linuxkit-1.0.0-amd64",
|
||||
format: "gcp",
|
||||
expected: "/dist/linuxkit/linuxkit-1.0.0-amd64.img.tar.gz",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
path := p.getArtifactPath(tc.outputDir, tc.outputName, tc.format)
|
||||
assert.Equal(t, tc.expected, path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_GetFormatExtension_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
tests := []struct {
|
||||
format string
|
||||
expected string
|
||||
}{
|
||||
{"iso", ".iso"},
|
||||
{"raw", ".raw"},
|
||||
{"qcow2", ".qcow2"},
|
||||
{"vmdk", ".vmdk"},
|
||||
{"vhd", ".vhd"},
|
||||
{"gcp", ".img.tar.gz"},
|
||||
{"aws", ".raw"},
|
||||
{"unknown", ".unknown"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.format, func(t *testing.T) {
|
||||
ext := p.getFormatExtension(tc.format)
|
||||
assert.Equal(t, tc.expected, ext)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_Publish_Bad(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("fails when config file not found", func(t *testing.T) {
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: "/nonexistent",
|
||||
}
|
||||
pubCfg := PublisherConfig{
|
||||
Type: "linuxkit",
|
||||
Extended: map[string]any{
|
||||
"config": "/nonexistent/config.yml",
|
||||
},
|
||||
}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||
|
||||
// Note: This test requires linuxkit to NOT be installed
|
||||
// or uses a non-existent config path
|
||||
err := p.Publish(nil, release, pubCfg, relCfg, false)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// mockReleaseConfig implements ReleaseConfig for testing.
|
||||
type mockReleaseConfig struct {
|
||||
repository string
|
||||
projectName string
|
||||
}
|
||||
|
||||
func (m *mockReleaseConfig) GetRepository() string {
|
||||
return m.repository
|
||||
}
|
||||
|
||||
func (m *mockReleaseConfig) GetProjectName() string {
|
||||
return m.projectName
|
||||
}
|
||||
|
|
@ -21,12 +21,14 @@ type Release struct {
|
|||
|
||||
// PublisherConfig holds configuration for a publisher.
|
||||
type PublisherConfig struct {
|
||||
// Type is the publisher type (e.g., "github").
|
||||
// Type is the publisher type (e.g., "github", "linuxkit", "docker").
|
||||
Type string
|
||||
// Prerelease marks the release as a prerelease.
|
||||
Prerelease bool
|
||||
// Draft creates the release as a draft.
|
||||
Draft bool
|
||||
// Extended holds publisher-specific configuration.
|
||||
Extended any
|
||||
}
|
||||
|
||||
// ReleaseConfig holds release configuration needed by publishers.
|
||||
|
|
@ -56,10 +58,11 @@ func NewRelease(version string, artifacts []build.Artifact, changelog, projectDi
|
|||
}
|
||||
|
||||
// NewPublisherConfig creates a PublisherConfig.
|
||||
func NewPublisherConfig(pubType string, prerelease, draft bool) PublisherConfig {
|
||||
func NewPublisherConfig(pubType string, prerelease, draft bool, extended any) PublisherConfig {
|
||||
return PublisherConfig{
|
||||
Type: pubType,
|
||||
Prerelease: prerelease,
|
||||
Draft: draft,
|
||||
Extended: extended,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,9 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
|
|||
return release, fmt.Errorf("release.Run: %w", err)
|
||||
}
|
||||
|
||||
publisherCfg := publishers.NewPublisherConfig(pubCfg.Type, pubCfg.Prerelease, pubCfg.Draft)
|
||||
// Build extended config for publisher-specific settings
|
||||
extendedCfg := buildExtendedConfig(pubCfg)
|
||||
publisherCfg := publishers.NewPublisherConfig(pubCfg.Type, pubCfg.Prerelease, pubCfg.Draft, extendedCfg)
|
||||
if err := publisher.Publish(ctx, pubRelease, publisherCfg, cfg, dryRun); err != nil {
|
||||
return release, fmt.Errorf("release.Run: publish to %s failed: %w", pubCfg.Type, err)
|
||||
}
|
||||
|
|
@ -210,7 +212,59 @@ func getPublisher(pubType string) (publishers.Publisher, error) {
|
|||
switch pubType {
|
||||
case "github":
|
||||
return publishers.NewGitHubPublisher(), nil
|
||||
case "linuxkit":
|
||||
return publishers.NewLinuxKitPublisher(), nil
|
||||
case "docker":
|
||||
return publishers.NewDockerPublisher(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported publisher type: %s", pubType)
|
||||
}
|
||||
}
|
||||
|
||||
// buildExtendedConfig builds a map of extended configuration for a publisher.
|
||||
func buildExtendedConfig(pubCfg PublisherConfig) map[string]any {
|
||||
ext := make(map[string]any)
|
||||
|
||||
// LinuxKit-specific config
|
||||
if pubCfg.Config != "" {
|
||||
ext["config"] = pubCfg.Config
|
||||
}
|
||||
if len(pubCfg.Formats) > 0 {
|
||||
ext["formats"] = toAnySlice(pubCfg.Formats)
|
||||
}
|
||||
if len(pubCfg.Platforms) > 0 {
|
||||
ext["platforms"] = toAnySlice(pubCfg.Platforms)
|
||||
}
|
||||
|
||||
// Docker-specific config
|
||||
if pubCfg.Registry != "" {
|
||||
ext["registry"] = pubCfg.Registry
|
||||
}
|
||||
if pubCfg.Image != "" {
|
||||
ext["image"] = pubCfg.Image
|
||||
}
|
||||
if pubCfg.Dockerfile != "" {
|
||||
ext["dockerfile"] = pubCfg.Dockerfile
|
||||
}
|
||||
if len(pubCfg.Tags) > 0 {
|
||||
ext["tags"] = toAnySlice(pubCfg.Tags)
|
||||
}
|
||||
if len(pubCfg.BuildArgs) > 0 {
|
||||
args := make(map[string]any)
|
||||
for k, v := range pubCfg.BuildArgs {
|
||||
args[k] = v
|
||||
}
|
||||
ext["build_args"] = args
|
||||
}
|
||||
|
||||
return ext
|
||||
}
|
||||
|
||||
// toAnySlice converts a string slice to an any slice.
|
||||
func toAnySlice(s []string) []any {
|
||||
result := make([]any, len(s))
|
||||
for i, v := range s {
|
||||
result[i] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue