cli/pkg/release/config.go
Snider d25a86feca 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>
2026-01-28 18:42:04 +00:00

228 lines
6.7 KiB
Go

// Package release provides release automation with changelog generation and publishing.
package release
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// ConfigFileName is the name of the release configuration file.
const ConfigFileName = "release.yaml"
// ConfigDir is the directory where release configuration is stored.
const ConfigDir = ".core"
// Config holds the complete release configuration loaded from .core/release.yaml.
type Config struct {
// Version is the config file format version.
Version int `yaml:"version"`
// Project contains project metadata.
Project ProjectConfig `yaml:"project"`
// Build contains build settings for the release.
Build BuildConfig `yaml:"build"`
// Publishers defines where to publish the release.
Publishers []PublisherConfig `yaml:"publishers"`
// Changelog configures changelog generation.
Changelog ChangelogConfig `yaml:"changelog"`
// Internal fields (not serialized)
projectDir string // Set by LoadConfig
version string // Set by CLI flag
}
// ProjectConfig holds project metadata for releases.
type ProjectConfig struct {
// Name is the project name.
Name string `yaml:"name"`
// Repository is the GitHub repository in owner/repo format.
Repository string `yaml:"repository"`
}
// BuildConfig holds build settings for releases.
type BuildConfig struct {
// Targets defines the build targets.
Targets []TargetConfig `yaml:"targets"`
}
// TargetConfig defines a build target.
type TargetConfig struct {
// OS is the target operating system (e.g., "linux", "darwin", "windows").
OS string `yaml:"os"`
// Arch is the target architecture (e.g., "amd64", "arm64").
Arch string `yaml:"arch"`
}
// PublisherConfig holds configuration for a publisher.
type PublisherConfig struct {
// 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.
type ChangelogConfig struct {
// Include specifies commit types to include in the changelog.
Include []string `yaml:"include"`
// Exclude specifies commit types to exclude from the changelog.
Exclude []string `yaml:"exclude"`
}
// LoadConfig loads release configuration from the .core/release.yaml file in the given directory.
// If the config file does not exist, it returns DefaultConfig().
// Returns an error if the file exists but cannot be parsed.
func LoadConfig(dir string) (*Config, error) {
configPath := filepath.Join(dir, ConfigDir, ConfigFileName)
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
cfg := DefaultConfig()
cfg.projectDir = dir
return cfg, nil
}
return nil, fmt.Errorf("release.LoadConfig: failed to read config file: %w", err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("release.LoadConfig: failed to parse config file: %w", err)
}
// Apply defaults for any missing fields
applyDefaults(&cfg)
cfg.projectDir = dir
return &cfg, nil
}
// DefaultConfig returns sensible defaults for release configuration.
func DefaultConfig() *Config {
return &Config{
Version: 1,
Project: ProjectConfig{
Name: "",
Repository: "",
},
Build: BuildConfig{
Targets: []TargetConfig{
{OS: "linux", Arch: "amd64"},
{OS: "linux", Arch: "arm64"},
{OS: "darwin", Arch: "amd64"},
{OS: "darwin", Arch: "arm64"},
{OS: "windows", Arch: "amd64"},
},
},
Publishers: []PublisherConfig{
{
Type: "github",
Prerelease: false,
Draft: false,
},
},
Changelog: ChangelogConfig{
Include: []string{"feat", "fix", "perf", "refactor"},
Exclude: []string{"chore", "docs", "style", "test", "ci"},
},
}
}
// applyDefaults fills in default values for any empty fields in the config.
func applyDefaults(cfg *Config) {
defaults := DefaultConfig()
if cfg.Version == 0 {
cfg.Version = defaults.Version
}
if len(cfg.Build.Targets) == 0 {
cfg.Build.Targets = defaults.Build.Targets
}
if len(cfg.Publishers) == 0 {
cfg.Publishers = defaults.Publishers
}
if len(cfg.Changelog.Include) == 0 && len(cfg.Changelog.Exclude) == 0 {
cfg.Changelog.Include = defaults.Changelog.Include
cfg.Changelog.Exclude = defaults.Changelog.Exclude
}
}
// SetProjectDir sets the project directory on the config.
func (c *Config) SetProjectDir(dir string) {
c.projectDir = dir
}
// SetVersion sets the version override on the config.
func (c *Config) SetVersion(version string) {
c.version = version
}
// ConfigPath returns the path to the release config file for a given directory.
func ConfigPath(dir string) string {
return filepath.Join(dir, ConfigDir, ConfigFileName)
}
// ConfigExists checks if a release config file exists in the given directory.
func ConfigExists(dir string) bool {
_, err := os.Stat(ConfigPath(dir))
return err == nil
}
// GetRepository returns the repository from the config.
func (c *Config) GetRepository() string {
return c.Project.Repository
}
// GetProjectName returns the project name from the config.
func (c *Config) GetProjectName() string {
return c.Project.Name
}
// WriteConfig writes the config to the .core/release.yaml file.
func WriteConfig(cfg *Config, dir string) error {
configPath := ConfigPath(dir)
// Ensure directory exists
configDir := filepath.Dir(configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("release.WriteConfig: failed to create directory: %w", err)
}
data, err := yaml.Marshal(cfg)
if err != nil {
return fmt.Errorf("release.WriteConfig: failed to marshal config: %w", err)
}
if err := os.WriteFile(configPath, data, 0644); err != nil {
return fmt.Errorf("release.WriteConfig: failed to write config file: %w", err)
}
return nil
}