cli/cmd/ci/ci_release.go

256 lines
7.2 KiB
Go
Raw Normal View History

// Package ci provides release publishing commands.
package ci
import (
"bufio"
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/pkg/release"
"github.com/leaanthony/clir"
)
// CIRelease command styles
var (
releaseHeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#3b82f6")) // blue-500
releaseSuccessStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#22c55e")) // green-500
releaseErrorStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#ef4444")) // red-500
releaseDimStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#6b7280")) // gray-500
releaseValueStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#e2e8f0")) // gray-200
)
// AddCIReleaseCommand adds the release command and its subcommands.
func AddCIReleaseCommand(app *clir.Cli) {
releaseCmd := app.NewSubCommand("ci", "Publish releases (dry-run by default)")
releaseCmd.LongDescription("Publishes pre-built artifacts from dist/ to configured targets.\n" +
"Run 'core build' first to create artifacts.\n\n" +
"SAFE BY DEFAULT: Runs in dry-run mode unless --we-are-go-for-launch is specified.\n\n" +
"Configuration: .core/release.yaml")
// Flags for the main release command
var goForLaunch bool
var version string
var draft bool
var prerelease bool
releaseCmd.BoolFlag("we-are-go-for-launch", "Actually publish (default is dry-run for safety)", &goForLaunch)
releaseCmd.StringFlag("version", "Version to release (e.g., v1.2.3)", &version)
releaseCmd.BoolFlag("draft", "Create release as a draft", &draft)
releaseCmd.BoolFlag("prerelease", "Mark release as a prerelease", &prerelease)
// Default action for `core ci` - dry-run by default for safety
releaseCmd.Action(func() error {
dryRun := !goForLaunch
return runCIPublish(dryRun, version, draft, prerelease)
})
// `release init` subcommand
initCmd := releaseCmd.NewSubCommand("init", "Initialize release configuration")
initCmd.LongDescription("Creates a .core/release.yaml configuration file interactively.")
initCmd.Action(func() error {
return runCIReleaseInit()
})
// `release changelog` subcommand
changelogCmd := releaseCmd.NewSubCommand("changelog", "Generate changelog")
changelogCmd.LongDescription("Generates a changelog from conventional commits.")
var fromRef, toRef string
changelogCmd.StringFlag("from", "Starting ref (default: previous tag)", &fromRef)
changelogCmd.StringFlag("to", "Ending ref (default: HEAD)", &toRef)
changelogCmd.Action(func() error {
return runChangelog(fromRef, toRef)
})
// `release version` subcommand
versionCmd := releaseCmd.NewSubCommand("version", "Show or set version")
versionCmd.LongDescription("Shows the determined version or validates a version string.")
versionCmd.Action(func() error {
return runCIReleaseVersion()
})
}
// runCIPublish publishes pre-built artifacts from dist/.
// It does NOT build - use `core build` first.
func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
ctx := context.Background()
// Get current directory
projectDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}
// Load configuration
cfg, err := release.LoadConfig(projectDir)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
// Apply CLI overrides
if version != "" {
cfg.SetVersion(version)
}
// Apply draft/prerelease overrides to all publishers
if draft || prerelease {
for i := range cfg.Publishers {
if draft {
cfg.Publishers[i].Draft = true
}
if prerelease {
cfg.Publishers[i].Prerelease = true
}
}
}
// Print header
fmt.Printf("%s Publishing release\n", releaseHeaderStyle.Render("CI:"))
if dryRun {
fmt.Printf(" %s\n", releaseDimStyle.Render("(dry-run) use --we-are-go-for-launch to publish"))
} else {
fmt.Printf(" %s\n", releaseSuccessStyle.Render("🚀 GO FOR LAUNCH"))
}
fmt.Println()
// Check for publishers
if len(cfg.Publishers) == 0 {
return fmt.Errorf("no publishers configured in .core/release.yaml")
}
// Publish pre-built artifacts
rel, err := release.Publish(ctx, cfg, dryRun)
if err != nil {
fmt.Printf("%s %v\n", releaseErrorStyle.Render("Error:"), err)
return err
}
// Print summary
fmt.Println()
fmt.Printf("%s Publish completed!\n", releaseSuccessStyle.Render("Success:"))
fmt.Printf(" Version: %s\n", releaseValueStyle.Render(rel.Version))
fmt.Printf(" Artifacts: %d\n", len(rel.Artifacts))
if !dryRun {
for _, pub := range cfg.Publishers {
fmt.Printf(" Published: %s\n", releaseValueStyle.Render(pub.Type))
}
}
return nil
}
// runCIReleaseInit creates a release configuration interactively.
func runCIReleaseInit() error {
projectDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}
// Check if config already exists
if release.ConfigExists(projectDir) {
fmt.Printf("%s Configuration already exists at %s\n",
releaseDimStyle.Render("Note:"),
release.ConfigPath(projectDir))
reader := bufio.NewReader(os.Stdin)
fmt.Print("Overwrite? [y/N]: ")
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response))
if response != "y" && response != "yes" {
fmt.Println("Aborted.")
return nil
}
}
fmt.Printf("%s Creating release configuration\n", releaseHeaderStyle.Render("Init:"))
fmt.Println()
reader := bufio.NewReader(os.Stdin)
// Project name
defaultName := filepath.Base(projectDir)
fmt.Printf("Project name [%s]: ", defaultName)
name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name)
if name == "" {
name = defaultName
}
// Repository
fmt.Print("GitHub repository (owner/repo): ")
repo, _ := reader.ReadString('\n')
repo = strings.TrimSpace(repo)
// Create config
cfg := release.DefaultConfig()
cfg.Project.Name = name
cfg.Project.Repository = repo
// Write config
if err := release.WriteConfig(cfg, projectDir); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
fmt.Println()
fmt.Printf("%s Configuration written to %s\n",
releaseSuccessStyle.Render("Success:"),
release.ConfigPath(projectDir))
return nil
}
// runChangelog generates and prints a changelog.
func runChangelog(fromRef, toRef string) error {
projectDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}
// Load config for changelog settings
cfg, err := release.LoadConfig(projectDir)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
// Generate changelog
changelog, err := release.GenerateWithConfig(projectDir, fromRef, toRef, &cfg.Changelog)
if err != nil {
return fmt.Errorf("failed to generate changelog: %w", err)
}
fmt.Println(changelog)
return nil
}
// runCIReleaseVersion shows the determined version.
func runCIReleaseVersion() error {
projectDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}
version, err := release.DetermineVersion(projectDir)
if err != nil {
return fmt.Errorf("failed to determine version: %w", err)
}
fmt.Printf("Version: %s\n", releaseValueStyle.Render(version))
return nil
}