feat(signing)!: sign, notarize and staple Emacs.app and disk image

Update the build-emacs-for-macos to v0.5.0, which includes a new
emacs-builder CLI tool written in Go, which handles signing, dmg
packaging, notarizing and stapling of Emacs builds. The main build
process is still handled by the old Ruby script for now however.

emacs-builder also includes plan and release commands, negating the need
for the our custom github-release CLI tool.

BREAKING CHANGE: Release assets are now signed *.dmg files instead
of *.tbz archives.
This commit is contained in:
2021-06-20 20:45:03 +01:00
parent ffad4f21d2
commit ca4e7e2c44
11 changed files with 285 additions and 711 deletions

View File

@@ -13,74 +13,182 @@ on:
Description: "Extra plan args"
required: false
default: ""
extraCheckArgs:
Description: "Extra check args"
required: false
default: ""
extraBuildArgs:
Description: "Extra build args"
required: false
default: ""
extraPackageArgs:
Description: "Extra package args"
required: false
default: ""
extraReleaseArgs:
Description: "Extra release args"
required: false
default: ""
jobs:
build-and-publish:
plan:
runs-on: macos-10.15
outputs:
check: "${{ steps.check.outcome }}"
steps:
- name: Checkout emacs-builds repo
uses: actions/checkout@v2
with:
path: releaser
- name: Checkout build-emacs-for-macos repo
uses: actions/checkout@v2
with:
repository: jimeh/build-emacs-for-macos
ref: "0.4.16"
ref: "v0.5.0"
path: builder
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Compile github-release tool
run: >-
go build -o ./github-release ./cmd/github-release
working-directory: releaser
- name: Pre-build emacs-builder tool
run: make build
working-directory: builder
- name: Plan build
run: >-
./releaser/github-release --plan plan.yml plan
--work-dir '${{ github.workspace }}'
builder/bin/emacs-builder -l debug plan
--output build-plan.yml
--output-dir '${{ github.workspace }}/builds'
${{ github.event.inputs.extraPlanArgs }}
${{ github.event.inputs.gitRef }}
'${{ github.event.inputs.gitRef }}'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Show plan
run: >-
cat plan.yml
run: cat build-plan.yml
- name: Check if planned release and asset already exist
id: check
continue-on-error: true
run: >-
./releaser/github-release --plan plan.yml check
builder/bin/emacs-builder -l debug release --plan build-plan.yml check
${{ github.event.inputs.extraCheckArgs }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload pre-built emacs-builder artifact
uses: actions/upload-artifact@v2
with:
name: emacs-builder
path: builder/bin/emacs-builder
if-no-files-found: error
- name: Upload build-plan.yml artifact
uses: actions/upload-artifact@v2
with:
name: build-plan.yml
path: build-plan.yml
if-no-files-found: error
build:
runs-on: macos-10.15
needs: [plan]
# Only run if check for existing release and asset failed.
if: ${{ needs.plan.outputs.check == 'failure' }}
steps:
- name: Checkout build-emacs-for-macos repo
uses: actions/checkout@v2
with:
repository: jimeh/build-emacs-for-macos
ref: "v0.5.0"
path: builder
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- name: Install dependencies
if: steps.check.outcome == 'failure'
run: >-
brew bundle --file=builder/Brewfile
run: make bootstrap-ci
working-directory: builder
- name: Download pre-built emacs-builder artifact
uses: actions/download-artifact@v2
id: builder
with:
name: emacs-builder
path: bin
- name: Ensure emacs-builder is executable
run: chmod +x bin/emacs-builder
- name: Download build-plan.yml artifact
uses: actions/download-artifact@v2
id: plan
with:
name: build-plan.yml
path: ./
- name: Build Emacs
if: steps.check.outcome == 'failure'
run: >-
./builder/build-emacs-for-macos --plan=plan.yml
--work-dir '${{ github.workspace }}'
--native-full-aot
${{ github.event.inputs.extraReleaseArgs }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish release
if: steps.check.outcome == 'failure'
run: >-
./releaser/github-release --plan plan.yml publish
./builder/build-emacs-for-macos --plan build-plan.yml
--native-full-aot ${{ github.event.inputs.extraBuildArgs }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install the Apple signing certificate
run: |
# create variables
CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
# import certificate and provisioning profile from secrets
echo -n "$CERT_BASE64" | base64 --decode --output "$CERTIFICATE_PATH"
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# import certificate to keychain
security import "$CERTIFICATE_PATH" -P "$CERT_PASSWORD" -A \
-t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
env:
CERT_BASE64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
CERT_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Sign, package and notarize build
run: >-
bin/emacs-builder -l debug package -v --plan build-plan.yml
--sign --remove-source-dir
${{ github.event.inputs.extraPackageArgs }}
env:
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
AC_PROVIDER: ${{ secrets.AC_PROVIDER }}
AC_SIGN_IDENTITY: ${{ secrets.AC_SIGN_IDENTITY }}
- name: Upload disk image artifacts
uses: actions/upload-artifact@v2
with:
name: dmg
path: |
builds/*.dmg
builds/*.sha*
if-no-files-found: error
- name: Clean up keychain used for signing certificate
if: ${{ always() }}
run: |
security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db"
release:
runs-on: macos-10.15
needs: [build]
steps:
- name: Download pre-built emacs-builder artifact
uses: actions/download-artifact@v2
id: builder
with:
name: emacs-builder
path: bin
- name: Ensure emacs-builder is executable
run: chmod +x bin/emacs-builder
- name: Download build-plan.yml artifact
uses: actions/download-artifact@v2
id: plan
with:
name: build-plan.yml
path: ./
- name: Download disk image artifact
uses: actions/download-artifact@v2
with:
name: dmg
path: builds
- name: Publish disk image to GitHub Release
run: >-
bin/emacs-builder -l debug release --plan build-plan.yml publish
${{ github.event.inputs.extraReleaseArgs }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -7,18 +7,13 @@ on:
description: "Emacs git ref to build"
required: true
default: "master"
releaseToolGitRef:
description: "Git ref to checkout of emacs-builds (github-release)"
required: true
default: "main"
buildScriptGitRef:
emacsBuilderGitRef:
description: "Git ref to checkout of build-emacs-for-macos"
required: true
default: "master"
testBuildName:
description: "Test build name"
required: false
default: ""
required: true
testReleaseType:
description: "prerelease or draft"
required: true
@@ -27,77 +22,184 @@ on:
Description: "Extra plan args"
required: false
default: ""
extraCheckArgs:
Description: "Extra check args"
required: false
default: ""
extraBuildArgs:
Description: "Extra build args"
required: false
default: ""
extraPackageArgs:
Description: "Extra package args"
required: false
default: ""
extraReleaseArgs:
Description: "Extra release args"
required: false
default: ""
jobs:
build-and-publish:
plan:
runs-on: macos-10.15
outputs:
check: "${{ steps.check.outcome }}"
steps:
- name: Checkout emacs-builds repo
uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.releaseToolGitRef }}
path: releaser
- name: Checkout build-emacs-for-macos repo
uses: actions/checkout@v2
with:
repository: jimeh/build-emacs-for-macos
ref: ${{ github.event.inputs.buildScriptGitRef }}
ref: ${{ github.event.inputs.emacsBuilderGitRef }}
path: builder
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Compile github-release tool
run: >-
go build -o ./github-release ./cmd/github-release
working-directory: releaser
- name: Pre-build emacs-builder tool
run: make build
working-directory: builder
- name: Plan build
run: >-
./releaser/github-release --plan plan.yml plan
--work-dir '${{ github.workspace }}'
--test-build
--test-build-name="${{ github.event.inputs.testBuildName }}"
--test-release-type="${{ github.event.inputs.testReleaseType }}"
builder/bin/emacs-builder -l debug plan
--output build-plan.yml
--output-dir '${{ github.workspace }}/builds'
--test-build '${{ github.event.inputs.testBuildName }}'
--test-release-type '${{ github.event.inputs.testReleaseType }}'
${{ github.event.inputs.extraPlanArgs }}
${{ github.event.inputs.gitRef }}
'${{ github.event.inputs.gitRef }}'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Show plan
run: >-
cat plan.yml
run: cat build-plan.yml
- name: Check if planned release and asset already exist
id: check
continue-on-error: true
run: >-
./releaser/github-release --plan plan.yml check
builder/bin/emacs-builder -l debug release --plan build-plan.yml check
${{ github.event.inputs.extraCheckArgs }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload pre-built emacs-builder artifact
uses: actions/upload-artifact@v2
with:
name: emacs-builder
path: builder/bin/emacs-builder
if-no-files-found: error
- name: Upload build-plan.yml artifact
uses: actions/upload-artifact@v2
with:
name: build-plan.yml
path: build-plan.yml
if-no-files-found: error
build:
runs-on: macos-10.15
needs: [plan]
# Only run if check for existing release and asset failed.
if: ${{ needs.plan.outputs.check == 'failure' }}
steps:
- name: Checkout build-emacs-for-macos repo
uses: actions/checkout@v2
with:
repository: jimeh/build-emacs-for-macos
ref: ${{ github.event.inputs.emacsBuilderGitRef }}
path: builder
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- name: Install dependencies
if: steps.check.outcome == 'failure'
run: >-
brew bundle --file=builder/Brewfile
run: make bootstrap-ci
working-directory: builder
- name: Download pre-built emacs-builder artifact
uses: actions/download-artifact@v2
id: builder
with:
name: emacs-builder
path: bin
- name: Ensure emacs-builder is executable
run: chmod +x bin/emacs-builder
- name: Download build-plan.yml artifact
uses: actions/download-artifact@v2
id: plan
with:
name: build-plan.yml
path: ./
- name: Build Emacs
if: steps.check.outcome == 'failure'
run: >-
./builder/build-emacs-for-macos --plan=plan.yml
--work-dir '${{ github.workspace }}'
--native-full-aot
${{ github.event.inputs.extraReleaseArgs }}
- name: Publish release
if: steps.check.outcome == 'failure'
./builder/build-emacs-for-macos --plan build-plan.yml
--native-full-aot ${{ github.event.inputs.extraBuildArgs }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install the Apple signing certificate
run: |
# create variables
CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
# import certificate and provisioning profile from secrets
echo -n "$CERT_BASE64" | base64 --decode --output "$CERTIFICATE_PATH"
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# import certificate to keychain
security import "$CERTIFICATE_PATH" -P "$CERT_PASSWORD" -A \
-t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
env:
CERT_BASE64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
CERT_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Sign, package and notarize build
run: >-
./releaser/github-release --plan plan.yml publish
--release-sha="${{ github.event.inputs.releaseToolGitRef }}"
bin/emacs-builder -l debug package -v --plan build-plan.yml
--sign --remove-source-dir
${{ github.event.inputs.extraPackageArgs }}
env:
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
AC_PROVIDER: ${{ secrets.AC_PROVIDER }}
AC_SIGN_IDENTITY: ${{ secrets.AC_SIGN_IDENTITY }}
- name: Upload disk image artifacts
uses: actions/upload-artifact@v2
with:
name: dmg
path: |
builds/*.dmg
builds/*.sha*
if-no-files-found: error
- name: Clean up keychain used for signing certificate
if: ${{ always() }}
run: |
security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db"
release:
runs-on: macos-10.15
needs: [build]
steps:
- name: Download pre-built emacs-builder artifact
uses: actions/download-artifact@v2
id: builder
with:
name: emacs-builder
path: bin
- name: Ensure emacs-builder is executable
run: chmod +x bin/emacs-builder
- name: Download build-plan.yml artifact
uses: actions/download-artifact@v2
id: plan
with:
name: build-plan.yml
path: ./
- name: Download disk image artifact
uses: actions/download-artifact@v2
with:
name: dmg
path: builds
- name: Publish disk image to GitHub Release
run: >-
bin/emacs-builder -l debug release --plan build-plan.yml publish
${{ github.event.inputs.extraReleaseArgs }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,57 +0,0 @@
package main
import (
"fmt"
"net/http"
"path/filepath"
"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.Name,
)
release, resp, err := gh.Repositories.GetReleaseByTag(
c.Context, repo.Owner, repo.Name, plan.Release.Name,
)
if err != nil {
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("release %s does not exist", plan.Release.Name)
} else {
return err
}
}
fmt.Println(" -> Release exists")
filename := filepath.Base(plan.Archive)
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)
}

View File

@@ -1,46 +0,0 @@
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")
}

View File

@@ -1,22 +0,0 @@
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)
}

View File

@@ -1,74 +0,0 @@
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)
}
}

View File

@@ -1,40 +0,0 @@
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], ".")
}

View File

@@ -1,33 +0,0 @@
package main
import (
"os"
"gopkg.in/yaml.v3"
)
type Release struct {
Name string `yaml:"name"`
Title string `yaml:"title,omitempty"`
Draft bool `yaml:"draft,omitempty"`
Pre bool `yaml:"prerelease,omitempty"`
}
type Plan struct {
Commit *Commit `yaml:"commit"`
OS *OSInfo `yaml:"os"`
Release *Release `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
}

View File

@@ -1,142 +0,0 @@
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",
},
&cli.BoolFlag{
Name: "test-build",
Usage: "Plan a test build, which is published to a draft " +
"\"test-builds\" release",
},
&cli.StringFlag{
Name: "test-build-name",
Usage: "Set a unique name to distinguish the ",
},
&cli.StringFlag{
Name: "test-release-type",
Value: "prerelease",
Usage: "Type of release when doing a test-build " +
"(prerelease or draft)",
},
},
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
}
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)
release := &Release{
Name: fmt.Sprintf(
"Emacs.%s.%s.%s",
commit.DateString(), commit.ShortSHA(), cleanRef,
),
}
archiveName := fmt.Sprintf(
"Emacs.%s.%s.%s.%s.%s",
commit.DateString(), commit.ShortSHA(), cleanRef, cleanOS, cleanArch,
)
if c.Bool("test-build") {
release.Title = "Test Builds"
release.Name = "test-builds"
if c.String("test-release-type") == "draft" {
release.Draft = true
} else {
release.Pre = true
}
archiveName += ".test"
if t := c.String("test-build-name"); t != "" {
archiveName += "." + sanitizeString(t)
}
}
plan := &Plan{
Commit: commit,
OS: osInfo,
Release: release,
Archive: filepath.Join(buildsDir, archiveName+".tbz"),
}
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, "-")
}

View File

@@ -1,196 +0,0 @@
package main
import (
"crypto/sha256"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"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"},
},
},
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")
assetBaseName := filepath.Base(plan.Archive)
assetSumFile := plan.Archive + ".sha256"
if _, err := os.Stat(assetSumFile); os.IsNotExist(err) {
fmt.Printf("==> Generating SHA256 sum for %s\n", assetBaseName)
assetSum, err := fileSHA256(plan.Archive)
if err != nil {
return err
}
content := fmt.Sprintf("%s %s", assetSum, assetBaseName)
err = os.WriteFile(assetSumFile, []byte(content), 0666)
if err != nil {
return err
}
fmt.Printf(" -> Done: %s\n", assetSum)
}
tagName := plan.Release.Name
name := plan.Release.Title
if name == "" {
name = tagName
}
fmt.Printf("==> Checking release %s\n", tagName)
release, resp, err := gh.Repositories.GetReleaseByTag(
c.Context, repo.Owner, repo.Name, plan.Release.Name,
)
if err != nil {
if resp.StatusCode == http.StatusNotFound {
fmt.Println(" -> Release not found, creating...")
prerelease := false
if !plan.Release.Draft && plan.Release.Pre {
prerelease = true
}
release, _, err = gh.Repositories.CreateRelease(
c.Context, repo.Owner, repo.Name, &github.RepositoryRelease{
Name: &name,
TagName: &tagName,
TargetCommitish: &releaseSHA,
Prerelease: &prerelease,
Draft: &plan.Release.Draft,
},
)
if err != nil {
return err
}
} else {
return err
}
}
if release.GetName() != name {
release.Name = &name
release, _, err = gh.Repositories.EditRelease(
c.Context, repo.Owner, repo.Name, release.GetID(), release,
)
if err != nil {
return err
}
}
if !plan.Release.Draft && release.GetPrerelease() != plan.Release.Pre {
release.Prerelease = &plan.Release.Pre
release, _, err = gh.Repositories.EditRelease(
c.Context, repo.Owner, repo.Name, release.GetID(), release,
)
if err != nil {
return err
}
}
assetFiles := []string{plan.Archive, assetSumFile}
for _, fileName := range assetFiles {
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
fmt.Printf("==> Checking asset %s\n", fileBaseName)
for _, a := range release.Assets {
if a.GetName() != fileBaseName {
continue
}
if a.GetSize() == int(fileInfo.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: fileBaseName},
fileIO,
)
if err != nil {
return err
}
fmt.Println(" -> Done")
}
}
fmt.Printf("==> Release available at: %s\n", release.GetHTMLURL())
return nil
}
func fileSHA256(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

View File

@@ -1,26 +0,0 @@
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
}