diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 10a4fbe..9d54855 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -48,6 +48,7 @@ func New(version, commit, date string) *CLI { signCmd(), notarizeCmd(), packageCmd(), + releaseCmd(), { Name: "version", Usage: "print the version", diff --git a/pkg/cli/release.go b/pkg/cli/release.go new file mode 100644 index 0000000..2c48b73 --- /dev/null +++ b/pkg/cli/release.go @@ -0,0 +1,124 @@ +package cli + +import ( + "os" + + "github.com/jimeh/build-emacs-for-macos/pkg/plan" + "github.com/jimeh/build-emacs-for-macos/pkg/release" + "github.com/jimeh/build-emacs-for-macos/pkg/repository" + cli2 "github.com/urfave/cli/v2" +) + +type releaseOptions struct { + Plan *plan.Plan + Repository *repository.Repository + Name string + GithubToken string +} + +func releaseCmd() *cli2.Command { + tokenDefaultText := "" + if len(os.Getenv("GITHUB_TOKEN")) > 0 { + tokenDefaultText = "***" + } + + return &cli2.Command{ + Name: "release", + Usage: "manage GitHub releases", + Flags: []cli2.Flag{ + &cli2.StringFlag{ + Name: "plan", + Usage: "path to build plan YAML file produced by " + + "emacs-builder plan", + Aliases: []string{"p"}, + EnvVars: []string{"EMACS_BUILDER_PLAN"}, + TakesFile: true, + }, + &cli2.StringFlag{ + Name: "repository", + Aliases: []string{"repo", "r"}, + Usage: "owner/name of GitHub repo to check for release, " + + "ignored if a plan is provided", + EnvVars: []string{"GITHUB_REPOSITORY"}, + Value: "jimeh/emacs-builds", + }, + &cli2.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "name of release to operate on, ignored if plan " + + "is provided", + }, + &cli2.StringFlag{ + Name: "github-token", + Usage: "GitHub API Token", + EnvVars: []string{"GITHUB_TOKEN"}, + DefaultText: tokenDefaultText, + }, + }, + Subcommands: []*cli2.Command{ + releaseCheckCmd(), + }, + } +} + +func releaseActionWrapper( + f func(*cli2.Context, *Options, *releaseOptions) error, +) func(*cli2.Context) error { + return actionWrapper(func(c *cli2.Context, opts *Options) error { + rOpts := &releaseOptions{ + Name: c.String("name"), + GithubToken: c.String("github-token"), + } + + if r := c.String("repository"); r != "" { + var err error + rOpts.Repository, err = repository.NewGitHub(r) + if err != nil { + return err + } + } + + if f := c.String("plan"); f != "" { + p, err := plan.Load(f) + if err != nil { + return err + } + + rOpts.Plan = p + } + + return f(c, opts, rOpts) + }) +} + +func releaseCheckCmd() *cli2.Command { + return &cli2.Command{ + Name: "check", + Usage: "check if a GitHub release exists and has specified " + + "asset files", + ArgsUsage: "[ ...]", + Action: releaseActionWrapper(releaseCheckAction), + } +} + +func releaseCheckAction( + c *cli2.Context, + opts *Options, + rOpts *releaseOptions, +) error { + rlsOpts := &release.CheckOptions{ + Repository: rOpts.Repository, + ReleaseName: rOpts.Name, + AssetFiles: c.Args().Slice(), + GithubToken: rOpts.GithubToken, + } + + if rOpts.Plan != nil && rOpts.Plan.Release != nil { + rlsOpts.ReleaseName = rOpts.Plan.Release.Name + } + if rOpts.Plan != nil && rOpts.Plan.Output != nil { + rlsOpts.AssetFiles = []string{rOpts.Plan.Output.DiskImage} + } + + return release.Check(c.Context, rlsOpts) +} diff --git a/pkg/release/check.go b/pkg/release/check.go new file mode 100644 index 0000000..c4c7a6d --- /dev/null +++ b/pkg/release/check.go @@ -0,0 +1,80 @@ +package release + +import ( + "context" + "fmt" + "net/http" + "path/filepath" + "strings" + + "github.com/hashicorp/go-hclog" + "github.com/jimeh/build-emacs-for-macos/pkg/gh" + "github.com/jimeh/build-emacs-for-macos/pkg/repository" +) + +type CheckOptions struct { + // Repository is the GitHub repository to check. + Repository *repository.Repository + + // ReleaseName is the name of the GitHub Release to check. + ReleaseName string + + // AssetFiles is a list of files which must all exist in the release for + // the check to pass. + AssetFiles []string + + // GitHubToken is the OAuth token used to talk to the GitHub API. + GithubToken string +} + +// Check checks if a GitHub repository has a Release by given name, and if the +// release contains assets with given filenames. +func Check(ctx context.Context, opts *CheckOptions) error { + logger := hclog.FromContext(ctx).Named("release") + gh := gh.New(ctx, opts.GithubToken) + + repo := opts.Repository + + release, resp, err := gh.Repositories.GetReleaseByTag( + ctx, repo.Owner(), repo.Name(), opts.ReleaseName, + ) + if err != nil { + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("release %s does not exist", opts.ReleaseName) + } + + return err + } + + logger.Info("release exists", "release", opts.ReleaseName) + + missingMap := map[string]bool{} + for _, filename := range opts.AssetFiles { + filename = filepath.Base(filename) + missingMap[filename] = true + for _, a := range release.Assets { + if a.GetName() == filename { + logger.Info("asset exists", "filename", filename) + delete(missingMap, filename) + + break + } + } + } + + if len(missingMap) == 0 { + return nil + } + + var missing []string + for f := range missingMap { + missing = append(missing, f) + } + + logger.Error("missing assets", "filenames", missing) + + return fmt.Errorf( + "release %s is missing assets:\n- %s", + opts.ReleaseName, strings.Join(missing, "\n-"), + ) +}