github.com/letsencrypt/boulder@v0.20251208.0/tools/release/branch/main.go (about)

     1  /*
     2  Branch Release creates a new Boulder hotfix release branch and pushes it to
     3  GitHub. It ensures that the release branch has a standard name, and starts at
     4  a previously-tagged mainline release.
     5  
     6  The expectation is that this branch will then be the target of one or more PRs
     7  copying (cherry-picking) commits from main to the release branch, and then a
     8  hotfix release will be tagged on the branch using the related Tag Release tool.
     9  
    10  Usage:
    11  
    12  	go run github.com/letsencrypt/boulder/tools/release/branch@main [-push] tagname
    13  
    14  The provided tagname must be a pre-existing release tag which is reachable from
    15  the "main" branch.
    16  
    17  If the -push flag is not provided, it will simply print the details of the new
    18  branch and then exit. If it is provided, it will initiate a push to the remote.
    19  
    20  In all cases, it assumes that the upstream remote is named "origin".
    21  */
    22  package main
    23  
    24  import (
    25  	"errors"
    26  	"flag"
    27  	"fmt"
    28  	"os"
    29  	"os/exec"
    30  	"strings"
    31  	"time"
    32  )
    33  
    34  type cmdError struct {
    35  	error
    36  	output string
    37  }
    38  
    39  func (e cmdError) Unwrap() error {
    40  	return e.error
    41  }
    42  
    43  func git(args ...string) (string, error) {
    44  	cmd := exec.Command("git", args...)
    45  	fmt.Println("Running:", cmd.String())
    46  	out, err := cmd.CombinedOutput()
    47  	if err != nil {
    48  		return string(out), cmdError{
    49  			error:  fmt.Errorf("running %q: %w", cmd.String(), err),
    50  			output: string(out),
    51  		}
    52  	}
    53  	return string(out), nil
    54  }
    55  
    56  func show(output string) {
    57  	for line := range strings.SplitSeq(strings.TrimSpace(output), "\n") {
    58  		fmt.Println("  ", line)
    59  	}
    60  }
    61  
    62  func main() {
    63  	err := branch(os.Args[1:])
    64  	if err != nil {
    65  		var cmdErr cmdError
    66  		if errors.As(err, &cmdErr) {
    67  			show(cmdErr.output)
    68  		}
    69  		fmt.Println(err.Error())
    70  		os.Exit(1)
    71  	}
    72  }
    73  
    74  func branch(args []string) error {
    75  	fs := flag.NewFlagSet("branch", flag.ContinueOnError)
    76  	var push bool
    77  	fs.BoolVar(&push, "push", false, "If set, push the resulting hotfix release branch to GitHub.")
    78  	err := fs.Parse(args)
    79  	if err != nil {
    80  		return fmt.Errorf("invalid flags: %w", err)
    81  	}
    82  
    83  	if len(fs.Args()) != 1 {
    84  		return fmt.Errorf("must supply exactly one argument, got %d: %#v", len(fs.Args()), fs.Args())
    85  	}
    86  
    87  	tag := fs.Arg(0)
    88  
    89  	// Confirm the reasonableness of the given tag name by inspecting each of its
    90  	// components.
    91  	parts := strings.SplitN(tag, ".", 3)
    92  	if len(parts) != 3 {
    93  		return fmt.Errorf("failed to parse release tag %q as semver", tag)
    94  	}
    95  
    96  	major := parts[0]
    97  	if major != "v0" {
    98  		return fmt.Errorf("expected major portion of release tag to be 'v0', got %q", major)
    99  	}
   100  
   101  	minor := parts[1]
   102  	t, err := time.Parse("20060102", minor)
   103  	if err != nil {
   104  		return fmt.Errorf("expected minor portion of release tag to be a date: %w", err)
   105  	}
   106  	if t.Year() < 2015 {
   107  		return fmt.Errorf("minor portion of release tag appears to be an unrealistic date: %q", t.String())
   108  	}
   109  
   110  	patch := parts[2]
   111  	if patch != "0" {
   112  		return fmt.Errorf("expected patch portion of release tag to be '0', got %q", patch)
   113  	}
   114  
   115  	// Fetch all of the latest refs from origin, so that we can get the most
   116  	// complete view of this tag and its relationship to main.
   117  	_, err = git("fetch", "origin")
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	_, err = git("merge-base", "--is-ancestor", tag, "origin/main")
   123  	if err != nil {
   124  		return fmt.Errorf("tag %q is not reachable from origin/main, may not have been created properly: %w", tag, err)
   125  	}
   126  
   127  	// Create the branch. We could skip this and instead push the tag directly
   128  	// to the desired ref name on the remote, but that wouldn't give the operator
   129  	// a chance to inspect it locally.
   130  	branch := fmt.Sprintf("release-branch-%s.%s", major, minor)
   131  	_, err = git("branch", branch, tag)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	// Show the HEAD of the new branch, not including its diff.
   137  	out, err := git("show", "-s", branch)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	show(out)
   142  
   143  	refspec := fmt.Sprintf("%s:%s", branch, branch)
   144  
   145  	if push {
   146  		_, err = git("push", "origin", refspec)
   147  		if err != nil {
   148  			return err
   149  		}
   150  	} else {
   151  		fmt.Println()
   152  		fmt.Println("Please inspect the branch above, then run:")
   153  		fmt.Printf("    git push origin %s\n", refspec)
   154  	}
   155  	return nil
   156  }