Add source publication command and runbook

This commit is contained in:
2026-10-05 19:04:55 +00:00
parent 7481dd0fd2
commit c83313b3b9
15 changed files with 295 additions and 60 deletions

View File

@@ -16,13 +16,14 @@ import (
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process"
)
const Usage = "usage: releasekit version\n releasekit check [--repo DIR] [--config FILE] VERSION"
const Usage = "usage: releasekit version\n releasekit check [--repo DIR] [--config FILE] VERSION\n releasekit publish [--repo DIR] [--config FILE] VERSION"
type Command string
const (
VersionCommand Command = "version"
CheckAction Command = "check"
PublishAction Command = "publish"
)
// Invocation contains parsed command settings owned by the CLI adapter.
@@ -44,8 +45,9 @@ func Parse(args []string) (Invocation, error) {
return Invocation{}, errors.New("version takes no arguments")
}
return Invocation{Command: VersionCommand}, nil
case string(CheckAction):
flags := flag.NewFlagSet("check", flag.ContinueOnError)
case string(CheckAction), string(PublishAction):
action := Command(args[0])
flags := flag.NewFlagSet(string(action), flag.ContinueOnError)
flags.SetOutput(io.Discard)
repository := flags.String("repo", ".", "repository directory")
configPath := flags.String("config", "", "profile path")
@@ -53,9 +55,9 @@ func Parse(args []string) (Invocation, error) {
return Invocation{}, err
}
if flags.NArg() != 1 {
return Invocation{}, errors.New("check requires one version argument")
return Invocation{}, fmt.Errorf("%s requires one version argument", action)
}
return Invocation{Command: CheckAction, Repository: *repository, ConfigPath: *configPath, Version: flags.Arg(0)}, nil
return Invocation{Command: action, Repository: *repository, ConfigPath: *configPath, Version: flags.Arg(0)}, nil
default:
return Invocation{}, fmt.Errorf("unknown command %q", args[0])
}

View File

@@ -26,14 +26,21 @@ func TestParse(t *testing.T) {
if check.Command != CheckAction || check.Repository != "repository with spaces" || check.ConfigPath != "profiles/release config.json" || check.Version != "v1.2.3" {
t.Fatalf("Parse(check) = %#v", check)
}
publish, err := Parse([]string{"publish", "--repo", "candidate", "--config", "releasekit.json", "v1.2.4"})
if err != nil {
t.Fatal(err)
}
if publish.Command != PublishAction || publish.Repository != "candidate" || publish.ConfigPath != "releasekit.json" || publish.Version != "v1.2.4" {
t.Fatalf("Parse(publish) = %#v", publish)
}
if _, err := Parse([]string{"check", "v1.2.3", "--repo", "repository"}); err == nil {
t.Fatal("Parse accepted a flag after the version")
}
if _, err := Parse(nil); err == nil || !strings.Contains(err.Error(), "command is required") {
t.Fatalf("Parse(nil) error = %v, want missing-command error", err)
}
if _, err := Parse([]string{"publish"}); err == nil || !strings.Contains(err.Error(), "unknown command") {
t.Fatalf("Parse(publish) error = %v, want unknown-command error", err)
if _, err := Parse([]string{"unknown"}); err == nil || !strings.Contains(err.Error(), "unknown command") {
t.Fatalf("Parse(unknown) error = %v, want unknown-command error", err)
}
}

View File

@@ -32,7 +32,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
case cli.VersionCommand:
fmt.Fprintf(stdout, "releasekit %s\n", buildinfo.Version())
return 0
case cli.CheckAction:
case cli.CheckAction, cli.PublishAction:
settings, err := cli.LoadSettings(invocation.Repository, invocation.ConfigPath)
if err != nil {
fmt.Fprintf(stderr, "load profile: %v\n", err)
@@ -54,6 +54,22 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
}, stderr, stderr)
}
}
if invocation.Command == cli.PublishAction {
fmt.Fprintf(stderr, "validating and publishing %s\n", invocation.Version)
result, err := releasekit.Publish(ctx, request)
if err != nil {
fmt.Fprintf(stderr, "publication state=%s phase=%s version=%s commit=%s tag-object=%s: %v\n", result.State, result.Phase, result.Version, result.CandidateCommit, result.TagObject, err)
if errors.Is(err, releasekit.ErrInvalidVersion) {
return 2
}
if result.State == releasekit.PublicationUnknown {
return 3
}
return 1
}
fmt.Fprintf(stdout, "published %s at %s (tag object %s)\n", result.Version, result.CandidateCommit, result.TagObject)
return 0
}
candidate, err := releasekit.Check(ctx, request)
if err != nil {
fmt.Fprintf(stderr, "check failed: %v\n", err)

View File

@@ -30,13 +30,13 @@ func TestVersionCommand(t *testing.T) {
func TestUnknownCommand(t *testing.T) {
var stdout, stderr bytes.Buffer
if status := run(context.Background(), []string{"publish"}, &stdout, &stderr); status != 2 {
if status := run(context.Background(), []string{"unknown"}, &stdout, &stderr); status != 2 {
t.Fatalf("run() status = %d, want 2", status)
}
if stdout.Len() != 0 {
t.Fatalf("unknown command wrote to stdout: %q", stdout.String())
}
if want := "unknown command \"publish\"\n" + cli.Usage + "\n"; stderr.String() != want {
if want := "unknown command \"unknown\"\n" + cli.Usage + "\n"; stderr.String() != want {
t.Fatalf("usage output = %q, want %q", stderr.String(), want)
}
}
@@ -82,6 +82,52 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
}
}
func TestPublishCommandPublishesConfiguredCandidate(t *testing.T) {
repository := newCLIRepository(t)
settings := cli.Settings{
Product: "Example Product",
Remote: "origin",
Checks: []cli.CheckCommand{{
Args: []string{"go", "test", "-mod=readonly", "-count=1", "./..."},
Env: map[string]string{
"GOTOOLCHAIN": "local",
"GOPROXY": "off",
"GOSUMDB": "off",
},
}},
}
contents, err := json.Marshal(settings)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(repository, "releasekit.json"), contents, 0o600); err != nil {
t.Fatal(err)
}
cliGitRun(t, repository, "add", "releasekit.json")
cliGitRun(t, repository, "commit", "-m", "configure source release")
remote := filepath.Join(t.TempDir(), "origin.git")
cliGitRun(t, repository, "init", "--bare", remote)
cliGitRun(t, repository, "remote", "add", "origin", remote)
cliGitRun(t, repository, "push", "--set-upstream", "origin", "main")
var stdout, stderr bytes.Buffer
args := []string{"publish", "--repo", repository, "v1.2.3"}
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
}
commit := cliGitOutput(t, repository, "rev-parse", "HEAD")
tagObject := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3")
if want := fmt.Sprintf("published v1.2.3 at %s (tag object %s)\n", commit, tagObject); stdout.String() != want {
t.Fatalf("stdout = %q, want %q", stdout.String(), want)
}
if !bytes.Contains(stderr.Bytes(), []byte("validating and publishing v1.2.3")) || !bytes.Contains(stderr.Bytes(), []byte("example.com/cli-candidate")) {
t.Fatalf("stderr does not show publication progress and validation: %q", stderr.String())
}
if got := cliGitOutput(t, remote, "rev-parse", "refs/tags/v1.2.3"); got != tagObject {
t.Fatalf("remote tag object = %s, want %s", got, tagObject)
}
}
func TestCandidateCheckHelper(t *testing.T) {
if os.Getenv("RELEASEKIT_CHECK_HELPER") != "1" {
return