Setup command now has three modes:
- Registry mode: interactive wizard to select packages
- Bootstrap mode: clones core-devops first, then wizard
- Repo setup mode: generates .core/{build,release,test}.yaml
Changes:
- setup: add interactive package selection with charmbracelet/huh
- setup: detect project type (go/php/node/wails) and generate configs
- setup: auto-detect GitHub repo from git remote
- ai: fix command examples (core dev -> core ai)
- ci: rename flag to --we-are-go-for-launch
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
255 lines
7.2 KiB
Go
255 lines
7.2 KiB
Go
// 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
|
|
}
|