mirror of
https://github.com/jimeh/build-emacs-for-macos.git
synced 2026-02-19 07:16:39 +00:00
feat(plan): add plan command to create build plans
This commit is contained in:
@@ -44,6 +44,7 @@ func New(version, commit, date string) *CLI {
|
||||
cli2.VersionFlag,
|
||||
},
|
||||
Commands: []*cli2.Command{
|
||||
planCmd(),
|
||||
{
|
||||
Name: "version",
|
||||
Usage: "print the version",
|
||||
|
||||
128
pkg/cli/plan.go
Normal file
128
pkg/cli/plan.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/plan"
|
||||
cli2 "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func planCmd() *cli2.Command {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
wd = ""
|
||||
}
|
||||
|
||||
tokenDefaultText := ""
|
||||
if len(os.Getenv("GITHUB_TOKEN")) > 0 {
|
||||
tokenDefaultText = "***"
|
||||
}
|
||||
|
||||
return &cli2.Command{
|
||||
Name: "plan",
|
||||
Usage: "plan a Emacs.app bundle with codeplan",
|
||||
ArgsUsage: "<branch/tag>",
|
||||
Flags: []cli2.Flag{
|
||||
&cli2.StringFlag{
|
||||
Name: "emacs-repo",
|
||||
Usage: "GitHub repository to get Emacs commit info and " +
|
||||
"tarball from",
|
||||
Aliases: []string{"e"},
|
||||
EnvVars: []string{"EMACS_REPO"},
|
||||
Value: "emacs-mirror/emacs",
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "sha",
|
||||
Usage: "override commit SHA of specified git branch/tag",
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "output",
|
||||
Usage: "output filename to write plan to instead of printing " +
|
||||
"to STDOUT",
|
||||
Aliases: []string{"o"},
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "output-dir",
|
||||
Usage: "output directory where build result is stored",
|
||||
Value: filepath.Join(wd, "builds"),
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "test-build",
|
||||
Usage: "plan a test build with given name, which is " +
|
||||
"published to a draft or pre-release " +
|
||||
"\"test-builds\" release",
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "test-release-type",
|
||||
Value: "prerelease",
|
||||
Usage: "type of release when doing a test-build " +
|
||||
"(prerelease or draft)",
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "github-token",
|
||||
Usage: "GitHub API Token",
|
||||
EnvVars: []string{"GITHUB_TOKEN"},
|
||||
DefaultText: tokenDefaultText,
|
||||
},
|
||||
},
|
||||
Action: actionWrapper(planAction),
|
||||
}
|
||||
}
|
||||
|
||||
func planAction(c *cli2.Context, opts *Options) error {
|
||||
logger := hclog.FromContext(c.Context).Named("plan")
|
||||
|
||||
ref := c.Args().Get(0)
|
||||
if ref == "" {
|
||||
ref = "master"
|
||||
}
|
||||
|
||||
planOpts := &plan.Options{
|
||||
EmacsRepo: c.String("emacs-repo"),
|
||||
Ref: ref,
|
||||
SHAOverride: c.String("sha"),
|
||||
OutputDir: c.String("output-dir"),
|
||||
TestBuild: c.String("test-build"),
|
||||
TestBuildType: plan.Prerelease,
|
||||
GithubToken: c.String("github-token"),
|
||||
}
|
||||
|
||||
if c.String("test-build-type") == "draft" {
|
||||
planOpts.TestBuildType = plan.Draft
|
||||
}
|
||||
|
||||
if !opts.quiet {
|
||||
planOpts.Output = os.Stdout
|
||||
}
|
||||
|
||||
p, err := plan.Create(c.Context, planOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
planYAML, err := p.YAML()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var out *os.File
|
||||
out = os.Stdout
|
||||
if f := c.String("output"); f != "" {
|
||||
logger.Info("writing plan", "file", f)
|
||||
logger.Debug("content", "yaml", planYAML)
|
||||
out, err = os.Create(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
}
|
||||
|
||||
_, err = out.WriteString(planYAML)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
42
pkg/commit/commit.go
Normal file
42
pkg/commit/commit.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v35/github"
|
||||
)
|
||||
|
||||
type Commit struct {
|
||||
SHA string `yaml:"sha"`
|
||||
Date *time.Time `yaml:"date"`
|
||||
Author string `yaml:"author"`
|
||||
Committer string `yaml:"committer"`
|
||||
Message string `yaml:"message"`
|
||||
}
|
||||
|
||||
func New(rc *github.RepositoryCommit) *Commit {
|
||||
return &Commit{
|
||||
SHA: rc.GetSHA(),
|
||||
Date: rc.GetCommit().GetCommitter().Date,
|
||||
Author: fmt.Sprintf(
|
||||
"%s <%s>",
|
||||
rc.GetCommit().GetAuthor().GetName(),
|
||||
rc.GetCommit().GetAuthor().GetEmail(),
|
||||
),
|
||||
Committer: fmt.Sprintf(
|
||||
"%s <%s>",
|
||||
rc.GetCommit().GetCommitter().GetName(),
|
||||
rc.GetCommit().GetCommitter().GetEmail(),
|
||||
),
|
||||
Message: rc.GetCommit().GetMessage(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Commit) ShortSHA() string {
|
||||
return s.SHA[0:7]
|
||||
}
|
||||
|
||||
func (s *Commit) DateString() string {
|
||||
return s.Date.Format("2006-01-02")
|
||||
}
|
||||
24
pkg/gh/gh.go
Normal file
24
pkg/gh/gh.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package gh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-github/v35/github"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func New(ctx context.Context, token string) *github.Client {
|
||||
if token == "" {
|
||||
token = os.Getenv("GITHUB_TOKEN")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return github.NewClient(nil)
|
||||
}
|
||||
|
||||
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
|
||||
tc := oauth2.NewClient(ctx, ts)
|
||||
|
||||
return github.NewClient(tc)
|
||||
}
|
||||
40
pkg/osinfo/osinfo.go
Normal file
40
pkg/osinfo/osinfo.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package osinfo
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OSInfo struct {
|
||||
Name string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
Arch string `yaml:"arch"`
|
||||
}
|
||||
|
||||
func New() (*OSInfo, error) {
|
||||
version, err := exec.Command("sw_vers", "-productVersion").CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arch, err := exec.Command("uname", "-m").CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &OSInfo{
|
||||
Name: "macOS",
|
||||
Version: strings.TrimSpace(string(version)),
|
||||
Arch: strings.TrimSpace(string(arch)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OSInfo) MajorMinor() string {
|
||||
parts := strings.Split(s.Version, ".")
|
||||
max := len(parts)
|
||||
if max > 2 {
|
||||
max = 2
|
||||
}
|
||||
|
||||
return strings.Join(parts[0:max], ".")
|
||||
}
|
||||
125
pkg/plan/create.go
Normal file
125
pkg/plan/create.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/commit"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/osinfo"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
|
||||
)
|
||||
|
||||
var nonAlphaNum = regexp.MustCompile(`[^\w_-]+`)
|
||||
|
||||
type TestBuildType string
|
||||
|
||||
//nolint:golint
|
||||
const (
|
||||
Draft TestBuildType = "draft"
|
||||
Prerelease TestBuildType = "prerelease"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
GithubToken string
|
||||
EmacsRepo string
|
||||
Ref string
|
||||
SHAOverride string
|
||||
OutputDir string
|
||||
TestBuild string
|
||||
TestBuildType TestBuildType
|
||||
Output io.Writer
|
||||
}
|
||||
|
||||
func Create(ctx context.Context, opts *Options) (*Plan, error) {
|
||||
logger := hclog.FromContext(ctx).Named("plan")
|
||||
|
||||
repo, err := repository.NewGitHub(opts.EmacsRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gh := gh.New(ctx, opts.GithubToken)
|
||||
|
||||
lookupRef := opts.Ref
|
||||
if opts.SHAOverride != "" {
|
||||
lookupRef = opts.SHAOverride
|
||||
}
|
||||
logger.Info("fetching commit info", "ref", lookupRef)
|
||||
|
||||
repoCommit, _, err := gh.Repositories.GetCommit(
|
||||
ctx, repo.Owner(), repo.Name(), lookupRef,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commitInfo := commit.New(repoCommit)
|
||||
osInfo, err := osinfo.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
releaseName := fmt.Sprintf(
|
||||
"Emacs.%s.%s.%s",
|
||||
commitInfo.DateString(),
|
||||
commitInfo.ShortSHA(),
|
||||
sanitizeString(opts.Ref),
|
||||
)
|
||||
buildName := fmt.Sprintf(
|
||||
"%s.%s.%s",
|
||||
releaseName,
|
||||
sanitizeString(osInfo.Name+"-"+osInfo.MajorMinor()),
|
||||
sanitizeString(osInfo.Arch),
|
||||
)
|
||||
diskImage := buildName + ".dmg"
|
||||
|
||||
plan := &Plan{
|
||||
Build: &Build{
|
||||
Name: buildName,
|
||||
},
|
||||
Source: &Source{
|
||||
Ref: opts.Ref,
|
||||
Repository: repo,
|
||||
Commit: commitInfo,
|
||||
Tarball: &Tarball{
|
||||
URL: repo.TarballURL(commitInfo.SHA),
|
||||
},
|
||||
},
|
||||
OS: osInfo,
|
||||
Release: &Release{
|
||||
Name: releaseName,
|
||||
},
|
||||
Output: &Output{
|
||||
Directory: opts.OutputDir,
|
||||
DiskImage: diskImage,
|
||||
},
|
||||
}
|
||||
|
||||
if opts.TestBuild != "" {
|
||||
testName := sanitizeString(opts.TestBuild)
|
||||
|
||||
plan.Build.Name += ".test." + testName
|
||||
plan.Release.Title = "Test Builds"
|
||||
plan.Release.Name = "test-builds"
|
||||
if opts.TestBuildType == Draft {
|
||||
plan.Release.Draft = true
|
||||
} else {
|
||||
plan.Release.Prerelease = true
|
||||
}
|
||||
|
||||
index := strings.LastIndex(diskImage, ".")
|
||||
plan.Output.DiskImage = diskImage[:index] + ".test." +
|
||||
testName + diskImage[index:]
|
||||
}
|
||||
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
func sanitizeString(s string) string {
|
||||
return nonAlphaNum.ReplaceAllString(s, "-")
|
||||
}
|
||||
82
pkg/plan/plan.go
Normal file
82
pkg/plan/plan.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/commit"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/osinfo"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Plan struct {
|
||||
Build *Build `yaml:"build,omitempty"`
|
||||
Source *Source `yaml:"source,omitempty"`
|
||||
OS *osinfo.OSInfo `yaml:"os,omitempty"`
|
||||
Release *Release `yaml:"release,omitempty"`
|
||||
Output *Output `yaml:"output,omitempty"`
|
||||
}
|
||||
|
||||
// Load attempts to loads a plan YAML from given filename.
|
||||
func Load(filename string) (*Plan, error) {
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &Plan{}
|
||||
err = yaml.Unmarshal(b, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// WriteYAML writes plan in YAML format to given io.Writer.
|
||||
func (s *Plan) WriteYAML(w io.Writer) error {
|
||||
enc := yaml.NewEncoder(w)
|
||||
enc.SetIndent(2)
|
||||
|
||||
return enc.Encode(s)
|
||||
}
|
||||
|
||||
// YAML returns plan in YAML format.
|
||||
func (s *Plan) YAML() (string, error) {
|
||||
var buf bytes.Buffer
|
||||
err := s.WriteYAML(&buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
type Build struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Ref string `yaml:"ref,omitempty"`
|
||||
Repository *repository.Repository `yaml:"repository,omitempty"`
|
||||
Commit *commit.Commit `yaml:"commit,omitempty"`
|
||||
Tarball *Tarball `yaml:"tarball,omitempty"`
|
||||
}
|
||||
|
||||
type Tarball struct {
|
||||
URL string `yaml:"url,omitempty"`
|
||||
}
|
||||
|
||||
type Release struct {
|
||||
Name string `yaml:"name"`
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Draft bool `yaml:"draft,omitempty"`
|
||||
Prerelease bool `yaml:"prerelease,omitempty"`
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Directory string `yaml:"directory,omitempty"`
|
||||
DiskImage string `yaml:"disk_image,omitempty"`
|
||||
}
|
||||
91
pkg/repository/repository.go
Normal file
91
pkg/repository/repository.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//nolint:golint
|
||||
var (
|
||||
Err = errors.New("repository")
|
||||
ErrGitHub = fmt.Errorf("%w: github", Err)
|
||||
)
|
||||
|
||||
const GitHubBaseURL = "https://github.com/"
|
||||
|
||||
// Type is a repository type
|
||||
type Type string
|
||||
|
||||
const GitHub Type = "github"
|
||||
|
||||
// Repository represents basic information about a repository with helper
|
||||
// methods to get various pieces of information from it.
|
||||
type Repository struct {
|
||||
Type Type `yaml:"type,omitempty"`
|
||||
Source string `yaml:"source,omitempty"`
|
||||
}
|
||||
|
||||
func NewGitHub(ownerAndName string) (*Repository, error) {
|
||||
parts := strings.Split(ownerAndName, "/")
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: repository must be give in \"owner/name\" format",
|
||||
ErrGitHub,
|
||||
)
|
||||
}
|
||||
|
||||
return &Repository{
|
||||
Type: GitHub,
|
||||
Source: ownerAndName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Repository) Owner() string {
|
||||
switch s.Type {
|
||||
case GitHub:
|
||||
return strings.SplitN(s.Source, "/", 2)[0]
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Repository) Name() string {
|
||||
switch s.Type {
|
||||
case GitHub:
|
||||
return strings.SplitN(s.Source, "/", 2)[1]
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Repository) URL() string {
|
||||
switch s.Type {
|
||||
case GitHub:
|
||||
return GitHubBaseURL + s.Source
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Repository) CloneURL() string {
|
||||
switch s.Type {
|
||||
case GitHub:
|
||||
return GitHubBaseURL + s.Source + ".git"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Repository) TarballURL(ref string) string {
|
||||
if ref == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch s.Type {
|
||||
case GitHub:
|
||||
return GitHubBaseURL + s.Source + "/tarball/" + ref
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user