go-devops/cmd/ci/ci.go
Snider ac6cff9b18 refactor: migrate CLI imports from core/go to core/cli
Update 58 files from forge.lthn.ai/core/go/pkg/cli to
forge.lthn.ai/core/cli/pkg/cli. Also fix pre-existing broken
imports: cmd/workspace → go-agentic, cmd/dev → self-reference.

Resolves circular dependency that caused qa docblock stub in core/go.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-22 23:28:58 +00:00

239 lines
6 KiB
Go

package ci
import (
"context"
"errors"
"os"
"os/exec"
"strings"
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/go-devops/release"
)
// Style aliases from shared
var (
headerStyle = cli.RepoStyle
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle
valueStyle = cli.ValueStyle
)
// Flag variables for ci command
var (
ciGoForLaunch bool
ciVersion string
ciDraft bool
ciPrerelease bool
)
// Flag variables for changelog subcommand
var (
changelogFromRef string
changelogToRef string
)
var ciCmd = &cli.Command{
Use: "ci",
Short: i18n.T("cmd.ci.short"),
Long: i18n.T("cmd.ci.long"),
RunE: func(cmd *cli.Command, args []string) error {
dryRun := !ciGoForLaunch
return runCIPublish(dryRun, ciVersion, ciDraft, ciPrerelease)
},
}
var ciInitCmd = &cli.Command{
Use: "init",
Short: i18n.T("cmd.ci.init.short"),
Long: i18n.T("cmd.ci.init.long"),
RunE: func(cmd *cli.Command, args []string) error {
return runCIReleaseInit()
},
}
var ciChangelogCmd = &cli.Command{
Use: "changelog",
Short: i18n.T("cmd.ci.changelog.short"),
Long: i18n.T("cmd.ci.changelog.long"),
RunE: func(cmd *cli.Command, args []string) error {
return runChangelog(changelogFromRef, changelogToRef)
},
}
var ciVersionCmd = &cli.Command{
Use: "version",
Short: i18n.T("cmd.ci.version.short"),
Long: i18n.T("cmd.ci.version.long"),
RunE: func(cmd *cli.Command, args []string) error {
return runCIReleaseVersion()
},
}
func init() {
// Main ci command flags
ciCmd.Flags().BoolVar(&ciGoForLaunch, "we-are-go-for-launch", false, i18n.T("cmd.ci.flag.go_for_launch"))
ciCmd.Flags().StringVar(&ciVersion, "version", "", i18n.T("cmd.ci.flag.version"))
ciCmd.Flags().BoolVar(&ciDraft, "draft", false, i18n.T("cmd.ci.flag.draft"))
ciCmd.Flags().BoolVar(&ciPrerelease, "prerelease", false, i18n.T("cmd.ci.flag.prerelease"))
// Changelog subcommand flags
ciChangelogCmd.Flags().StringVar(&changelogFromRef, "from", "", i18n.T("cmd.ci.changelog.flag.from"))
ciChangelogCmd.Flags().StringVar(&changelogToRef, "to", "", i18n.T("cmd.ci.changelog.flag.to"))
// Add subcommands
ciCmd.AddCommand(ciInitCmd)
ciCmd.AddCommand(ciChangelogCmd)
ciCmd.AddCommand(ciVersionCmd)
}
// runCIPublish publishes pre-built artifacts from dist/.
func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
ctx := context.Background()
projectDir, err := os.Getwd()
if err != nil {
return cli.WrapVerb(err, "get", "working directory")
}
cfg, err := release.LoadConfig(projectDir)
if err != nil {
return cli.WrapVerb(err, "load", "config")
}
if version != "" {
cfg.SetVersion(version)
}
if draft || prerelease {
for i := range cfg.Publishers {
if draft {
cfg.Publishers[i].Draft = true
}
if prerelease {
cfg.Publishers[i].Prerelease = true
}
}
}
cli.Print("%s %s\n", headerStyle.Render(i18n.T("cmd.ci.label.ci")), i18n.T("cmd.ci.publishing"))
if dryRun {
cli.Print(" %s\n", dimStyle.Render(i18n.T("cmd.ci.dry_run_hint")))
} else {
cli.Print(" %s\n", successStyle.Render(i18n.T("cmd.ci.go_for_launch")))
}
cli.Blank()
if len(cfg.Publishers) == 0 {
return errors.New(i18n.T("cmd.ci.error.no_publishers"))
}
rel, err := release.Publish(ctx, cfg, dryRun)
if err != nil {
cli.Print("%s %v\n", errorStyle.Render(i18n.Label("error")), err)
return err
}
cli.Blank()
cli.Print("%s %s\n", successStyle.Render(i18n.T("i18n.done.pass")), i18n.T("cmd.ci.publish_completed"))
cli.Print(" %s %s\n", i18n.Label("version"), valueStyle.Render(rel.Version))
cli.Print(" %s %d\n", i18n.T("cmd.ci.label.artifacts"), len(rel.Artifacts))
if !dryRun {
for _, pub := range cfg.Publishers {
cli.Print(" %s %s\n", i18n.T("cmd.ci.label.published"), valueStyle.Render(pub.Type))
}
}
return nil
}
// runCIReleaseInit scaffolds a release config.
func runCIReleaseInit() error {
cwd, err := os.Getwd()
if err != nil {
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
}
cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("init")), i18n.T("cmd.ci.init.initializing"))
if release.ConfigExists(cwd) {
cli.Text(i18n.T("cmd.ci.init.already_initialized"))
return nil
}
cfg := release.DefaultConfig()
if err := release.WriteConfig(cfg, cwd); err != nil {
return cli.Err("%s: %w", i18n.T("i18n.fail.create", "config"), err)
}
cli.Blank()
cli.Print("%s %s\n", successStyle.Render("v"), i18n.T("cmd.ci.init.created_config"))
cli.Blank()
cli.Text(i18n.T("cmd.ci.init.next_steps"))
cli.Print(" %s\n", i18n.T("cmd.ci.init.edit_config"))
cli.Print(" %s\n", i18n.T("cmd.ci.init.run_ci"))
return nil
}
// runChangelog generates a changelog between two git refs.
func runChangelog(fromRef, toRef string) error {
cwd, err := os.Getwd()
if err != nil {
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
}
if fromRef == "" || toRef == "" {
tag, err := latestTag(cwd)
if err == nil {
if fromRef == "" {
fromRef = tag
}
if toRef == "" {
toRef = "HEAD"
}
} else {
cli.Text(i18n.T("cmd.ci.changelog.no_tags"))
return nil
}
}
cli.Print("%s %s..%s\n\n", dimStyle.Render(i18n.T("cmd.ci.changelog.generating")), fromRef, toRef)
changelog, err := release.Generate(cwd, fromRef, toRef)
if err != nil {
return cli.Err("%s: %w", i18n.T("i18n.fail.generate", "changelog"), err)
}
cli.Text(changelog)
return nil
}
// runCIReleaseVersion shows the determined version.
func runCIReleaseVersion() error {
projectDir, err := os.Getwd()
if err != nil {
return cli.WrapVerb(err, "get", "working directory")
}
version, err := release.DetermineVersion(projectDir)
if err != nil {
return cli.WrapVerb(err, "determine", "version")
}
cli.Print("%s %s\n", i18n.Label("version"), valueStyle.Render(version))
return nil
}
func latestTag(dir string) (string, error) {
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}