Merge pull request #58 from jimeh/release-improvements

This commit is contained in:
2021-10-10 18:42:37 +01:00
committed by GitHub
15 changed files with 380 additions and 127 deletions

View File

@@ -75,6 +75,9 @@ issues:
- source: "`json:"
linters:
- lll
- source: "`yaml:"
linters:
- lll
run:
skip-dirs:

View File

@@ -53,7 +53,7 @@ class OSVersion
end
def to_s
@to_s ||= "#{major}.#{minor}"
@to_s ||= major >= 11 ? major.to_s : "#{major}.#{minor}"
end
def major

View File

@@ -11,9 +11,11 @@ type ReleaseInfo struct {
Assets map[string]*ReleaseAsset
}
func (s *ReleaseInfo) Asset(nameMatch string) *ReleaseAsset {
if a, ok := s.Assets[nameMatch]; ok {
return a
func (s *ReleaseInfo) Asset(needles ...string) *ReleaseAsset {
if len(needles) == 1 {
if a, ok := s.Assets[needles[0]]; ok {
return a
}
}
// Dirty and inefficient way to ensure assets are searched in a predictable
@@ -27,16 +29,20 @@ func (s *ReleaseInfo) Asset(nameMatch string) *ReleaseAsset {
})
for _, a := range assets {
if strings.Contains(a.Filename, nameMatch) {
return a
for _, needle := range needles {
if !strings.Contains(a.Filename, needle) {
continue
}
}
return a
}
return nil
}
func (s *ReleaseInfo) DownloadURL(nameMatch string) string {
a := s.Asset(nameMatch)
func (s *ReleaseInfo) DownloadURL(needles ...string) string {
a := s.Asset(needles...)
if a == nil {
return ""
}
@@ -44,8 +50,8 @@ func (s *ReleaseInfo) DownloadURL(nameMatch string) string {
return a.DownloadURL
}
func (s *ReleaseInfo) SHA256(nameMatch string) string {
a := s.Asset(nameMatch)
func (s *ReleaseInfo) SHA256(needles ...string) string {
a := s.Asset(needles...)
if a == nil {
return ""
}

View File

@@ -112,7 +112,7 @@ func caskUpdateCmd() *cli2.Command {
func caskUpdateAction(
c *cli2.Context,
opts *Options,
_ *Options,
cOpts *caskOptions,
) error {
updateOpts := &cask.UpdateOptions{

View File

@@ -52,7 +52,7 @@ func notarizeCmd() *cli2.Command {
}
}
func notarizeAction(c *cli2.Context, opts *Options) error {
func notarizeAction(c *cli2.Context, _ *Options) error {
options := &notarize.Options{
File: c.Args().Get(0),
BundleID: c.String("bundle-id"),

View File

@@ -1,6 +1,7 @@
package cli
import (
"fmt"
"os"
"path/filepath"
@@ -37,6 +38,12 @@ func planCmd() *cli2.Command {
Name: "sha",
Usage: "override commit SHA of specified git branch/tag",
},
&cli2.StringFlag{
Name: "format",
Aliases: []string{"f"},
Usage: "output format of build plan (yaml or json)",
Value: "yaml",
},
&cli2.StringFlag{
Name: "output",
Usage: "output filename to write plan to instead of printing " +
@@ -102,7 +109,18 @@ func planAction(c *cli2.Context, opts *Options) error {
return err
}
planYAML, err := p.YAML()
format := c.String("format")
var plan string
switch format {
case "yaml", "yml":
format = "yaml"
plan, err = p.YAML()
case "json":
format = "json"
plan, err = p.JSON()
default:
err = fmt.Errorf("--format must be yaml or json")
}
if err != nil {
return err
}
@@ -111,7 +129,7 @@ func planAction(c *cli2.Context, opts *Options) error {
out = os.Stdout
if f := c.String("output"); f != "" {
logger.Info("writing plan", "file", f)
logger.Debug("content", "yaml", planYAML)
logger.Debug("content", format, plan)
out, err = os.Create(f)
if err != nil {
return err
@@ -119,7 +137,7 @@ func planAction(c *cli2.Context, opts *Options) error {
defer out.Close()
}
_, err = out.WriteString(planYAML)
_, err = out.WriteString(plan)
if err != nil {
return err
}

View File

@@ -43,7 +43,7 @@ func releaseCmd() *cli2.Command {
Usage: "owner/name of GitHub repo to check for release, " +
"ignored if a plan is provided",
EnvVars: []string{"GITHUB_REPOSITORY"},
Value: "jimeh/emacs-builds",
Value: "",
},
&cli2.StringFlag{
Name: "name",
@@ -108,7 +108,7 @@ func releaseCheckCmd() *cli2.Command {
func releaseCheckAction(
c *cli2.Context,
opts *Options,
_ *Options,
rOpts *releaseOptions,
) error {
rlsOpts := &release.CheckOptions{
@@ -153,6 +153,12 @@ func releasePublishCmd() *cli2.Command {
"specified",
Value: "",
},
&cli2.BoolFlag{
Name: "asset-size-check",
Usage: "Do not replace existing asset files if local and " +
"remote file sizes match.",
Value: false,
},
},
Action: releaseActionWrapper(releasePublishAction),
}
@@ -160,16 +166,17 @@ func releasePublishCmd() *cli2.Command {
func releasePublishAction(
c *cli2.Context,
opts *Options,
_ *Options,
rOpts *releaseOptions,
) error {
rlsOpts := &release.PublishOptions{
Repository: rOpts.Repository,
CommitRef: c.String("release-sha"),
ReleaseName: rOpts.Name,
ReleaseTitle: c.String("title"),
AssetFiles: c.Args().Slice(),
GithubToken: rOpts.GithubToken,
Repository: rOpts.Repository,
CommitRef: c.String("release-sha"),
ReleaseName: rOpts.Name,
ReleaseTitle: c.String("title"),
AssetFiles: c.Args().Slice(),
AssetSizeCheck: c.Bool("asset-size-check"),
GithubToken: rOpts.GithubToken,
}
rlsType := c.String("type")
@@ -184,7 +191,13 @@ func releasePublishAction(
return fmt.Errorf("invalid --type \"%s\"", rlsType)
}
if c.Args().Len() > 0 {
rlsOpts.AssetFiles = c.Args().Slice()
}
if rOpts.Plan != nil {
rlsOpts.Source = rOpts.Plan.Source
if rOpts.Plan.Release != nil {
rlsOpts.ReleaseName = rOpts.Plan.Release.Name
rlsOpts.ReleaseTitle = rOpts.Plan.Release.Title
@@ -196,7 +209,8 @@ func releasePublishAction(
}
}
if rOpts.Plan.Output != nil {
// Set asset files based on plan if no file arguments were given.
if len(rlsOpts.AssetFiles) == 0 && rOpts.Plan.Output != nil {
rlsOpts.AssetFiles = []string{
filepath.Join(
rOpts.Plan.Output.Directory,
@@ -235,7 +249,7 @@ func releaseBulkCmd() *cli2.Command {
func releaseBulkAction(
c *cli2.Context,
opts *Options,
_ *Options,
rOpts *releaseOptions,
) error {
bulkOpts := &release.BulkOptions{

View File

@@ -8,11 +8,11 @@ import (
)
type Commit struct {
SHA string `yaml:"sha"`
Date *time.Time `yaml:"date"`
Author string `yaml:"author"`
Committer string `yaml:"committer"`
Message string `yaml:"message"`
SHA string `yaml:"sha" json:"sha"`
Date *time.Time `yaml:"date" json:"date"`
Author string `yaml:"author" json:"author"`
Committer string `yaml:"committer" json:"committer"`
Message string `yaml:"message" json:"message"`
}
func New(rc *github.RepositoryCommit) *Commit {

View File

@@ -2,13 +2,14 @@ package osinfo
import (
"os/exec"
"strconv"
"strings"
)
type OSInfo struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Arch string `yaml:"arch"`
Name string `yaml:"name" json:"name"`
Version string `yaml:"version" json:"version"`
Arch string `yaml:"arch" json:"arch"`
}
func New() (*OSInfo, error) {
@@ -29,8 +30,17 @@ func New() (*OSInfo, error) {
}, nil
}
func (s *OSInfo) MajorMinor() string {
// DistinctVersion returns macOS version down to a distinct "major"
// version. For macOS 10.x, this will include the first two numeric parts of the
// version (10.15), while for 11.x and later, the first numeric part is enough
// (11).
func (s *OSInfo) DistinctVersion() string {
parts := strings.Split(s.Version, ".")
if n, _ := strconv.Atoi(parts[0]); n >= 11 {
return parts[0]
}
max := len(parts)
if max > 2 {
max = 2

View File

@@ -13,6 +13,7 @@ import (
"github.com/jimeh/build-emacs-for-macos/pkg/osinfo"
"github.com/jimeh/build-emacs-for-macos/pkg/release"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
"github.com/jimeh/build-emacs-for-macos/pkg/source"
)
var nonAlphaNum = regexp.MustCompile(`[^\w_-]+`)
@@ -76,7 +77,7 @@ func Create(ctx context.Context, opts *Options) (*Plan, error) {
buildName := fmt.Sprintf(
"Emacs.%s.%s.%s",
version,
sanitizeString(osInfo.Name+"-"+osInfo.MajorMinor()),
sanitizeString(osInfo.Name+"-"+osInfo.DistinctVersion()),
sanitizeString(osInfo.Arch),
)
diskImage := buildName + ".dmg"
@@ -85,11 +86,11 @@ func Create(ctx context.Context, opts *Options) (*Plan, error) {
Build: &Build{
Name: buildName,
},
Source: &Source{
Source: &source.Source{
Ref: opts.Ref,
Repository: repo,
Commit: commitInfo,
Tarball: &Tarball{
Tarball: &source.Tarball{
URL: repo.TarballURL(commitInfo.SHA),
},
},

View File

@@ -2,21 +2,21 @@ package plan
import (
"bytes"
"encoding/json"
"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"
"github.com/jimeh/build-emacs-for-macos/pkg/source"
"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"`
Build *Build `yaml:"build,omitempty" json:"build,omitempty"`
Source *source.Source `yaml:"source,omitempty" json:"source,omitempty"`
OS *osinfo.OSInfo `yaml:"os,omitempty" json:"os,omitempty"`
Release *Release `yaml:"release,omitempty" json:"release,omitempty"`
Output *Output `yaml:"output,omitempty" json:"output,omitempty"`
}
// Load attempts to loads a plan YAML from given filename.
@@ -54,29 +54,37 @@ func (s *Plan) YAML() (string, error) {
return buf.String(), nil
}
// WriteJSON writes plan in JSON format to given io.Writer.
func (s *Plan) WriteJSON(w io.Writer) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(s)
}
// JSON returns plan in JSON format.
func (s *Plan) JSON() (string, error) {
var buf bytes.Buffer
err := s.WriteJSON(&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"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
}
type Release struct {
Name string `yaml:"name"`
Title string `yaml:"title,omitempty"`
Draft bool `yaml:"draft,omitempty"`
Prerelease bool `yaml:"prerelease,omitempty"`
Name string `yaml:"name" json:"name"`
Title string `yaml:"title,omitempty" json:"title,omitempty"`
Draft bool `yaml:"draft,omitempty" json:"draft,omitempty"`
Prerelease bool `yaml:"prerelease,omitempty" json:"prerelease,omitempty"`
}
type Output struct {
Directory string `yaml:"directory,omitempty"`
DiskImage string `yaml:"disk_image,omitempty"`
Directory string `yaml:"directory,omitempty" json:"directory,omitempty"`
DiskImage string `yaml:"disk_image,omitempty" json:"disk_image,omitempty"`
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
"github.com/jimeh/build-emacs-for-macos/pkg/source"
)
type releaseType int
@@ -40,15 +41,26 @@ type PublishOptions struct {
// draft)
ReleaseType releaseType
// Source contains the source used to build the asset files. When set a
// release body/description text will be generated based on source commit
// details.
Source *source.Source
// AssetFiles is a list of files which must all exist in the release for
// the check to pass.
AssetFiles []string
// AssetSizeCheck causes a file size check for any existing asset files on a
// release which have the same filename as a asset we want to upload. If the
// size of the local and remote files are the same, the existing asset file
// is left in place. When this is false, given asset files will always be
// uploaded, replacing any asset files with the same filename.
AssetSizeCheck bool
// GitHubToken is the OAuth token used to talk to the GitHub API.
GithubToken string
}
//nolint:funlen,gocyclo
// Publish creates and publishes a GitHub release.
func Publish(ctx context.Context, opts *PublishOptions) error {
logger := hclog.FromContext(ctx).Named("release")
@@ -68,6 +80,16 @@ func Publish(ctx context.Context, opts *PublishOptions) error {
prerelease := opts.ReleaseType == Prerelease
draft := opts.ReleaseType == Draft
body := ""
if opts.Source != nil {
body, err = releaseBody(opts)
if err != nil {
return err
}
logger.Debug("rendered release body", "content", body)
}
created := false
logger.Info("checking release", "tag", tagName)
release, resp, err := gh.Repositories.GetReleaseByTag(
ctx, opts.Repository.Owner(), opts.Repository.Name(), tagName,
@@ -77,6 +99,7 @@ func Publish(ctx context.Context, opts *PublishOptions) error {
return err
}
created = true
logger.Info("creating release", "tag", tagName, "name", name)
release, _, err = gh.Repositories.CreateRelease(
@@ -87,6 +110,7 @@ func Publish(ctx context.Context, opts *PublishOptions) error {
TargetCommitish: &opts.CommitRef,
Prerelease: boolPtr(false),
Draft: boolPtr(true),
Body: &body,
},
)
if err != nil {
@@ -94,62 +118,9 @@ func Publish(ctx context.Context, opts *PublishOptions) error {
}
}
for _, fileName := range files {
fileIO, err2 := os.Open(fileName)
if err2 != nil {
return err2
}
defer fileIO.Close()
fileInfo, err2 := fileIO.Stat()
if err2 != nil {
return err2
}
fileBaseName := filepath.Base(fileName)
assetExists := false
for _, a := range release.Assets {
if a.GetName() != fileBaseName {
continue
}
if a.GetSize() == int(fileInfo.Size()) {
logger.Info("asset exists with correct size",
"file", fileBaseName,
"local_size", byteCountIEC(fileInfo.Size()),
"remote_size", byteCountIEC(int64(a.GetSize())),
)
assetExists = true
} else {
_, err = gh.Repositories.DeleteReleaseAsset(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
a.GetID(),
)
if err != nil {
return err
}
logger.Info(
"deleted asset with wrong size", "file", fileBaseName,
)
}
}
if !assetExists {
logger.Info("uploading asset",
"file", fileBaseName,
"size", byteCountIEC(fileInfo.Size()),
)
_, _, err2 = gh.Repositories.UploadReleaseAsset(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
release.GetID(),
&github.UploadOptions{Name: fileBaseName},
fileIO,
)
if err2 != nil {
return err2
}
}
err = uploadReleaseAssets(ctx, gh, release, files, opts)
if err != nil {
return err
}
changed := false
@@ -158,6 +129,11 @@ func Publish(ctx context.Context, opts *PublishOptions) error {
changed = true
}
if body != "" && release.GetBody() != body {
release.Body = &body
changed = true
}
if release.GetDraft() != draft {
release.Draft = &draft
changed = true
@@ -169,6 +145,7 @@ func Publish(ctx context.Context, opts *PublishOptions) error {
}
if changed {
logger.Info("updating release attributes", "url", release.GetHTMLURL())
release, _, err = gh.Repositories.EditRelease(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
release.GetID(), release,
@@ -178,13 +155,89 @@ func Publish(ctx context.Context, opts *PublishOptions) error {
}
}
logger.Info("release created", "url", release.GetHTMLURL())
if created {
logger.Info("release created", "url", release.GetHTMLURL())
} else {
logger.Info("release updated", "url", release.GetHTMLURL())
}
return nil
}
func uploadReleaseAssets(
ctx context.Context,
gh *github.Client,
release *github.RepositoryRelease,
fileNames []string,
opts *PublishOptions,
) error {
logger := hclog.FromContext(ctx).Named("release")
for _, fileName := range fileNames {
logger.Debug("processing asset", "file", filepath.Base(fileName))
fileIO, err := os.Open(fileName)
if err != nil {
return err
}
defer fileIO.Close()
fileInfo, err := fileIO.Stat()
if err != nil {
return err
}
fileBaseName := filepath.Base(fileName)
assetExists := false
for _, a := range release.Assets {
if a.GetName() != fileBaseName {
continue
}
if opts.AssetSizeCheck && a.GetSize() == int(fileInfo.Size()) {
logger.Info("asset exists with correct size",
"file", fileBaseName,
"local_size", byteCountIEC(fileInfo.Size()),
"remote_size", byteCountIEC(int64(a.GetSize())),
)
assetExists = true
} else {
logger.Info(
"deleting existing asset", "file", fileBaseName,
)
_, err = gh.Repositories.DeleteReleaseAsset(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
a.GetID(),
)
if err != nil {
return err
}
}
}
if !assetExists {
logger.Info("uploading asset",
"file", fileBaseName,
"size", byteCountIEC(fileInfo.Size()),
)
_, _, err = gh.Repositories.UploadReleaseAsset(
ctx, opts.Repository.Owner(), opts.Repository.Name(),
release.GetID(),
&github.UploadOptions{Name: fileBaseName},
fileIO,
)
if err != nil {
return err
}
}
}
return nil
}
func publishFileList(files []string) ([]string, error) {
var output []string
results := map[string]struct{}{}
for _, file := range files {
var err error
file, err = filepath.Abs(file)
@@ -200,11 +253,10 @@ func publishFileList(files []string) ([]string, error) {
return nil, fmt.Errorf("\"%s\" is not a file", file)
}
output = append(output, file)
results[file] = struct{}{}
sumFile := file + ".sha256"
_, err = os.Stat(sumFile)
fmt.Printf("err: %+v\n", err)
if err != nil {
if os.IsNotExist(err) {
continue
@@ -212,7 +264,12 @@ func publishFileList(files []string) ([]string, error) {
return nil, err
}
output = append(output, sumFile)
results[sumFile] = struct{}{}
}
var output []string
for f := range results {
output = append(output, f)
}
return output, nil

View File

@@ -0,0 +1,80 @@
package release
import (
"bytes"
"os"
"strings"
"text/template"
)
var tplFuncs = template.FuncMap{
"indent": func(n int, s string) string {
pad := strings.Repeat(" ", n)
return pad + strings.ReplaceAll(s, "\n", "\n"+pad)
},
}
var bodyTpl = template.Must(template.New("body").Funcs(tplFuncs).Parse(`
{{- $t := "` + "`" + `" -}}
### Build Details
{{ with .SourceURL -}}
- Source: {{ . }}
{{- end }}
{{- if and .CommitSHA .CommitURL }}
- Commit: [{{ $t }}{{ .CommitSHA }}{{ $t }}]
{{- if .CommitURL }}({{ .CommitURL }}){{ end }}
{{- else if and .CommitSHA }}
- Commit: {{ $t }}{{ .CommitSHA }}{{ $t }}
{{- end }}
{{- with .TarballURL }}
- Tarball: {{ . }}
{{- end }}
{{- with .BuildLogURL }}
- Build Log: {{ . }} (available for 90 days)
{{- end }}`,
))
type bodyData struct {
SourceURL string
CommitSHA string
CommitURL string
BuildLogURL string
TarballURL string
}
func releaseBody(opts *PublishOptions) (string, error) {
src := opts.Source
if src.Repository == nil || src.Commit == nil {
return "", nil
}
data := &bodyData{
SourceURL: src.Repository.TreeURL(src.Ref),
CommitSHA: src.Commit.SHA,
CommitURL: src.Repository.CommitURL(src.Commit.SHA),
TarballURL: src.Repository.TarballURL(src.Commit.SHA),
}
// If available, use the exact value from the build plan.
if src.Tarball != nil {
data.TarballURL = src.Tarball.URL
}
// If running within GitHub Actions, provide link to build log.
if opts.Repository != nil {
if id := os.Getenv("GITHUB_RUN_ID"); id != "" {
data.BuildLogURL = opts.Repository.ActionRunURL(id)
}
}
var buf bytes.Buffer
err := bodyTpl.Execute(&buf, data)
if err != nil {
return "", err
}
return buf.String(), nil
}

View File

@@ -22,8 +22,8 @@ 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"`
Type Type `yaml:"type,omitempty" json:"type,omitempty"`
Source string `yaml:"source,omitempty" json:"source,omitempty"`
}
func NewGitHub(ownerAndName string) (*Repository, error) {
@@ -89,3 +89,42 @@ func (s *Repository) TarballURL(ref string) string {
return ""
}
}
func (s *Repository) CommitURL(ref string) string {
if ref == "" {
return ""
}
switch s.Type {
case GitHub:
return GitHubBaseURL + s.Source + "/commit/" + ref
default:
return ""
}
}
func (s *Repository) TreeURL(ref string) string {
if ref == "" {
return ""
}
switch s.Type {
case GitHub:
return GitHubBaseURL + s.Source + "/tree/" + ref
default:
return ""
}
}
func (s *Repository) ActionRunURL(runID string) string {
if runID == "" {
return ""
}
switch s.Type {
case GitHub:
return GitHubBaseURL + s.Source + "/actions/runs/" + runID
default:
return ""
}
}

17
pkg/source/source.go Normal file
View File

@@ -0,0 +1,17 @@
package source
import (
"github.com/jimeh/build-emacs-for-macos/pkg/commit"
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
)
type Source struct {
Ref string `yaml:"ref,omitempty" json:"ref,omitempty"`
Repository *repository.Repository `yaml:"repository,omitempty" json:"repository,omitempty"`
Commit *commit.Commit `yaml:"commit,omitempty" json:"commit,omitempty"`
Tarball *Tarball `yaml:"tarball,omitempty" json:"tarball,omitempty"`
}
type Tarball struct {
URL string `yaml:"url,omitempty" json:"url,omitempty"`
}