vault/tools/pipeline/internal/cmd/github_create_backport.go
Ryan Cragun 025a6d5071
[VAULT-34829] pipeline(backport): add github create backport command (#30713)
Add a new `github create backport` sub-command that can create a
backport of a given pull request. The command has been designed around a
Github Actions workflow where it is triggered on a closed pull request
event with a guard that checks for merges:

```yaml
pull_request_target:
  types: closed

jobs:
  backport:
    if: github.even.pull_request.merged
    runs-on: "..."
```

Eventually this sub-command (or another similar one) can be used to
implemente backporting a CE pull request to the corresponding ce/*
branch in vault-enterprise. This functionality will be implemented in
VAULT-34827.

This backport runner has several new behaviors not present in the
existing backport assistant:
  - If the source PR was made against an enterprise branch we'll assume
    that we want create a CE backport.
  - Enterprise only files will be automatically _removed_ from the CE
    backport for you. This will not guarantee a working CE pull request
    but does quite a bit of the heavy lifting for you.
  - If the change only contains enterprise files we'll skip creating a
    CE backport.
  - If the corresponding CE branch is inactive (as defined in
    .release/versions.hcl) then we will skip creating a backport in most
    cases. The exceptions are changes that include docs, README, or
    pipeline changes as we assume that even active branches will want
    those changes.
  - Backport labels still work but _only_ to enterprise PR's. It is
    assumed that when the subsequent PRs are merged that their
    corresponding CE backports will be created.
  - Backport labels no longer include editions. They will now use the
    same schema as active versions defined .release/verions.hcl. E.g.
    `backport/1.19.x`. `main` is always assumed to be active.
  - The runner will always try and update the source PR with a Github
    comment regarding the status of each individual backport. Even if
    one attempt at backporting fails we'll continue until we've
    attempted all backports.

Signed-off-by: Ryan Cragun <me@ryan.ec>
2025-05-23 14:02:24 -06:00

117 lines
4.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cmd
import (
"context"
"errors"
"fmt"
"math"
"strconv"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/changed"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/github"
"github.com/spf13/cobra"
)
var createGithubBackportState struct {
req github.CreateBackportReq
ceExclude []string
ceAllowInactive []string
}
func newGithubCreateBackportCmd() *cobra.Command {
listRuns := &cobra.Command{
Use: "backport 1234",
Short: "Create a backport pull request from another pull request",
Long: "Create a backport pull request from another pull request",
RunE: runCreateGithubBackportCmd,
Args: func(cmd *cobra.Command, args []string) error {
switch len(args) {
case 1:
pr, err := strconv.ParseUint(args[0], 10, 0)
if err != nil {
return fmt.Errorf("invalid pull number: %s: %w", args[0], err)
}
if pr <= math.MaxUint32 {
createGithubBackportState.req.PullNumber = uint(pr)
} else {
return fmt.Errorf("invalid pull number: %s: number is too large", args[0])
}
return nil
case 0:
return errors.New("no pull request number has been provided")
default:
return fmt.Errorf("invalid arguments: only pull request number is expected, received %d arguments: %v", len(args), args)
}
},
}
listRuns.PersistentFlags().StringSliceVarP(&createGithubBackportState.ceAllowInactive, "ce-allow-inactive-groups", "a", []string{"docs", "changelog", "pipeline"}, "Change file groups that should be allowed to backport to inactive CE branches")
listRuns.PersistentFlags().StringVar(&createGithubBackportState.req.CEBranchPrefix, "ce-branch-prefix", "ce", "The branch name prefix")
listRuns.PersistentFlags().StringSliceVarP(&createGithubBackportState.ceExclude, "ce-exclude-groups", "e", []string{"enterprise"}, "Change file groups that should be excluded from the backporting to CE branches")
listRuns.PersistentFlags().StringVar(&createGithubBackportState.req.BaseOrigin, "base-origin", "origin", "The name to use for the base remote origin")
listRuns.PersistentFlags().StringVarP(&createGithubBackportState.req.Owner, "owner", "o", "hashicorp", "The Github organization")
listRuns.PersistentFlags().StringVarP(&createGithubBackportState.req.Repo, "repo", "r", "vault-enterprise", "The Github repository. Private repositories require auth via a GITHUB_TOKEN env var")
listRuns.PersistentFlags().StringVarP(&createGithubBackportState.req.RepoDir, "repo-dir", "d", "", "The path to the vault repository dir. If not set a temporary directory will be used")
listRuns.PersistentFlags().StringVarP(&createGithubBackportState.req.ReleaseVersionConfigPath, "releases-version-path", "m", "", "The path to .release/versions.hcl")
listRuns.PersistentFlags().UintVar(&createGithubBackportState.req.ReleaseRecurseDepth, "recurse", 3, "If no path to a config file is given, recursively search backwards for it and stop at root or until we've his the configured depth.")
// NOTE: The following are technically flags but they only for testing testing
// the command before we cut over to new utility.
listRuns.PersistentFlags().StringVar(&createGithubBackportState.req.EntBranchPrefix, "ent-branch-prefix", "", "The ent branch name prefix. Only used for testing before migration to the new workflow")
listRuns.PersistentFlags().StringVar(&createGithubBackportState.req.BackportLabelPrefix, "backport-label-prefix", "backport", "The name to use for the base remote origin")
err := listRuns.PersistentFlags().MarkHidden("ent-branch-prefix")
if err != nil {
panic(err)
}
err = listRuns.PersistentFlags().MarkHidden("backport-label-prefix")
if err != nil {
panic(err)
}
return listRuns
}
func runCreateGithubBackportCmd(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true // Don't spam the usage on failure
for i, ig := range createGithubBackportState.ceAllowInactive {
if i == 0 && createGithubBackportState.req.CEAllowInactiveGroups == nil {
createGithubBackportState.req.CEAllowInactiveGroups = changed.FileGroups{}
}
createGithubBackportState.req.CEAllowInactiveGroups = createGithubBackportState.req.CEAllowInactiveGroups.Add(changed.FileGroup(ig))
}
for i, eg := range createGithubBackportState.ceExclude {
if i == 0 && createGithubBackportState.req.CEExclude == nil {
createGithubBackportState.req.CEExclude = changed.FileGroups{}
}
createGithubBackportState.req.CEExclude = createGithubBackportState.req.CEExclude.Add(changed.FileGroup(eg))
}
res := createGithubBackportState.req.Run(context.TODO(), githubCmdState.Github, githubCmdState.Git)
if res == nil {
res = &github.CreateBackportRes{}
}
if err := res.Err(); err != nil {
res.ErrorMessage = err.Error()
}
switch rootCfg.format {
case "json":
b, err := res.ToJSON()
if err != nil {
return errors.Join(res.Err(), err)
}
fmt.Println(string(b))
default:
fmt.Println(res.ToTable().Render())
}
return res.Err()
}