Files
build-emacs-for-macos/pkg/release/publish.go
Jim Myhrberg 7118ed8560 feat(release): add description to GitHub Releases
The description includes links to the Emacs source repo used, the git
ref, commit, tarball download URL, and build log (GitHub Actions Run).
2021-10-10 18:25:36 +01:00

269 lines
5.6 KiB
Go

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"
"github.com/jimeh/build-emacs-for-macos/pkg/source"
)
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
// 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
// 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
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,
)
if err != nil {
if resp.StatusCode != http.StatusNotFound {
return err
}
created = true
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),
Body: &body,
},
)
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 body != "" && release.GetBody() != body {
release.Body = &body
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
}
}
if created {
logger.Info("release created", "url", release.GetHTMLURL())
} else {
logger.Info("release updated", "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)
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
}