mirror of
https://github.com/jimeh/emacs-builds.git
synced 2026-02-19 13:06:40 +00:00
feat(builds): add custom github-release tool
This tool is responsible for three distinct operations:
- Plan:
- Accepts a optional git ref (branch/tag), defaulting to "master"
branch if not specified.
- Fetches commit information about the git-ref from the
emacs-mirror/emacs GitHub repo.
- Based on commit info, a plan.yml file is created, key information in
it is the git ref, git SHA, date of commit, current macOS version
and CPU architecture.
- Check:
- Loads plan.yml and checks if GitHub Release name specified in plan
exists, exiting with an error if it does not exist.
- Checks if GitHub Release contains a asset file matching the name of
the archive specified in the plan, exiting with an error if it does
not exist.
- If both GitHub Release and asset exist, it exits with not errors.
- Release:
- Loads plan.yml and checks if archive specified in plan exists
on disk.
- Checks if GitHub Release name specified in plan exists, creating it
if it does not exist.
- Checks if GitHub Release contains a asset file matching that of
archive specified in plan. If the asset it missing, it will be
uploaded. If it exists but size does not match that of archive on
disk, the existing asset will be replaced.
This commit is contained in:
56
cmd/github-release/check_cmd.go
Normal file
56
cmd/github-release/check_cmd.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func checkCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "check",
|
||||
Usage: "Check if GitHub release and asset exists",
|
||||
UsageText: "github-release [global options] check [options]",
|
||||
Action: actionHandler(checkAction),
|
||||
}
|
||||
}
|
||||
|
||||
func checkAction(c *cli.Context, opts *globalOptions) error {
|
||||
gh := opts.gh
|
||||
repo := opts.repo
|
||||
plan, err := LoadPlan(opts.plan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
"==> Checking github.com/%s for release: %s\n",
|
||||
repo.String(), plan.Release,
|
||||
)
|
||||
|
||||
release, resp, err := gh.Repositories.GetReleaseByTag(
|
||||
c.Context, repo.Owner, repo.Name, plan.Release,
|
||||
)
|
||||
if err != nil {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return fmt.Errorf("release %s does not exist", plan.Release)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(" -> Release exists")
|
||||
|
||||
filename := plan.ReleaseAsset()
|
||||
|
||||
fmt.Printf("==> Checking release for asset: %s\n", filename)
|
||||
for _, a := range release.Assets {
|
||||
if a.Name != nil && filename == *a.Name {
|
||||
fmt.Println(" -> Asset exists")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("release does contain asset: %s", filename)
|
||||
}
|
||||
46
cmd/github-release/commit.go
Normal file
46
cmd/github-release/commit.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v35/github"
|
||||
)
|
||||
|
||||
type Commit struct {
|
||||
Repo *Repo `yaml:"repo"`
|
||||
Ref string `yaml:"ref"`
|
||||
SHA string `yaml:"sha"`
|
||||
Date *time.Time `yaml:"date"`
|
||||
Author string `yaml:"author"`
|
||||
Committer string `yaml:"committer"`
|
||||
Message string `yaml:"message"`
|
||||
}
|
||||
|
||||
func NewCommit(repo *Repo, ref string, rc *github.RepositoryCommit) *Commit {
|
||||
return &Commit{
|
||||
Repo: repo,
|
||||
Ref: ref,
|
||||
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")
|
||||
}
|
||||
22
cmd/github-release/github.go
Normal file
22
cmd/github-release/github.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/go-github/v35/github"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func NewGitHubClient(ctx context.Context, token string) *github.Client {
|
||||
var tc *http.Client
|
||||
|
||||
if token != "" {
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
)
|
||||
tc = oauth2.NewClient(ctx, ts)
|
||||
}
|
||||
|
||||
return github.NewClient(tc)
|
||||
}
|
||||
74
cmd/github-release/main.go
Normal file
74
cmd/github-release/main.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-github/v35/github"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var app = &cli.App{
|
||||
Name: "github-release",
|
||||
Usage: "manage GitHub releases",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "github-token",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "GitHub API Token (read from GITHUB_TOKEN if set)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "release-repo",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "Owner/name of GitHub repo to publish releases to",
|
||||
EnvVars: []string{"GITHUB_REPOSITORY"},
|
||||
Value: "jimeh/emacs-builds",
|
||||
},
|
||||
&cli.PathFlag{
|
||||
Name: "plan",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "Load plan from `FILE`",
|
||||
EnvVars: []string{"BUILD_PLAN"},
|
||||
Required: true,
|
||||
TakesFile: true,
|
||||
},
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
checkCmd(),
|
||||
publishCmd(),
|
||||
planCmd(),
|
||||
},
|
||||
}
|
||||
|
||||
type globalOptions struct {
|
||||
gh *github.Client
|
||||
repo *Repo
|
||||
plan string
|
||||
}
|
||||
|
||||
func actionHandler(
|
||||
f func(*cli.Context, *globalOptions) error,
|
||||
) func(*cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
token := c.String("github-token")
|
||||
if t := os.Getenv("GITHUB_TOKEN"); t != "" {
|
||||
token = t
|
||||
}
|
||||
|
||||
opts := &globalOptions{
|
||||
gh: NewGitHubClient(c.Context, token),
|
||||
repo: NewRepo(c.String("release-repo")),
|
||||
plan: c.String("plan"),
|
||||
}
|
||||
|
||||
return f(c, opts)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
40
cmd/github-release/os_info.go
Normal file
40
cmd/github-release/os_info.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OSInfo struct {
|
||||
Name string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
Arch string `yaml:"arch"`
|
||||
}
|
||||
|
||||
func NewOSInfo() (*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) ShortVersion() string {
|
||||
parts := strings.Split(s.Version, ".")
|
||||
max := len(parts)
|
||||
if max > 2 {
|
||||
max = 2
|
||||
}
|
||||
|
||||
return strings.Join(parts[0:max], ".")
|
||||
}
|
||||
31
cmd/github-release/plan.go
Normal file
31
cmd/github-release/plan.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Plan struct {
|
||||
Commit *Commit `yaml:"commit"`
|
||||
OS *OSInfo `yaml:"os"`
|
||||
Release string `yaml:"release"`
|
||||
Archive string `yaml:"archive"`
|
||||
}
|
||||
|
||||
func LoadPlan(filename string) (*Plan, error) {
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plan := &Plan{}
|
||||
err = yaml.Unmarshal(b, plan)
|
||||
|
||||
return plan, err
|
||||
}
|
||||
|
||||
func (s *Plan) ReleaseAsset() string {
|
||||
return filepath.Base(s.Archive)
|
||||
}
|
||||
113
cmd/github-release/plan_cmd.go
Normal file
113
cmd/github-release/plan_cmd.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var nonAlphaNum = regexp.MustCompile(`[^\w-_]+`)
|
||||
|
||||
func planCmd() *cli.Command {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
wd = ""
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
Name: "plan",
|
||||
Usage: "Plan if GitHub release and asset exists",
|
||||
UsageText: "github-release [global options] plan [<branch/tag>]",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "emacs-mirror-repo",
|
||||
Usage: "Github owner/repo to get Emacs commit info from",
|
||||
Aliases: []string{"e"},
|
||||
EnvVars: []string{"EMACS_MIRROR_REPO"},
|
||||
Value: "emacs-mirror/emacs",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "work-dir",
|
||||
Usage: "Github owner/repo to get Emacs commit info from",
|
||||
Value: wd,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "sha",
|
||||
Usage: "Override commit SHA of specified git branch/tag",
|
||||
},
|
||||
},
|
||||
Action: actionHandler(planAction),
|
||||
}
|
||||
}
|
||||
|
||||
func planAction(c *cli.Context, opts *globalOptions) error {
|
||||
gh := opts.gh
|
||||
planFile := opts.plan
|
||||
repo := NewRepo(c.String("emacs-mirror-repo"))
|
||||
buildsDir := filepath.Join(c.String("work-dir"), "builds")
|
||||
|
||||
ref := c.Args().Get(0)
|
||||
if ref == "" {
|
||||
ref = "master"
|
||||
}
|
||||
|
||||
lookupRef := ref
|
||||
if s := c.String("sha"); s != "" {
|
||||
lookupRef = s
|
||||
}
|
||||
|
||||
repoCommit, _, err := gh.Repositories.GetCommit(
|
||||
c.Context, repo.Owner, repo.Name, lookupRef,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rb, _ := yaml.Marshal(repoCommit)
|
||||
fmt.Printf("commit:\n---\n%s\n", string(rb))
|
||||
|
||||
commit := NewCommit(repo, ref, repoCommit)
|
||||
osInfo, err := NewOSInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cleanRef := sanitizeString(ref)
|
||||
cleanOS := sanitizeString(osInfo.Name + "-" + osInfo.ShortVersion())
|
||||
cleanArch := sanitizeString(osInfo.Arch)
|
||||
|
||||
releaseName := fmt.Sprintf(
|
||||
"Emacs.%s.%s.%s",
|
||||
commit.DateString(), commit.ShortSHA(), cleanRef,
|
||||
)
|
||||
archiveName := fmt.Sprintf(
|
||||
"Emacs.%s.%s.%s.%s.%s.tbz",
|
||||
commit.DateString(), commit.ShortSHA(), cleanRef, cleanOS, cleanArch,
|
||||
)
|
||||
|
||||
plan := &Plan{
|
||||
Commit: commit,
|
||||
OS: osInfo,
|
||||
Release: releaseName,
|
||||
Archive: filepath.Join(buildsDir, archiveName),
|
||||
}
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
err = enc.Encode(plan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(planFile, buf.Bytes(), 0666)
|
||||
}
|
||||
|
||||
func sanitizeString(s string) string {
|
||||
return nonAlphaNum.ReplaceAllString(s, "-")
|
||||
}
|
||||
132
cmd/github-release/publish_cmd.go
Normal file
132
cmd/github-release/publish_cmd.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-github/v35/github"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func publishCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "publish",
|
||||
Usage: "publish a release",
|
||||
UsageText: "github-release [global-options] publish [options]",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "release-sha",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Git SHA of repo to create release on",
|
||||
EnvVars: []string{"GITHUB_SHA"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "prerelease",
|
||||
Usage: "Git SHA of repo to create release on",
|
||||
EnvVars: []string{"RELEASE_PRERELEASE"},
|
||||
Value: true,
|
||||
},
|
||||
},
|
||||
Action: actionHandler(publishAction),
|
||||
}
|
||||
}
|
||||
|
||||
func publishAction(c *cli.Context, opts *globalOptions) error {
|
||||
gh := opts.gh
|
||||
repo := opts.repo
|
||||
plan, err := LoadPlan(opts.plan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releaseSHA := c.String("release-sha")
|
||||
prerelease := c.Bool("prerelease")
|
||||
|
||||
assetFile, err := os.Open(plan.Archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assetInfo, err := assetFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("==> Checking release %s\n", plan.Release)
|
||||
|
||||
release, resp, err := gh.Repositories.GetReleaseByTag(
|
||||
c.Context, repo.Owner, repo.Name, plan.Release,
|
||||
)
|
||||
if err != nil {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
fmt.Println(" -> Release not found, creating...")
|
||||
release, _, err = gh.Repositories.CreateRelease(
|
||||
c.Context, repo.Owner, repo.Name, &github.RepositoryRelease{
|
||||
Name: &plan.Release,
|
||||
TagName: &plan.Release,
|
||||
TargetCommitish: &releaseSHA,
|
||||
Prerelease: &prerelease,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if release.GetPrerelease() != prerelease {
|
||||
release.Prerelease = &prerelease
|
||||
|
||||
release, _, err = gh.Repositories.EditRelease(
|
||||
c.Context, repo.Owner, repo.Name, release.GetID(), release,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
assetFilename := plan.ReleaseAsset()
|
||||
assetExists := false
|
||||
|
||||
fmt.Printf("==> Checking asset %s\n", assetFilename)
|
||||
|
||||
for _, a := range release.Assets {
|
||||
if a.GetName() != assetFilename {
|
||||
continue
|
||||
}
|
||||
|
||||
if a.GetSize() == int(assetInfo.Size()) {
|
||||
fmt.Println(" -> Asset already exists")
|
||||
assetExists = true
|
||||
} else {
|
||||
fmt.Println(" -> Asset exists with wrong file size, deleting...")
|
||||
_, err := gh.Repositories.DeleteReleaseAsset(
|
||||
c.Context, repo.Owner, repo.Name, a.GetID(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" -> Done")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !assetExists {
|
||||
fmt.Println(" -> Asset missing, uploading...")
|
||||
_, _, err = gh.Repositories.UploadReleaseAsset(
|
||||
c.Context, repo.Owner, repo.Name, release.GetID(),
|
||||
&github.UploadOptions{Name: assetFilename},
|
||||
assetFile,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" -> Done")
|
||||
}
|
||||
|
||||
fmt.Printf("==> Release available at: %s\n", release.GetHTMLURL())
|
||||
|
||||
return nil
|
||||
}
|
||||
26
cmd/github-release/repo.go
Normal file
26
cmd/github-release/repo.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Repo struct {
|
||||
URL string `yaml:"url"`
|
||||
Owner string `yaml:"owner"`
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
func NewRepo(ownerAndRepo string) *Repo {
|
||||
parts := strings.SplitN(ownerAndRepo, "/", 2)
|
||||
|
||||
return &Repo{
|
||||
URL: fmt.Sprintf("https://github.com/%s/%s", parts[0], parts[1]),
|
||||
Owner: parts[0],
|
||||
Name: parts[1],
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Repo) String() string {
|
||||
return s.Owner + "/" + s.Name
|
||||
}
|
||||
Reference in New Issue
Block a user