Merge branch 'fix/io-migration-release' into new

# Conflicts:
#	pkg/release/config.go
#	pkg/release/release.go
This commit is contained in:
Snider 2026-02-08 21:28:43 +00:00
commit 948dd16761
2 changed files with 90 additions and 90 deletions

View file

@ -3,9 +3,9 @@ package release
import (
"fmt"
"os"
"path/filepath"
"github.com/host-uk/core/pkg/config"
"github.com/host-uk/core/pkg/io"
"gopkg.in/yaml.v3"
)
@ -19,17 +19,17 @@ 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" mapstructure:"version"`
Version int `yaml:"version"`
// Project contains project metadata.
Project ProjectConfig `yaml:"project" mapstructure:"project"`
Project ProjectConfig `yaml:"project"`
// Build contains build settings for the release.
Build BuildConfig `yaml:"build" mapstructure:"build"`
Build BuildConfig `yaml:"build"`
// Publishers defines where to publish the release.
Publishers []PublisherConfig `yaml:"publishers" mapstructure:"publishers"`
Publishers []PublisherConfig `yaml:"publishers"`
// Changelog configures changelog generation.
Changelog ChangelogConfig `yaml:"changelog" mapstructure:"changelog"`
Changelog ChangelogConfig `yaml:"changelog"`
// SDK configures SDK generation.
SDK *SDKConfig `yaml:"sdk,omitempty" mapstructure:"sdk,omitempty"`
SDK *SDKConfig `yaml:"sdk,omitempty"`
// Internal fields (not serialized)
projectDir string // Set by LoadConfig
@ -39,165 +39,165 @@ type Config struct {
// ProjectConfig holds project metadata for releases.
type ProjectConfig struct {
// Name is the project name.
Name string `yaml:"name" mapstructure:"name"`
Name string `yaml:"name"`
// Repository is the GitHub repository in owner/repo format.
Repository string `yaml:"repository" mapstructure:"repository"`
Repository string `yaml:"repository"`
}
// BuildConfig holds build settings for releases.
type BuildConfig struct {
// Targets defines the build targets.
Targets []TargetConfig `yaml:"targets" mapstructure:"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" mapstructure:"os"`
OS string `yaml:"os"`
// Arch is the target architecture (e.g., "amd64", "arm64").
Arch string `yaml:"arch" mapstructure:"arch"`
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" mapstructure:"type"`
Type string `yaml:"type"`
// Prerelease marks the release as a prerelease.
Prerelease bool `yaml:"prerelease" mapstructure:"prerelease"`
Prerelease bool `yaml:"prerelease"`
// Draft creates the release as a draft.
Draft bool `yaml:"draft" mapstructure:"draft"`
Draft bool `yaml:"draft"`
// LinuxKit-specific configuration
// Config is the path to the LinuxKit YAML configuration file.
Config string `yaml:"config,omitempty" mapstructure:"config,omitempty"`
Config string `yaml:"config,omitempty"`
// Formats are the output formats to build (iso, raw, qcow2, vmdk).
Formats []string `yaml:"formats,omitempty" mapstructure:"formats,omitempty"`
Formats []string `yaml:"formats,omitempty"`
// Platforms are the target platforms (linux/amd64, linux/arm64).
Platforms []string `yaml:"platforms,omitempty" mapstructure:"platforms,omitempty"`
Platforms []string `yaml:"platforms,omitempty"`
// Docker-specific configuration
// Registry is the container registry (default: ghcr.io).
Registry string `yaml:"registry,omitempty" mapstructure:"registry,omitempty"`
Registry string `yaml:"registry,omitempty"`
// Image is the image name in owner/repo format.
Image string `yaml:"image,omitempty" mapstructure:"image,omitempty"`
Image string `yaml:"image,omitempty"`
// Dockerfile is the path to the Dockerfile (default: Dockerfile).
Dockerfile string `yaml:"dockerfile,omitempty" mapstructure:"dockerfile,omitempty"`
Dockerfile string `yaml:"dockerfile,omitempty"`
// Tags are the image tags to apply.
Tags []string `yaml:"tags,omitempty" mapstructure:"tags,omitempty"`
Tags []string `yaml:"tags,omitempty"`
// BuildArgs are additional Docker build arguments.
BuildArgs map[string]string `yaml:"build_args,omitempty" mapstructure:"build_args,omitempty"`
BuildArgs map[string]string `yaml:"build_args,omitempty"`
// npm-specific configuration
// Package is the npm package name (e.g., "@host-uk/core").
Package string `yaml:"package,omitempty" mapstructure:"package,omitempty"`
Package string `yaml:"package,omitempty"`
// Access is the npm access level: "public" or "restricted".
Access string `yaml:"access,omitempty" mapstructure:"access,omitempty"`
Access string `yaml:"access,omitempty"`
// Homebrew-specific configuration
// Tap is the Homebrew tap repository (e.g., "host-uk/homebrew-tap").
Tap string `yaml:"tap,omitempty" mapstructure:"tap,omitempty"`
Tap string `yaml:"tap,omitempty"`
// Formula is the formula name (defaults to project name).
Formula string `yaml:"formula,omitempty" mapstructure:"formula,omitempty"`
Formula string `yaml:"formula,omitempty"`
// Scoop-specific configuration
// Bucket is the Scoop bucket repository (e.g., "host-uk/scoop-bucket").
Bucket string `yaml:"bucket,omitempty" mapstructure:"bucket,omitempty"`
Bucket string `yaml:"bucket,omitempty"`
// AUR-specific configuration
// Maintainer is the AUR package maintainer (e.g., "Name <email>").
Maintainer string `yaml:"maintainer,omitempty" mapstructure:"maintainer,omitempty"`
Maintainer string `yaml:"maintainer,omitempty"`
// Chocolatey-specific configuration
// Push determines whether to push to Chocolatey (false = generate only).
Push bool `yaml:"push,omitempty" mapstructure:"push,omitempty"`
Push bool `yaml:"push,omitempty"`
// Official repo configuration (for Homebrew, Scoop)
// When enabled, generates files for PR to official repos.
Official *OfficialConfig `yaml:"official,omitempty" mapstructure:"official,omitempty"`
Official *OfficialConfig `yaml:"official,omitempty"`
}
// OfficialConfig holds configuration for generating files for official repo PRs.
type OfficialConfig struct {
// Enabled determines whether to generate files for official repos.
Enabled bool `yaml:"enabled" mapstructure:"enabled"`
Enabled bool `yaml:"enabled"`
// Output is the directory to write generated files.
Output string `yaml:"output,omitempty" mapstructure:"output,omitempty"`
Output string `yaml:"output,omitempty"`
}
// SDKConfig holds SDK generation configuration.
type SDKConfig struct {
// Spec is the path to the OpenAPI spec file.
Spec string `yaml:"spec,omitempty" mapstructure:"spec,omitempty"`
Spec string `yaml:"spec,omitempty"`
// Languages to generate.
Languages []string `yaml:"languages,omitempty" mapstructure:"languages,omitempty"`
Languages []string `yaml:"languages,omitempty"`
// Output directory (default: sdk/).
Output string `yaml:"output,omitempty" mapstructure:"output,omitempty"`
Output string `yaml:"output,omitempty"`
// Package naming.
Package SDKPackageConfig `yaml:"package,omitempty" mapstructure:"package,omitempty"`
Package SDKPackageConfig `yaml:"package,omitempty"`
// Diff configuration.
Diff SDKDiffConfig `yaml:"diff,omitempty" mapstructure:"diff,omitempty"`
Diff SDKDiffConfig `yaml:"diff,omitempty"`
// Publish configuration.
Publish SDKPublishConfig `yaml:"publish,omitempty" mapstructure:"publish,omitempty"`
Publish SDKPublishConfig `yaml:"publish,omitempty"`
}
// SDKPackageConfig holds package naming configuration.
type SDKPackageConfig struct {
Name string `yaml:"name,omitempty" mapstructure:"name,omitempty"`
Version string `yaml:"version,omitempty" mapstructure:"version,omitempty"`
Name string `yaml:"name,omitempty"`
Version string `yaml:"version,omitempty"`
}
// SDKDiffConfig holds diff configuration.
type SDKDiffConfig struct {
Enabled bool `yaml:"enabled,omitempty" mapstructure:"enabled,omitempty"`
FailOnBreaking bool `yaml:"fail_on_breaking,omitempty" mapstructure:"fail_on_breaking,omitempty"`
Enabled bool `yaml:"enabled,omitempty"`
FailOnBreaking bool `yaml:"fail_on_breaking,omitempty"`
}
// SDKPublishConfig holds monorepo publish configuration.
type SDKPublishConfig struct {
Repo string `yaml:"repo,omitempty" mapstructure:"repo,omitempty"`
Path string `yaml:"path,omitempty" mapstructure:"path,omitempty"`
Repo string `yaml:"repo,omitempty"`
Path string `yaml:"path,omitempty"`
}
// ChangelogConfig holds changelog generation settings.
type ChangelogConfig struct {
// Include specifies commit types to include in the changelog.
Include []string `yaml:"include" mapstructure:"include"`
Include []string `yaml:"include"`
// Exclude specifies commit types to exclude from the changelog.
Exclude []string `yaml:"exclude" mapstructure:"exclude"`
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(m io.Medium, dir string) (*Config, error) {
func LoadConfig(dir string) (*Config, error) {
configPath := filepath.Join(dir, ConfigDir, ConfigFileName)
// Convert to absolute path for io.Local
absPath, err := filepath.Abs(configPath)
if err != nil {
return nil, fmt.Errorf("release.LoadConfig: failed to resolve path: %w", err)
}
if !m.Exists(absPath) {
cfg := DefaultConfig()
cfg.projectDir = dir
return cfg, nil
}
// Use centralized config service
c, err := config.New(config.WithMedium(m), config.WithPath(absPath))
content, err := io.Local.Read(absPath)
if err != nil {
return nil, fmt.Errorf("release.LoadConfig: %w", err)
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)
}
cfg := DefaultConfig()
if err := c.Get("", cfg); err != nil {
return nil, fmt.Errorf("release.LoadConfig: %w", err)
var cfg Config
if err := yaml.Unmarshal([]byte(content), &cfg); err != nil {
return nil, fmt.Errorf("release.LoadConfig: failed to parse config file: %w", err)
}
// Apply defaults for any missing fields
applyDefaults(cfg)
applyDefaults(&cfg)
cfg.projectDir = dir
return cfg, nil
return &cfg, nil
}
// DefaultConfig returns sensible defaults for release configuration.
@ -212,6 +212,7 @@ func DefaultConfig() *Config {
Targets: []TargetConfig{
{OS: "linux", Arch: "amd64"},
{OS: "linux", Arch: "arm64"},
{OS: "darwin", Arch: "amd64"},
{OS: "darwin", Arch: "arm64"},
{OS: "windows", Arch: "amd64"},
},
@ -268,13 +269,13 @@ func ConfigPath(dir string) string {
}
// ConfigExists checks if a release config file exists in the given directory.
func ConfigExists(m io.Medium, dir string) bool {
func ConfigExists(dir string) bool {
configPath := ConfigPath(dir)
absPath, err := filepath.Abs(configPath)
if err != nil {
return false
}
return m.Exists(absPath)
return io.Local.IsFile(absPath)
}
// GetRepository returns the repository from the config.
@ -288,20 +289,27 @@ func (c *Config) GetProjectName() string {
}
// WriteConfig writes the config to the .core/release.yaml file.
func WriteConfig(m io.Medium, cfg *Config, dir string) error {
func WriteConfig(cfg *Config, dir string) error {
configPath := ConfigPath(dir)
// Convert to absolute path for io.Local
absPath, err := filepath.Abs(configPath)
if err != nil {
return fmt.Errorf("release.WriteConfig: failed to resolve path: %w", err)
}
// Ensure directory exists
configDir := filepath.Dir(absPath)
if err := io.Local.EnsureDir(configDir); 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)
}
// m.Write creates parent directories automatically
if err := m.Write(absPath, string(data)); err != nil {
if err := io.Local.Write(absPath, string(data)); err != nil {
return fmt.Errorf("release.WriteConfig: failed to write config file: %w", err)
}

View file

@ -25,8 +25,6 @@ type Release struct {
Changelog string
// ProjectDir is the root directory of the project.
ProjectDir string
// FS is the medium for file operations.
FS io.Medium
}
// Publish publishes pre-built artifacts from dist/ to configured targets.
@ -37,8 +35,6 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
return nil, fmt.Errorf("release.Publish: config is nil")
}
m := io.Local
projectDir := cfg.projectDir
if projectDir == "" {
projectDir = "."
@ -61,7 +57,7 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
// Step 2: Find pre-built artifacts in dist/
distDir := filepath.Join(absProjectDir, "dist")
artifacts, err := findArtifacts(m, distDir)
artifacts, err := findArtifacts(distDir)
if err != nil {
return nil, fmt.Errorf("release.Publish: %w", err)
}
@ -82,12 +78,11 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
Artifacts: artifacts,
Changelog: changelog,
ProjectDir: absProjectDir,
FS: m,
}
// Step 4: Publish to configured targets
if len(cfg.Publishers) > 0 {
pubRelease := publishers.NewRelease(release.Version, release.Artifacts, release.Changelog, release.ProjectDir, release.FS)
pubRelease := publishers.NewRelease(release.Version, release.Artifacts, release.Changelog, release.ProjectDir)
for _, pubCfg := range cfg.Publishers {
publisher, err := getPublisher(pubCfg.Type)
@ -107,14 +102,14 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
}
// findArtifacts discovers pre-built artifacts in the dist directory.
func findArtifacts(m io.Medium, distDir string) ([]build.Artifact, error) {
if !m.IsDir(distDir) {
func findArtifacts(distDir string) ([]build.Artifact, error) {
if !io.Local.IsDir(distDir) {
return nil, fmt.Errorf("dist/ directory not found")
}
var artifacts []build.Artifact
entries, err := m.List(distDir)
entries, err := io.Local.List(distDir)
if err != nil {
return nil, fmt.Errorf("failed to read dist/: %w", err)
}
@ -148,8 +143,6 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
return nil, fmt.Errorf("release.Run: config is nil")
}
m := io.Local
projectDir := cfg.projectDir
if projectDir == "" {
projectDir = "."
@ -178,7 +171,7 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
}
// Step 3: Build artifacts
artifacts, err := buildArtifacts(ctx, m, cfg, absProjectDir, version)
artifacts, err := buildArtifacts(ctx, cfg, absProjectDir, version)
if err != nil {
return nil, fmt.Errorf("release.Run: build failed: %w", err)
}
@ -188,13 +181,12 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
Artifacts: artifacts,
Changelog: changelog,
ProjectDir: absProjectDir,
FS: m,
}
// Step 4: Publish to configured targets
if len(cfg.Publishers) > 0 {
// Convert to publisher types
pubRelease := publishers.NewRelease(release.Version, release.Artifacts, release.Changelog, release.ProjectDir, release.FS)
pubRelease := publishers.NewRelease(release.Version, release.Artifacts, release.Changelog, release.ProjectDir)
for _, pubCfg := range cfg.Publishers {
publisher, err := getPublisher(pubCfg.Type)
@ -215,9 +207,9 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
}
// buildArtifacts builds all artifacts for the release.
func buildArtifacts(ctx context.Context, fs io.Medium, cfg *Config, projectDir, version string) ([]build.Artifact, error) {
func buildArtifacts(ctx context.Context, cfg *Config, projectDir, version string) ([]build.Artifact, error) {
// Load build configuration
buildCfg, err := build.LoadConfig(fs, projectDir)
buildCfg, err := build.LoadConfig(projectDir)
if err != nil {
return nil, fmt.Errorf("failed to load build config: %w", err)
}
@ -235,6 +227,7 @@ func buildArtifacts(ctx context.Context, fs io.Medium, cfg *Config, projectDir,
targets = []build.Target{
{OS: "linux", Arch: "amd64"},
{OS: "linux", Arch: "arm64"},
{OS: "darwin", Arch: "amd64"},
{OS: "darwin", Arch: "arm64"},
{OS: "windows", Arch: "amd64"},
}
@ -256,7 +249,7 @@ func buildArtifacts(ctx context.Context, fs io.Medium, cfg *Config, projectDir,
outputDir := filepath.Join(projectDir, "dist")
// Get builder (detect project type)
projectType, err := build.PrimaryType(fs, projectDir)
projectType, err := build.PrimaryType(projectDir)
if err != nil {
return nil, fmt.Errorf("failed to detect project type: %w", err)
}
@ -268,7 +261,6 @@ func buildArtifacts(ctx context.Context, fs io.Medium, cfg *Config, projectDir,
// Build configuration
buildConfig := &build.Config{
FS: fs,
ProjectDir: projectDir,
OutputDir: outputDir,
Name: binaryName,
@ -283,20 +275,20 @@ func buildArtifacts(ctx context.Context, fs io.Medium, cfg *Config, projectDir,
}
// Archive artifacts
archivedArtifacts, err := build.ArchiveAll(fs, artifacts)
archivedArtifacts, err := build.ArchiveAll(artifacts)
if err != nil {
return nil, fmt.Errorf("archive failed: %w", err)
}
// Compute checksums
checksummedArtifacts, err := build.ChecksumAll(fs, archivedArtifacts)
checksummedArtifacts, err := build.ChecksumAll(archivedArtifacts)
if err != nil {
return nil, fmt.Errorf("checksum failed: %w", err)
}
// Write CHECKSUMS.txt
checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt")
if err := build.WriteChecksumFile(fs, checksummedArtifacts, checksumPath); err != nil {
if err := build.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil {
return nil, fmt.Errorf("failed to write checksums file: %w", err)
}
@ -317,7 +309,7 @@ func getBuilder(projectType build.ProjectType) (build.Builder, error) {
case build.ProjectTypeGo:
return builders.NewGoBuilder(), nil
case build.ProjectTypeNode:
return nil, fmt.Errorf("node.js builder not yet implemented")
return nil, fmt.Errorf("Node.js builder not yet implemented")
case build.ProjectTypePHP:
return nil, fmt.Errorf("PHP builder not yet implemented")
default: