mirror of
https://github.com/jimeh/build-emacs-for-macos.git
synced 2026-02-19 02:36:39 +00:00
feat(release): add release publish command
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/plan"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/release"
|
||||
@@ -57,6 +59,7 @@ func releaseCmd() *cli2.Command {
|
||||
},
|
||||
Subcommands: []*cli2.Command{
|
||||
releaseCheckCmd(),
|
||||
releasePublishCmd(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -122,3 +125,84 @@ func releaseCheckAction(
|
||||
|
||||
return release.Check(c.Context, rlsOpts)
|
||||
}
|
||||
|
||||
func releasePublishCmd() *cli2.Command {
|
||||
return &cli2.Command{
|
||||
Name: "publish",
|
||||
Usage: "publish a GitHub release with specified asset " +
|
||||
"files",
|
||||
ArgsUsage: "[<asset-file> ...]",
|
||||
Flags: []cli2.Flag{
|
||||
&cli2.StringFlag{
|
||||
Name: "sha",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "git SHA to create release on",
|
||||
EnvVars: []string{"GITHUB_SHA"},
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "release type, must be normal, prerelease, or draft",
|
||||
Value: "normal",
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "title",
|
||||
Usage: "release title, will use release name if not " +
|
||||
"specified",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
Action: releaseActionWrapper(releasePublishAction),
|
||||
}
|
||||
}
|
||||
|
||||
func releasePublishAction(
|
||||
c *cli2.Context,
|
||||
opts *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,
|
||||
}
|
||||
|
||||
rlsType := c.String("type")
|
||||
switch rlsType {
|
||||
case "draft":
|
||||
rlsOpts.ReleaseType = release.Draft
|
||||
case "prerelease":
|
||||
rlsOpts.ReleaseType = release.Prerelease
|
||||
case "normal":
|
||||
rlsOpts.ReleaseType = release.Normal
|
||||
default:
|
||||
return fmt.Errorf("invalid --type \"%s\"", rlsType)
|
||||
}
|
||||
|
||||
if rOpts.Plan != nil {
|
||||
if rOpts.Plan.Release != nil {
|
||||
rlsOpts.ReleaseName = rOpts.Plan.Release.Name
|
||||
rlsOpts.ReleaseTitle = rOpts.Plan.Release.Title
|
||||
|
||||
if rOpts.Plan.Release.Draft {
|
||||
rlsOpts.ReleaseType = release.Draft
|
||||
} else if rOpts.Plan.Release.Prerelease {
|
||||
rlsOpts.ReleaseType = release.Prerelease
|
||||
}
|
||||
}
|
||||
|
||||
if rOpts.Plan.Output != nil {
|
||||
rlsOpts.AssetFiles = []string{
|
||||
filepath.Join(
|
||||
rOpts.Plan.Output.Directory,
|
||||
rOpts.Plan.Output.DiskImage,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return release.Publish(c.Context, rlsOpts)
|
||||
}
|
||||
|
||||
242
pkg/release/publish.go
Normal file
242
pkg/release/publish.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package release
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/go-github/v35/github"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
|
||||
)
|
||||
|
||||
type releaseType int
|
||||
|
||||
// Release type constants
|
||||
const (
|
||||
Normal releaseType = iota
|
||||
Draft
|
||||
Prerelease
|
||||
)
|
||||
|
||||
type PublishOptions struct {
|
||||
// Repository is the GitHub repository to publish the release on.
|
||||
Repository *repository.Repository
|
||||
|
||||
// CommitRef is the git commit ref to create the release tag on.
|
||||
CommitRef string
|
||||
|
||||
// ReleaseName is the name of the git tag to create the release on.
|
||||
ReleaseName string
|
||||
|
||||
// ReleaseTitle is the title of the release, if not specified ReleaseName
|
||||
// will be used.
|
||||
ReleaseTitle string
|
||||
|
||||
// ReleaseType is the type of release to create (normal, prerelease, or
|
||||
// draft)
|
||||
ReleaseType releaseType
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
//nolint:funlen,gocyclo
|
||||
// Publish creates and publishes a GitHub release.
|
||||
func Publish(ctx context.Context, opts *PublishOptions) error {
|
||||
logger := hclog.FromContext(ctx).Named("release")
|
||||
gh := gh.New(ctx, opts.GithubToken)
|
||||
|
||||
files, err := publishFileList(opts.AssetFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tagName := opts.ReleaseName
|
||||
name := opts.ReleaseTitle
|
||||
if name == "" {
|
||||
name = tagName
|
||||
}
|
||||
|
||||
prerelease := opts.ReleaseType == Prerelease
|
||||
draft := opts.ReleaseType == Draft
|
||||
|
||||
logger.Info("checking release", "tag", tagName)
|
||||
release, resp, err := gh.Repositories.GetReleaseByTag(
|
||||
ctx, opts.Repository.Owner(), opts.Repository.Name(), tagName,
|
||||
)
|
||||
if err != nil {
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("creating release", "tag", tagName, "name", name)
|
||||
|
||||
release, _, err = gh.Repositories.CreateRelease(
|
||||
ctx, opts.Repository.Owner(), opts.Repository.Name(),
|
||||
&github.RepositoryRelease{
|
||||
Name: &name,
|
||||
TagName: &tagName,
|
||||
TargetCommitish: &opts.CommitRef,
|
||||
Prerelease: boolPtr(false),
|
||||
Draft: boolPtr(true),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changed := false
|
||||
if release.GetName() != name {
|
||||
release.Name = &name
|
||||
changed = true
|
||||
}
|
||||
|
||||
if release.GetDraft() != draft {
|
||||
release.Draft = &draft
|
||||
changed = true
|
||||
}
|
||||
|
||||
if !draft && release.GetPrerelease() != prerelease {
|
||||
release.Prerelease = &prerelease
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
release, _, err = gh.Repositories.EditRelease(
|
||||
ctx, opts.Repository.Owner(), opts.Repository.Name(),
|
||||
release.GetID(), release,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("release created", "url", release.GetHTMLURL())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishFileList(files []string) ([]string, error) {
|
||||
var output []string
|
||||
for _, file := range files {
|
||||
var err error
|
||||
file, err = filepath.Abs(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stat, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !stat.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("\"%s\" is not a file", file)
|
||||
}
|
||||
|
||||
output = append(output, file)
|
||||
sumFile := file + ".sha256"
|
||||
|
||||
_, err = os.Stat(sumFile)
|
||||
fmt.Printf("err: %+v\n", err)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
output = append(output, sumFile)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func byteCountIEC(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
if b == 1 {
|
||||
return fmt.Sprintf("%d byte", b)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d bytes", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.1f %ciB",
|
||||
float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
func boolPtr(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
Reference in New Issue
Block a user