Files
ReleaseKit/cmd/releasekit/internal/cli/config.go

149 lines
4.4 KiB
Go

// Package cli owns command parsing, profile settings, and child-command
// adaptation for the ReleaseKit executable.
package cli
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process"
)
const Usage = "usage: releasekit version\n releasekit check [--repo DIR] [--config FILE] VERSION"
type Command string
const (
VersionCommand Command = "version"
CheckAction Command = "check"
)
// Invocation contains parsed command settings owned by the CLI adapter.
type Invocation struct {
Command Command
Repository string
ConfigPath string
Version string
}
// Parse accepts only commands that have been implemented by the executable.
func Parse(args []string) (Invocation, error) {
if len(args) == 0 {
return Invocation{}, errors.New("a command is required")
}
switch args[0] {
case string(VersionCommand):
if len(args) != 1 {
return Invocation{}, errors.New("version takes no arguments")
}
return Invocation{Command: VersionCommand}, nil
case string(CheckAction):
flags := flag.NewFlagSet("check", flag.ContinueOnError)
flags.SetOutput(io.Discard)
repository := flags.String("repo", ".", "repository directory")
configPath := flags.String("config", "", "profile path")
if err := flags.Parse(args[1:]); err != nil {
return Invocation{}, err
}
if flags.NArg() != 1 {
return Invocation{}, errors.New("check requires one version argument")
}
return Invocation{Command: CheckAction, Repository: *repository, ConfigPath: *configPath, Version: flags.Arg(0)}, nil
default:
return Invocation{}, fmt.Errorf("unknown command %q", args[0])
}
}
// CheckCommand contains one direct child-process invocation from a profile.
type CheckCommand struct {
Args []string `json:"args"`
Env map[string]string `json:"env,omitempty"`
}
// Settings contains the product, remote, and check commands read from a
// releasekit.json profile.
type Settings struct {
Product string `json:"product"`
Remote string `json:"remote,omitempty"`
Checks []CheckCommand `json:"checks,omitempty"`
}
// LoadSettings reads a profile path relative to repoDir unless it is absolute.
// An empty configPath selects <repoDir>/releasekit.json.
func LoadSettings(repoDir, configPath string) (Settings, error) {
resolvedRepo, err := filepath.Abs(repoDir)
if err != nil {
return Settings{}, fmt.Errorf("resolve repository directory %q: %w", repoDir, err)
}
if configPath == "" {
configPath = "releasekit.json"
}
if !filepath.IsAbs(configPath) {
configPath = filepath.Join(resolvedRepo, configPath)
}
file, err := os.Open(configPath)
if err != nil {
return Settings{}, fmt.Errorf("open profile %q: %w", configPath, err)
}
defer file.Close()
var settings Settings
decoder := json.NewDecoder(file)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&settings); err != nil {
return Settings{}, fmt.Errorf("decode profile %q: %w", configPath, err)
}
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
if err == nil {
return Settings{}, fmt.Errorf("decode profile %q: multiple JSON values", configPath)
}
return Settings{}, fmt.Errorf("decode profile %q: %w", configPath, err)
}
if strings.TrimSpace(settings.Product) == "" {
return Settings{}, fmt.Errorf("profile %q: product is required", configPath)
}
if settings.Remote == "" {
settings.Remote = "origin"
}
for i, check := range settings.Checks {
if len(check.Args) == 0 || check.Args[0] == "" {
return Settings{}, fmt.Errorf("profile %q: checks[%d].args must include a command", configPath, i)
}
}
return settings, nil
}
// RunChecks runs configured argument vectors in repoDir. Each command receives
// its own environment overlay and inherits the remaining process environment.
func RunChecks(ctx context.Context, repoDir string, checks []CheckCommand, ownedEnv map[string]string, stdout, stderr io.Writer) error {
for i, check := range checks {
environment := make(map[string]string, len(check.Env)+len(ownedEnv))
for key, value := range check.Env {
environment[key] = value
}
for key, value := range ownedEnv {
environment[key] = value
}
if err := process.Run(ctx, process.Invocation{
Args: check.Args,
Dir: repoDir,
Env: environment,
Stdout: stdout,
Stderr: stderr,
}); err != nil {
return fmt.Errorf("run check %d: %w", i+1, err)
}
}
return nil
}