156 lines
4 KiB
Go
156 lines
4 KiB
Go
/*
|
|
Branch Release creates a new Boulder hotfix release branch and pushes it to
|
|
GitHub. It ensures that the release branch has a standard name, and starts at
|
|
a previously-tagged mainline release.
|
|
|
|
The expectation is that this branch will then be the target of one or more PRs
|
|
copying (cherry-picking) commits from main to the release branch, and then a
|
|
hotfix release will be tagged on the branch using the related Tag Release tool.
|
|
|
|
Usage:
|
|
|
|
go run github.com/letsencrypt/boulder/tools/release/tag@main [-push] tagname
|
|
|
|
The provided tagname must be a pre-existing release tag which is reachable from
|
|
the "main" branch.
|
|
|
|
If the -push flag is not provided, it will simply print the details of the new
|
|
branch and then exit. If it is provided, it will initiate a push to the remote.
|
|
|
|
In all cases, it assumes that the upstream remote is named "origin".
|
|
*/
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type cmdError struct {
|
|
error
|
|
output string
|
|
}
|
|
|
|
func (e cmdError) Unwrap() error {
|
|
return e.error
|
|
}
|
|
|
|
func git(args ...string) (string, error) {
|
|
cmd := exec.Command("git", args...)
|
|
fmt.Println("Running:", cmd.String())
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return string(out), cmdError{
|
|
error: fmt.Errorf("running %q: %w", cmd.String(), err),
|
|
output: string(out),
|
|
}
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
func show(output string) {
|
|
for line := range strings.SplitSeq(strings.TrimSpace(output), "\n") {
|
|
fmt.Println(" ", line)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
err := branch(os.Args[1:])
|
|
if err != nil {
|
|
var cmdErr cmdError
|
|
if errors.As(err, &cmdErr) {
|
|
show(cmdErr.output)
|
|
}
|
|
fmt.Println(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func branch(args []string) error {
|
|
fs := flag.NewFlagSet("branch", flag.ContinueOnError)
|
|
var push bool
|
|
fs.BoolVar(&push, "push", false, "If set, push the resulting hotfix release branch to GitHub.")
|
|
err := fs.Parse(args)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid flags: %w", err)
|
|
}
|
|
|
|
if len(fs.Args()) != 1 {
|
|
return fmt.Errorf("must supply exactly one argument, got %d: %#v", len(fs.Args()), fs.Args())
|
|
}
|
|
|
|
tag := fs.Arg(0)
|
|
|
|
// Confirm the reasonableness of the given tag name by inspecting each of its
|
|
// components.
|
|
parts := strings.SplitN(tag, ".", 3)
|
|
if len(parts) != 3 {
|
|
return fmt.Errorf("failed to parse patch version from release tag %q", tag)
|
|
}
|
|
|
|
major := parts[0]
|
|
if major != "v0" {
|
|
return fmt.Errorf("expected major portion of release tag to be 'v0', got %q", major)
|
|
}
|
|
|
|
minor := parts[1]
|
|
t, err := time.Parse("20060102", minor)
|
|
if err != nil {
|
|
return fmt.Errorf("expected minor portion of release tag to be a ")
|
|
}
|
|
if t.Year() < 2015 {
|
|
return fmt.Errorf("minor portion of release tag appears to be an unrealistic date: %q", t.String())
|
|
}
|
|
|
|
patch := parts[2]
|
|
if patch != "0" {
|
|
return fmt.Errorf("expected patch portion of release tag to be '0', got %q", patch)
|
|
}
|
|
|
|
// Fetch all of the latest refs from origin, so that we can get the most
|
|
// complete view of this tag and its relationship to main.
|
|
_, err = git("fetch", "origin")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = git("merge-base", "--is-ancestor", tag, "origin/main")
|
|
if err != nil {
|
|
return fmt.Errorf("tag %q is not reachable from origin/main, may not have been created properly: %w", tag, err)
|
|
}
|
|
|
|
// Create the branch. We could skip this and instead push the tag directly
|
|
// to the desired ref name on the remote, but that wouldn't give the operator
|
|
// a chance to inspect it locally.
|
|
branch := fmt.Sprintf("release-branch-%s.%s", major, minor)
|
|
_, err = git("branch", branch, tag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Show the HEAD of the new branch, not including its diff.
|
|
out, err := git("show", "-s", branch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
show(out)
|
|
|
|
refspec := fmt.Sprintf("%s:%s", branch, branch)
|
|
|
|
if push {
|
|
_, err = git("push", "origin", refspec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
fmt.Println()
|
|
fmt.Println("Please inspect the branch above, then run:")
|
|
fmt.Printf(" git push origin %s\n", refspec)
|
|
}
|
|
return nil
|
|
}
|