mirror of
https://github.com/gopasspw/gopass.git
synced 2026-09-30 11:16:53 +02:00
* [chore] Clean up release helpers Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> * [chore] Add GOPASS_NOUPGRADE variable to the release helper To avoid runing `make upgrade` during a release. Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> --------- Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
611 lines
13 KiB
Go
611 lines
13 KiB
Go
// Copyright 2021 The gopass Authors. All rights reserved.
|
|
// Use of this source code is governed by the MIT license,
|
|
// that can be found in the LICENSE file.
|
|
|
|
// Postrel is a helper that's supposed to be run after a release has been completed.
|
|
// It will update the gopasspw.github.io website and create a new GitHub milestone.
|
|
// Since it depends on the artifacts generated by the autorelease GitHub action we
|
|
// can't run it as part of the release helper.
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/blang/semver/v4"
|
|
"github.com/google/go-github/v61/github"
|
|
"github.com/gopasspw/gopass/helpers/gitutils"
|
|
"github.com/gopasspw/gopass/pkg/fsutil"
|
|
"golang.org/x/mod/modfile"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
var verTmpl = `package main
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/blang/semver/v4"
|
|
)
|
|
|
|
func getVersion() semver.Version {
|
|
sv, err := semver.Parse(strings.TrimPrefix(version, "v"))
|
|
if err == nil {
|
|
return sv
|
|
}
|
|
|
|
return semver.Version{
|
|
Major: {{ .Major }},
|
|
Minor: {{ .Minor }},
|
|
Patch: {{ .Patch }},
|
|
Pre: []semver.PRVersion{
|
|
{VersionStr: "git"},
|
|
},
|
|
Build: []string{"HEAD"},
|
|
}
|
|
}
|
|
`
|
|
|
|
const logo = `
|
|
__ _ _ _ _ _ ___ ___
|
|
/'_ '\ /'_'\ ( '_'\ /'_' )/',__)/',__)
|
|
( (_) |( (_) )| (_) )( (_| |\__, \\__, \
|
|
'\__ |'\___/'| ,__/''\__,_)(____/(____/
|
|
( )_) | | |
|
|
\___/' (_)
|
|
`
|
|
|
|
func main() {
|
|
gitutils.Verbose = true
|
|
ctx := context.Background()
|
|
|
|
fmt.Print(logo)
|
|
fmt.Println()
|
|
fmt.Println("🌟 Performing post-release cleanup.")
|
|
|
|
curVer, err := versionFile()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
nextVer := curVer
|
|
nextVer.IncrementPatch()
|
|
|
|
htmlDir := "../gopasspw.github.io"
|
|
if h := os.Getenv("GOPASS_HTMLDIR"); h != "" {
|
|
htmlDir = h
|
|
}
|
|
|
|
mustCheckEnv()
|
|
|
|
ghCl, err := newGHClient(ctx)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Printf("✅ Current version is: %s\n", curVer.String())
|
|
fmt.Printf("✅ New version milestone will be: %s\n", nextVer.String())
|
|
fmt.Printf("✅ Expecting HTML in: %s\n", htmlDir)
|
|
fmt.Println()
|
|
fmt.Println("❓ Do you want to continue? (press any key to continue or Ctrl+C to abort)")
|
|
fmt.Scanln()
|
|
|
|
// create a new GitHub milestone
|
|
fmt.Println("☝ Creating new GitHub Milestone(s) ...")
|
|
if err := ghCl.createMilestones(ctx, nextVer); err != nil {
|
|
fmt.Printf("Failed to create GitHub milestones: %s\n", err)
|
|
}
|
|
|
|
// update gopass integrations
|
|
ui, err := newIntegrationsUpdater(ghCl.client, curVer)
|
|
if err != nil {
|
|
fmt.Printf("Failed to create integrations updater: %s\n", err)
|
|
} else {
|
|
ui.update(ctx)
|
|
}
|
|
|
|
fmt.Println("💎🙌 Done 🚀🚀🚀🚀🚀🚀")
|
|
}
|
|
|
|
func mustCheckEnv() {
|
|
want := []string{"GITHUB_TOKEN", "GITHUB_USER", "GITHUB_FORK"}
|
|
for _, e := range want {
|
|
if sv := os.Getenv(e); sv == "" {
|
|
panic("Please set: " + fmt.Sprintf("%v", want))
|
|
}
|
|
}
|
|
}
|
|
|
|
type ghClient struct {
|
|
client *github.Client
|
|
org string
|
|
repo string
|
|
}
|
|
|
|
func newGHClient(ctx context.Context) (*ghClient, error) {
|
|
pat := os.Getenv("GITHUB_TOKEN")
|
|
if pat == "" {
|
|
return nil, fmt.Errorf("❌ Please set GITHUB_TOKEN")
|
|
}
|
|
|
|
ts := oauth2.StaticTokenSource(
|
|
&oauth2.Token{AccessToken: pat},
|
|
)
|
|
tc := oauth2.NewClient(ctx, ts)
|
|
client := github.NewClient(tc)
|
|
|
|
return &ghClient{
|
|
client: client,
|
|
org: "gopasspw",
|
|
repo: "gopass",
|
|
}, nil
|
|
}
|
|
|
|
func (g *ghClient) createMilestones(ctx context.Context, v semver.Version) error {
|
|
ms, _, err := g.client.Issues.ListMilestones(ctx, g.org, g.repo, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// create a milestone for the next patch version
|
|
if err := g.createMilestone(ctx, v.String(), 1, ms); err != nil {
|
|
return err
|
|
}
|
|
|
|
// create a milestone for the next+1 patch version
|
|
v.IncrementPatch()
|
|
if err := g.createMilestone(ctx, v.String(), 2, ms); err != nil {
|
|
return err
|
|
}
|
|
|
|
// create a milestone for the next minor version
|
|
v.IncrementMinor()
|
|
v.Patch = 0
|
|
|
|
return g.createMilestone(ctx, v.String(), 90, ms)
|
|
}
|
|
|
|
func (g *ghClient) createMilestone(ctx context.Context, title string, offset int, ms []*github.Milestone) error {
|
|
for _, m := range ms {
|
|
if *m.Title == title {
|
|
fmt.Printf("❌ Milestone %s exists\n", title)
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
_, _, err := g.client.Issues.CreateMilestone(ctx, g.org, g.repo, &github.Milestone{
|
|
Title: &title,
|
|
DueOn: &github.Timestamp{Time: time.Now().Add(time.Duration(offset) * 30 * 24 * time.Hour)},
|
|
})
|
|
if err == nil {
|
|
fmt.Printf("✅ Milestone %s created\n", title)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func runCmd(dir string, args ...string) error {
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
cmd.Dir = dir
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
return cmd.Run()
|
|
}
|
|
|
|
func versionFile() (semver.Version, error) {
|
|
buf, err := os.ReadFile("VERSION")
|
|
if err != nil {
|
|
return semver.Version{}, err
|
|
}
|
|
|
|
return semver.Parse(strings.TrimSpace(string(buf)))
|
|
}
|
|
|
|
func goVersion(v string) string {
|
|
v = strings.TrimPrefix(v, "go")
|
|
sv, err := semver.ParseTolerant(v)
|
|
if err != nil {
|
|
// if we can't parse the version, we assume it's a dev version
|
|
// and return the current major.minor version
|
|
if len(v) > 3 {
|
|
sv, err = semver.ParseTolerant(v[:3])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf("%d.%d", sv.Major, sv.Minor)
|
|
}
|
|
|
|
type inUpdater struct {
|
|
github *github.Client
|
|
v semver.Version
|
|
goVer string // go version as major.minor (for use in go.mod and GH workflows)
|
|
}
|
|
|
|
func newIntegrationsUpdater(client *github.Client, v semver.Version) (*inUpdater, error) {
|
|
buf, err := os.ReadFile("go.mod")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
modfile, err := modfile.Parse("go.mod", buf, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &inUpdater{
|
|
github: client,
|
|
v: v,
|
|
goVer: goVersion(modfile.Go.Version),
|
|
}, nil
|
|
}
|
|
|
|
func (u *inUpdater) update(ctx context.Context) {
|
|
for _, upd := range []string{
|
|
"git-credential-gopass",
|
|
"gopass-hibp",
|
|
"gopass-jsonapi",
|
|
"gopass-summon-provider",
|
|
} {
|
|
fmt.Println()
|
|
fmt.Println("------------------------------")
|
|
fmt.Println()
|
|
fmt.Printf("🌟 Updating: %s ...\n", upd)
|
|
fmt.Println()
|
|
if err := u.doUpdate(ctx, upd); err != nil {
|
|
fmt.Printf("❌ Updating %s failed: %s\n", upd, err)
|
|
|
|
continue
|
|
}
|
|
fmt.Printf("✅ Integration %s is up to date.\n", upd)
|
|
}
|
|
}
|
|
|
|
func (u *inUpdater) doUpdate(ctx context.Context, dir string) error {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := filepath.Join(filepath.Dir(cwd), dir)
|
|
|
|
tag := fmt.Sprintf("v%s", u.v.String())
|
|
// check if the release is already tagged
|
|
if gitutils.GitHasTag(path, tag) {
|
|
fmt.Printf("✅ Integration %s has tag %s already.\n", dir, tag)
|
|
|
|
return nil
|
|
}
|
|
fmt.Printf("✅ [%s] %s is not tagged, yet.\n", dir, tag)
|
|
|
|
// make sure we're at head
|
|
if !gitutils.IsGitClean(path) {
|
|
return fmt.Errorf("git not clean at %s", path)
|
|
}
|
|
fmt.Printf("✅ [%s] Git is clean.", dir)
|
|
|
|
// git pull origin master
|
|
if err := gitutils.GitPom(path); err != nil {
|
|
return fmt.Errorf("failed to fetch changes at %s: %s", path, err)
|
|
}
|
|
|
|
// make upgrade
|
|
if err := runCmd(path, "make", "upgrade"); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] make upgrade.\n", dir)
|
|
|
|
// go get github.com/gopasspw/gopass@tag
|
|
if err := runCmd(path, "go", "get", "github.com/gopasspw/gopass@"+tag); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] updated gopass dependency.\n", dir)
|
|
|
|
// sync .golangci.yml ?
|
|
if err := fsutil.CopyFile(filepath.Join(cwd, ".golangci.yml"), filepath.Join(path, ".golangci.yml")); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] synced .golangci.yml.\n", dir)
|
|
|
|
// update go.mod
|
|
if err := runCmd(path, "go", "mod", "edit", "-go="+u.goVer); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] updated Go version in go.mod to %s.\n", dir, u.goVer)
|
|
|
|
// go mod tidy
|
|
if err := runCmd(path, "go", "mod", "tidy"); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] go mod tidy.\n", dir)
|
|
|
|
// update workflows
|
|
if err := u.updateWorkflows(ctx, path); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] updated workflows.\n", dir)
|
|
|
|
// update VERSION
|
|
if err := os.WriteFile(filepath.Join(path, "VERSION"), []byte(u.v.String()+"\n"), 0o644); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] wrote VERSION.\n", dir)
|
|
|
|
// update version.go
|
|
if err := u.writeVersionGo(path); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] wrote version.go.\n", dir)
|
|
|
|
// update CHANGELOG.md
|
|
if err := u.updateChangelog(ctx, path); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] wrote CHANGELOG.md.\n", dir)
|
|
|
|
// git commit
|
|
if err := gitutils.GitCommitAndPush(path, tag); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] committed.\n", dir)
|
|
|
|
// git tag v
|
|
if err := gitutils.GitTagAndPush(path, tag); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("✅ [%s] tagged.\n", dir)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *inUpdater) updateWorkflows(ctx context.Context, dir string) error {
|
|
filepath.Walk(filepath.Join(dir, ".github", "workflows"), func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
fmt.Printf("Failed to walk %s: %s\n", path, err)
|
|
|
|
return nil
|
|
}
|
|
if info.IsDir() {
|
|
// fmt.Printf("Skipping dir %s\n", path)
|
|
|
|
return nil
|
|
}
|
|
if !strings.HasSuffix(path, ".yml") {
|
|
// fmt.Printf("Skipping file %s\n", path)
|
|
|
|
return nil
|
|
}
|
|
|
|
return u.updateWorkflow(ctx, path)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
var goVersionRE = regexp.MustCompile(`go-version:\s+\d+\.\d+`)
|
|
|
|
func (u *inUpdater) updateWorkflow(_ context.Context, path string) error {
|
|
buf, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
str := goVersionRE.ReplaceAllString(string(buf), "go-version: "+u.goVer)
|
|
// no change, no write
|
|
if str == string(buf) {
|
|
// fmt.Printf("No changes in %s\n", path)
|
|
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("Wrote %s\n", path)
|
|
|
|
return os.WriteFile(path, []byte(str), 0o644)
|
|
}
|
|
|
|
type tplPayload struct {
|
|
Major uint64
|
|
Minor uint64
|
|
Patch uint64
|
|
}
|
|
|
|
func (u *inUpdater) writeVersionGo(path string) error {
|
|
tmpl, err := template.New("version").Parse(verTmpl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fn := filepath.Join(path, "version.go")
|
|
fh, err := os.Create(fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fh.Close()
|
|
|
|
return tmpl.Execute(fh, tplPayload{
|
|
Major: u.v.Major,
|
|
Minor: u.v.Minor,
|
|
Patch: u.v.Patch,
|
|
})
|
|
}
|
|
|
|
func (u *inUpdater) updateChangelog(_ context.Context, dir string) error {
|
|
fn := filepath.Join(dir, "CHANGELOG.md")
|
|
|
|
buf, err := os.ReadFile(fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var sb strings.Builder
|
|
fmt.Fprintf(&sb, "## %s\n", u.v.String())
|
|
fmt.Fprintln(&sb)
|
|
fmt.Fprintf(&sb, "- Bump dependencies to gopass release v%s\n", u.v.String())
|
|
fmt.Fprintln(&sb)
|
|
|
|
_, err = sb.Write(buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.WriteFile(fn, []byte(sb.String()), 0o644); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checksum(url string) (string, string, error) {
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
s2 := sha256.New()
|
|
s5 := sha512.New()
|
|
w := io.MultiWriter(s2, s5)
|
|
|
|
_, err = io.Copy(w, resp.Body)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return fmt.Sprintf("%x", s2.Sum(nil)), fmt.Sprintf("%x", s5.Sum(nil)), nil
|
|
}
|
|
|
|
func updateBuild(path string, m map[string]*string) error {
|
|
fin, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fin.Close()
|
|
|
|
npath := path + ".new"
|
|
fout, err := os.Create(npath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fout.Close()
|
|
|
|
s := bufio.NewScanner(fin)
|
|
SCAN:
|
|
for s.Scan() {
|
|
line := s.Text()
|
|
for match, repl := range m {
|
|
if strings.HasPrefix(line, match) {
|
|
if repl != nil {
|
|
fmt.Fprintln(fout, *repl)
|
|
}
|
|
|
|
continue SCAN
|
|
}
|
|
}
|
|
fmt.Fprintln(fout, line)
|
|
}
|
|
|
|
return os.Rename(npath, path)
|
|
}
|
|
|
|
type repo struct {
|
|
ver semver.Version // gopass version
|
|
url string // gopass download url
|
|
dir string // repo dir
|
|
msg string
|
|
rem string // remote
|
|
}
|
|
|
|
func (r *repo) branch() string {
|
|
return fmt.Sprintf("gopass-%s", r.ver.String())
|
|
}
|
|
|
|
func (r *repo) commitMsg() string {
|
|
if r.msg != "" {
|
|
return r.msg
|
|
}
|
|
|
|
return "gopass: update to " + r.ver.String() + "\nNote: This is an auto-generated change as part of the gopass release process.\n"
|
|
}
|
|
|
|
func (r *repo) updatePrepare() error {
|
|
fmt.Println("🌟 Running prepare ...")
|
|
|
|
// git co master
|
|
if err := r.gitCoMaster(); err != nil {
|
|
return fmt.Errorf("git checkout master failed: %w", err)
|
|
}
|
|
if !r.isGitClean() {
|
|
return fmt.Errorf("git is dirty")
|
|
}
|
|
// git pull origin master
|
|
if err := r.gitPom(); err != nil {
|
|
return fmt.Errorf("git pull origin master failed: %w", err)
|
|
}
|
|
// git co -b gopass-VER
|
|
if err := r.gitBranch(); err == nil {
|
|
return nil
|
|
}
|
|
|
|
// git branch -d gopass-VER
|
|
if err := r.gitBranchDel(); err != nil {
|
|
return fmt.Errorf("git branch -d failed: %w", err)
|
|
}
|
|
|
|
return r.gitBranch()
|
|
}
|
|
|
|
func (r *repo) updateFinalize(path string) error {
|
|
fmt.Println("🌟 Running finalize ...")
|
|
|
|
// git commit -m 'gopass: update to VER'
|
|
if err := r.gitCommit(path); err != nil {
|
|
return fmt.Errorf("git commit %s failed: %w", path, err)
|
|
}
|
|
// git push myfork gopass-VER
|
|
return r.gitPush(r.rem)
|
|
}
|
|
|
|
func (r *repo) gitCoMaster() error {
|
|
return gitutils.GitCoMaster(r.dir)
|
|
}
|
|
|
|
func (r *repo) gitBranch() error {
|
|
return gitutils.GitCoBranch(r.dir, r.branch())
|
|
}
|
|
|
|
func (r *repo) gitBranchDel() error {
|
|
return gitutils.GitDelBranch(r.dir, r.branch())
|
|
}
|
|
|
|
func (r *repo) gitPom() error {
|
|
return gitutils.GitPom(r.dir)
|
|
}
|
|
|
|
func (r *repo) gitPush(remote string) error {
|
|
return gitutils.GitPush(remote, r.branch())
|
|
}
|
|
|
|
func (r *repo) gitCommit(files ...string) error {
|
|
return gitutils.GitCommit(r.dir, r.commitMsg(), files...)
|
|
}
|
|
|
|
func (r *repo) isGitClean() bool {
|
|
return gitutils.IsGitClean(r.dir)
|
|
}
|
|
|
|
func strp(s string) *string {
|
|
return &s
|
|
}
|