package main import ( "context" "errors" "fmt" "io" "os" "os/signal" "strings" "syscall" "gitea.maximumdirect.net/eric/ReleaseKit" "gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" ) func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) code := run(ctx, os.Args[1:], os.Stdout, os.Stderr) stop() os.Exit(code) } func run(ctx context.Context, args []string, stdout, stderr io.Writer) int { invocation, err := cli.Parse(args) if err != nil { fmt.Fprintf(stderr, "%v\n%s\n", err, cli.Usage) return 2 } switch invocation.Command { case cli.VersionCommand: fmt.Fprintf(stdout, "releasekit %s\n", buildinfo.Version()) return 0 case cli.CheckAction, cli.PublishAction: settings, err := cli.LoadSettings(invocation.Repository, invocation.ConfigPath) if err != nil { fmt.Fprintf(stderr, "load profile: %v\n", err) return 2 } request := releasekit.ReleaseRequest{ RepositoryDir: invocation.Repository, Product: settings.Product, Version: invocation.Version, Remote: settings.Remote, Build: configuredBuildSpec(settings.Build), Output: stderr, } if len(settings.Checks) != 0 { checks := settings.Checks request.Validate = func(ctx context.Context, candidate releasekit.Candidate) error { return cli.RunChecks(ctx, candidate.RepositoryDir, checks, map[string]string{ "RELEASEKIT_VERSION": candidate.Version, "RELEASEKIT_CANDIDATE": candidate.Commit, }, 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) if errors.Is(err, releasekit.ErrInvalidVersion) { return 2 } return 1 } fmt.Fprintf(stdout, "local candidate %s at %s is valid\n", candidate.Version, candidate.Commit) return 0 case cli.BuildAction: settings, err := cli.LoadSettings(invocation.Repository, invocation.ConfigPath) if err != nil { fmt.Fprintf(stderr, "load profile: %v\n", err) return 2 } if settings.Build == nil { fmt.Fprintln(stderr, "build requires build settings in the profile") return 2 } result, err := releasekit.Build(ctx, releasekit.BuildRequest{ RepositoryDir: invocation.Repository, Version: invocation.Version, Spec: *configuredBuildSpec(settings.Build), OutputDir: invocation.OutputDir, Output: stderr, }) if err != nil { fmt.Fprintf(stderr, "build failed at %s: %v\n", result.CandidateCommit, err) for _, artifact := range result.Artifacts { fmt.Fprintf(stderr, "built %s %s\n", artifact.Filename, artifact.SHA256) } if errors.Is(err, releasekit.ErrInvalidVersion) { return 2 } return 1 } if len(result.Targets) == 0 { fmt.Fprintf(stdout, "no build targets selected for %s at %s\n", result.Version, result.CandidateCommit) return 0 } receipt := receiptFromBuild(result) receiptPath, err := cli.WriteBuildReceipt(result.OutputDir, receipt) if err != nil { fmt.Fprintf(stderr, "write build receipt: %v\n", err) return 1 } stored, err := cli.LoadBuildReceipt(receiptPath) if err != nil { fmt.Fprintf(stderr, "verify build receipt: %v\n", err) return 1 } if !stored.MatchesBuild(result.RepositoryDir, result.CandidateCommit, result.Version, *settings.Build) { fmt.Fprintln(stderr, "build receipt does not match the selected source and build settings") return 1 } fmt.Fprintf(stdout, "built %d artifacts for %s at %s with %s\n", len(result.Artifacts), result.Version, result.CandidateCommit, result.Compiler) for _, artifact := range result.Artifacts { fmt.Fprintf(stdout, "%s %s\n", artifact.Filename, artifact.SHA256) } fmt.Fprintf(stdout, "build receipt: %s\n", receiptPath) return 0 } return 2 } func configuredBuildSpec(settings *cli.BuildSettings) *releasekit.BuildSpec { if settings == nil { return nil } spec := &releasekit.BuildSpec{ MainPackage: settings.MainPackage, BinaryName: settings.BinaryName, Targets: make([]releasekit.Target, 0, len(settings.Targets)), } for _, value := range settings.Targets { goos, goarch, _ := strings.Cut(value, "/") spec.Targets = append(spec.Targets, releasekit.Target{OS: goos, Arch: goarch}) } return spec } func receiptFromBuild(result releasekit.BuildResult) cli.BuildReceipt { receipt := cli.BuildReceipt{ RepositoryDir: result.RepositoryDir, CandidateCommit: result.CandidateCommit, Version: result.Version, MainPackage: result.MainPackage, BinaryName: result.BinaryName, OutputDir: result.OutputDir, Compiler: result.Compiler, Targets: make([]cli.ReceiptTarget, 0, len(result.Targets)), Artifacts: make([]cli.ReceiptArtifact, 0, len(result.Artifacts)), } for _, target := range result.Targets { receipt.Targets = append(receipt.Targets, cli.ReceiptTarget{OS: target.OS, Arch: target.Arch}) } for _, artifact := range result.Artifacts { receipt.Artifacts = append(receipt.Artifacts, cli.ReceiptArtifact{ Path: artifact.Path, Filename: artifact.Filename, Target: cli.ReceiptTarget{OS: artifact.Target.OS, Arch: artifact.Target.Arch}, SHA256: artifact.SHA256, }) } return receipt }