From ca4e7e2c441620ac6608f9f26bd1c2f679f16f8f Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Sun, 20 Jun 2021 20:45:03 +0100 Subject: [PATCH] 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. --- .github/workflows/build.yml | 174 +++++++++++++++++++++----- .github/workflows/test-build.yml | 186 +++++++++++++++++++++------- cmd/github-release/check_cmd.go | 57 --------- cmd/github-release/commit.go | 46 ------- cmd/github-release/github.go | 22 ---- cmd/github-release/main.go | 74 ----------- cmd/github-release/os_info.go | 40 ------ cmd/github-release/plan.go | 33 ----- cmd/github-release/plan_cmd.go | 142 ---------------------- cmd/github-release/publish_cmd.go | 196 ------------------------------ cmd/github-release/repo.go | 26 ---- 11 files changed, 285 insertions(+), 711 deletions(-) delete mode 100644 cmd/github-release/check_cmd.go delete mode 100644 cmd/github-release/commit.go delete mode 100644 cmd/github-release/github.go delete mode 100644 cmd/github-release/main.go delete mode 100644 cmd/github-release/os_info.go delete mode 100644 cmd/github-release/plan.go delete mode 100644 cmd/github-release/plan_cmd.go delete mode 100644 cmd/github-release/publish_cmd.go delete mode 100644 cmd/github-release/repo.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92a6dfe..ee449dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 81dda16..4a0a28a 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -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 }} diff --git a/cmd/github-release/check_cmd.go b/cmd/github-release/check_cmd.go deleted file mode 100644 index 8e50530..0000000 --- a/cmd/github-release/check_cmd.go +++ /dev/null @@ -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) -} diff --git a/cmd/github-release/commit.go b/cmd/github-release/commit.go deleted file mode 100644 index ae8b9cc..0000000 --- a/cmd/github-release/commit.go +++ /dev/null @@ -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") -} diff --git a/cmd/github-release/github.go b/cmd/github-release/github.go deleted file mode 100644 index c4136be..0000000 --- a/cmd/github-release/github.go +++ /dev/null @@ -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) -} diff --git a/cmd/github-release/main.go b/cmd/github-release/main.go deleted file mode 100644 index 14c0da0..0000000 --- a/cmd/github-release/main.go +++ /dev/null @@ -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) - } -} diff --git a/cmd/github-release/os_info.go b/cmd/github-release/os_info.go deleted file mode 100644 index e7f8d5c..0000000 --- a/cmd/github-release/os_info.go +++ /dev/null @@ -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], ".") -} diff --git a/cmd/github-release/plan.go b/cmd/github-release/plan.go deleted file mode 100644 index 56acb31..0000000 --- a/cmd/github-release/plan.go +++ /dev/null @@ -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 -} diff --git a/cmd/github-release/plan_cmd.go b/cmd/github-release/plan_cmd.go deleted file mode 100644 index fa36434..0000000 --- a/cmd/github-release/plan_cmd.go +++ /dev/null @@ -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 []", - 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, "-") -} diff --git a/cmd/github-release/publish_cmd.go b/cmd/github-release/publish_cmd.go deleted file mode 100644 index f25b74f..0000000 --- a/cmd/github-release/publish_cmd.go +++ /dev/null @@ -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 -} diff --git a/cmd/github-release/repo.go b/cmd/github-release/repo.go deleted file mode 100644 index f7c00da..0000000 --- a/cmd/github-release/repo.go +++ /dev/null @@ -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 -}