Merge pull request #10 from jimeh/sign-and-notarize

feat(signing)!: sign, notarize and staple Emacs.app and disk image
This commit is contained in:
2021-06-22 00:57:23 +01:00
committed by GitHub
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
}