Add pkg/release package for automated releases: - Config loading from .core/release.yaml - Version detection from git tags with auto-increment - Changelog generation from conventional commits - GitHub publisher using gh CLI CLI commands: - core release - build + publish to GitHub - core release --dry-run - preview without publishing - core release init - interactive config setup - core release changelog - generate changelog - core release version - show/set version Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
208 lines
5.8 KiB
Go
208 lines
5.8 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").
|
|
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"`
|
|
}
|
|
|
|
// 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
|
|
}
|