[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>
This commit is contained in:
Ryan Cragun 2025-05-23 14:02:24 -06:00 committed by GitHub
parent 689ede2da5
commit 025a6d5071
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 2384 additions and 9 deletions

View file

@ -11,6 +11,7 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/veqryn/slog-context v0.7.0
github.com/zclconf/go-cty v1.16.2
)
require (
@ -56,7 +57,6 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/zclconf/go-cty v1.16.2 // indirect
go.mongodb.org/mongo-driver v1.17.3 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect

View file

@ -31,14 +31,6 @@ func newGithubCmd() *cobra.Command {
Long: "Github commands",
}
github.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if parent := cmd.Parent(); parent != nil {
if parent.PersistentPreRunE != nil {
err := parent.PersistentPreRunE(parent, args)
if err != nil {
return err
}
}
}
if token, set := os.LookupEnv("GITHUB_TOKEN"); set {
githubCmdState.Github = githubCmdState.Github.WithAuthToken(token)
} else {
@ -46,6 +38,7 @@ func newGithubCmd() *cobra.Command {
}
return nil
}
github.AddCommand(newGithubCreateCmd())
github.AddCommand(newGithubListCmd())
return github

View file

@ -0,0 +1,19 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cmd
import (
"github.com/spf13/cobra"
)
func newGithubCreateCmd() *cobra.Command {
create := &cobra.Command{
Use: "create",
Short: "Github create commands",
Long: "Github create commands",
}
create.AddCommand(newGithubCreateBackportCmd())
return create
}

View file

@ -0,0 +1,116 @@
// 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()
}

View file

@ -64,6 +64,7 @@ func newRootCmd() *cobra.Command {
// Execute executes the root pipeline command.
func Execute() {
cobra.EnableTraverseRunHooks = true // Automatically chain run hooks
rootCmd := newRootCmd()
rootCmd.SilenceErrors = true // We handle this below

View file

@ -0,0 +1,135 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strings"
)
// AmOpts are the git am flags and arguments
// See: https://git-scm.com/docs/git-am
type AmOpts struct {
// Options
CommitterDateIsAuthorDate bool // --committer-date-is-author-date
Empty EmptyCommit // --empty=<mode>
Keep bool // --keep
KeepNonPatch bool // --keep-non-patch
MessageID bool // --message-id
NoMessageID bool // --no-message-id
NoReReReAutoupdate bool // --no-rerere-autoupdate
NoVerify bool // --no-verify
Quiet bool // --quiet
ReReReAutoupdate bool // --rerere-autoupdate
Signoff bool // --signoff
ThreeWayMerge bool // --3way
Whitespace ApplyWhitespaceAction // --whitespace=<action>
// Targets, depending on which combination of options you're setting
Mbox []string // <mbox|Maildir>
// Sequences
Abort bool // --abort
Continue bool // --continue
Quit bool // --quit
Resolved bool // --resolved
Retry bool // --retry
// Options that are allowed on sequences
AllowEmpty bool // --allow-empty
}
// Am runs the git am command
func (c *Client) Am(ctx context.Context, opts *AmOpts) (*ExecResponse, error) {
return c.Exec(ctx, "am", opts)
}
// String returns the options as a string
func (o *AmOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *AmOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
switch {
case o.Abort:
return append(opts, "--abort")
case o.Continue:
return append(opts, "--continue")
case o.Quit:
return append(opts, "--quit")
case o.Resolved:
if o.AllowEmpty {
opts = append(opts, "--allow-empty")
}
return append(opts, "--resolved")
case o.Retry:
return append(opts, "--retry")
}
if o.CommitterDateIsAuthorDate {
opts = append(opts, "--committer-date-is-author-date")
}
if o.Empty != "" {
opts = append(opts, fmt.Sprintf("--empty=%s", string(o.Empty)))
}
if o.Keep {
opts = append(opts, "--keep")
}
if o.KeepNonPatch {
opts = append(opts, "--keep-non-patch")
}
if o.MessageID {
opts = append(opts, "--message-id")
}
if o.NoMessageID {
opts = append(opts, "--no-message-id")
}
if o.NoReReReAutoupdate {
opts = append(opts, "--no-rerere-autoupdate")
}
if o.NoVerify {
opts = append(opts, "--no-verify")
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.ReReReAutoupdate {
opts = append(opts, "--rerere-autoupdate")
}
if o.Signoff {
opts = append(opts, "--signoff")
}
if o.ThreeWayMerge {
opts = append(opts, "--3way")
}
if o.Whitespace != "" {
opts = append(opts, fmt.Sprintf("--whitespace=%s", string(o.Whitespace)))
}
if len(o.Mbox) > 0 {
opts = append(opts, o.Mbox...)
}
return opts
}

View file

@ -22,6 +22,77 @@ func TestOptsStringers(t *testing.T) {
opts OptStringer
expected string
}{
"am": {
&AmOpts{
AllowEmpty: true, // Only supported for --resolved
CommitterDateIsAuthorDate: true,
Empty: EmptyCommitKeep,
Keep: true,
KeepNonPatch: true,
MessageID: true,
NoMessageID: true,
NoReReReAutoupdate: true,
NoVerify: true,
Quiet: true,
ReReReAutoupdate: true,
Signoff: true,
ThreeWayMerge: true,
Whitespace: ApplyWhitespaceActionFix,
Mbox: []string{"/path/to/my.patch"},
},
"--committer-date-is-author-date --empty=keep --keep --keep-non-patch --message-id --no-message-id --no-rerere-autoupdate --no-verify --quiet --rerere-autoupdate --signoff --3way --whitespace=fix /path/to/my.patch",
},
"am --continue": {
&AmOpts{
// Unallowed options are ignored
Empty: EmptyCommitKeep,
AllowEmpty: true,
// Sequence
Continue: true,
},
"--continue",
},
"am --abort": {
&AmOpts{
// Unallowed options are ignored
Empty: EmptyCommitKeep,
AllowEmpty: true,
// Sequence
Abort: true,
},
"--abort",
},
"am --quit": {
&AmOpts{
// Unallowed options are ignored
Empty: EmptyCommitKeep,
AllowEmpty: true,
// Sequence
Quit: true,
},
"--quit",
},
"am --allow-empty --resolved": {
&AmOpts{
// Unallowed options are ignored
Empty: EmptyCommitKeep,
// Allowed options are kept
AllowEmpty: true,
// Sequence
Resolved: true,
},
"--allow-empty --resolved",
},
"am --retry": {
&AmOpts{
// Unallowed options are ignored
Empty: EmptyCommitKeep,
AllowEmpty: true,
// Sequence
Retry: true,
},
"--retry",
},
"apply": {
&ApplyOpts{
AllowEmpty: true,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,741 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package github
import (
"context"
"errors"
"testing"
libgithub "github.com/google/go-github/v68/github"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/changed"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/releases"
"github.com/stretchr/testify/require"
)
// TestCreateBackportReq_Validate tests validation of the request
func TestCreateBackportReq_Validate(t *testing.T) {
t.Parallel()
for name, test := range map[string]struct {
req *CreateBackportReq
valid bool
}{
"empty": {nil, false},
"valid": {NewCreateBackportReq(WithCreateBrackportReqPullNumber(1234)), true},
"no owner": {
NewCreateBackportReq(
WithCreateBrackportReqPullNumber(1234),
WithCreateBackportReqOwner(""),
), false,
},
"no repo": {
NewCreateBackportReq(
WithCreateBrackportReqPullNumber(1234),
WithCreateBrackportReqRepo(""),
), false,
},
"no pull number": {NewCreateBackportReq(), false},
"no ce branch prefix": {
NewCreateBackportReq(
WithCreateBrackportReqPullNumber(1234),
WithCreateBrackportReqCEBranchPrefix(""),
), false,
},
"no base origin": {
NewCreateBackportReq(
WithCreateBrackportReqPullNumber(1234),
WithCreateBrackportReqBaseOrigin(""),
), false,
},
"uninitialized exclude groups": {
NewCreateBackportReq(
WithCreateBrackportReqPullNumber(1234),
WithCreateBrackportReqCEExclude(nil),
), false,
},
"uninitialized inactive groups": {
NewCreateBackportReq(
WithCreateBrackportReqPullNumber(1234),
WithCreateBrackportReqAllowInactiveGroups(nil),
), false,
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
if test.valid {
require.NoError(t, test.req.Validate(context.Background()))
} else {
require.Error(t, test.req.Validate(context.Background()))
}
})
}
}
// TestCreateBackportReq_backportNameForRef tests generating the backport
// branch name from branch name ref and the original PR branch name.
func TestCreateBackportReq_backportNameForRef(t *testing.T) {
t.Parallel()
for name, test := range map[string]struct {
ref string // These should be full branch names
prBranch string
expected string
}{
// backporting to ent main should never really happen but we'll test the
// logic anyway
"ent main": {
"main",
"my-pr",
"backport/main/my-pr",
},
"ent release branch": {
"release/1.19.x+ent",
"my-pr",
"backport/release/1.19.x+ent/my-pr",
},
"ce main": {
"ce/main",
"my-pr",
"backport/ce/main/my-pr",
},
"ce release branch": {
"ce/release/1.19.x",
"my-pr",
"backport/ce/release/1.19.x/my-pr",
},
"truncates super long branch name": {
"main",
"my-really-really-long-pr-name-that-must-exceed-two-hundred-and-fifty-characters-when-it-is-appended-to-the-backport-and-base-ref-prefixes-ought-to-be-truncated-so-as-to-not-exceed-the-github-pr-branch-requirements-otherwise-bad-things-happen",
"backport/main/my-really-really-long-pr-name-that-must-exceed-two-hundred-and-fifty-characters-when-it-is-appended-to-the-backport-and-base-ref-prefixes-ought-to-be-truncated-so-as-to-not-exceed-the-github-pr-branch-requirements-otherwise-bad-things-h",
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
req := NewCreateBackportReq()
require.Equal(t, test.expected, req.backportBranchNameForRef(test.ref, test.prBranch))
})
}
}
// TestCreateBackportReq_baseRefVersion tests generating the base ref version
// from the backport branch reference. The base ref version matches the schema
// used in .release/versions.hcl.
func TestCreateBackportReq_baseRefVersion(t *testing.T) {
t.Parallel()
for ref, test := range map[string]struct {
req *CreateBackportReq
expectedRef string
}{
// backporting to ent main should never really happen but we'll test the
// logic anyway
"main": {req: NewCreateBackportReq(), expectedRef: "main"},
"ce/main": {req: NewCreateBackportReq(), expectedRef: "main"},
"ent/main": {req: NewCreateBackportReq(WithCreateBrackportReqEntBranchPrefix("ent")), expectedRef: "main"},
"release/1.19.x+ent": {req: NewCreateBackportReq(), expectedRef: "release/1.19.x"},
"ce/release/1.19.x": {req: NewCreateBackportReq(), expectedRef: "release/1.19.x"},
"ent/release/1.19.x": {req: NewCreateBackportReq(WithCreateBrackportReqEntBranchPrefix("ent")), expectedRef: "release/1.19.x"},
} {
t.Run(ref, func(t *testing.T) {
t.Parallel()
require.Equal(t, test.expectedRef, test.req.baseRefVersion(ref))
})
}
}
// TestCreateBackportReq_determineBackportRefs tests generating a list
// of backport refs when considering the base ref of the PR and any labels
// that have been applied to it.
func TestCreateBackportReq_determineBackportRefs(t *testing.T) {
t.Parallel()
for name, test := range map[string]struct {
req *CreateBackportReq
baseRef string
labels Labels
expected []string
}{
"ent main no labels": {
NewCreateBackportReq(),
"main",
nil,
[]string{"ce/main"},
},
"ent main no labels with ent prefix": {
NewCreateBackportReq(WithCreateBrackportReqEntBranchPrefix("ent")),
"ent/main",
nil,
[]string{"ce/main"},
},
"ent main with labels": {
NewCreateBackportReq(),
"main",
Labels{
&libgithub.Label{Name: libgithub.Ptr("backport/1.19.x")},
&libgithub.Label{Name: libgithub.Ptr("backport/1.18.x")},
},
[]string{"ce/main", "release/1.19.x+ent", "release/1.18.x+ent"},
},
"ent main with labels with ent prefix": {
NewCreateBackportReq(WithCreateBrackportReqEntBranchPrefix("ent")),
"ent/main",
Labels{
&libgithub.Label{Name: libgithub.Ptr("backport/1.19.x")},
&libgithub.Label{Name: libgithub.Ptr("backport/1.18.x")},
},
[]string{"ce/main", "ent/release/1.19.x+ent", "ent/release/1.18.x+ent"},
},
"ent release no labels": {
NewCreateBackportReq(),
"release/1.19.x+ent",
nil,
[]string{"ce/release/1.19.x"},
},
"ent release no labels with ent prefix": {
NewCreateBackportReq(WithCreateBrackportReqEntBranchPrefix("ent")),
"ent/release/1.19.x+ent",
nil,
[]string{"ce/release/1.19.x"},
},
"ent release with labels": {
NewCreateBackportReq(),
"release/1.19.x+ent",
Labels{
&libgithub.Label{Name: libgithub.Ptr("backport/1.18.x")},
&libgithub.Label{Name: libgithub.Ptr("backport/1.17.x")},
&libgithub.Label{Name: libgithub.Ptr("backport/1.16.x")},
},
[]string{
"ce/release/1.19.x",
"release/1.18.x+ent",
"release/1.17.x+ent",
"release/1.16.x+ent",
},
},
"ent release with labels with ent prefix": {
NewCreateBackportReq(WithCreateBrackportReqEntBranchPrefix("ent")),
"ent/release/1.19.x+ent",
Labels{
&libgithub.Label{Name: libgithub.Ptr("backport/1.18.x")},
&libgithub.Label{Name: libgithub.Ptr("backport/1.17.x")},
&libgithub.Label{Name: libgithub.Ptr("backport/1.16.x")},
},
[]string{
"ce/release/1.19.x",
"ent/release/1.18.x+ent",
"ent/release/1.17.x+ent",
"ent/release/1.16.x+ent",
},
},
"ce main no labels": {
NewCreateBackportReq(),
"ce/main",
nil,
nil,
},
"ce main with labels": {
NewCreateBackportReq(),
"ce/main",
Labels{
&libgithub.Label{Name: libgithub.Ptr("backport/1.19.x")},
&libgithub.Label{Name: libgithub.Ptr("backport/1.18.x")},
},
[]string{"ce/release/1.19.x", "ce/release/1.18.x"},
},
"ce release no labels": {
NewCreateBackportReq(),
"ce/release/1.19.x",
nil,
nil,
},
"ce release with labels": {
NewCreateBackportReq(),
"ce/release/1.19.x",
Labels{
&libgithub.Label{Name: libgithub.Ptr("backport/1.18.x")},
&libgithub.Label{Name: libgithub.Ptr("backport/1.17.x")},
&libgithub.Label{Name: libgithub.Ptr("backport/1.16.x")},
},
[]string{"ce/release/1.18.x", "ce/release/1.17.x", "ce/release/1.16.x"},
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
require.EqualValues(t, test.expected, test.req.determineBackportRefs(context.Background(), test.baseRef, test.labels))
})
}
}
// TestCreateBackportReq_shouldSkipRef tests whether various combinations of
// base refs, backport refs, changed files, and active CE versions are
// backportable references or should be skipped.
func TestCreateBackportReq_shouldSkipRef(t *testing.T) {
t.Parallel()
defaultActiveVersions := map[string]*releases.Version{
// main is never going to be in here as it's assumed it's always active
"1.19.x": {CEActive: true, LTS: true},
"1.18.x": {CEActive: false},
"1.17.x": {CEActive: false},
"1.16.x": {CEActive: true, LTS: true},
}
defaultChangedFiles := &ListChangedFilesRes{
Files: changed.Files{
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr(".github/workflows/build.yml"),
},
Groups: changed.FileGroups{"pipeline"},
},
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("go.mod"),
},
Groups: changed.FileGroups{"app", "gotoolchain"},
},
},
Groups: changed.FileGroups{
"app", "gotoolchain", "pipeline",
},
}
for name, test := range map[string]struct {
baseRefVersion string
ref string
activeVersions map[string]*releases.Version
changedFiles *ListChangedFilesRes
skip bool
}{
"main to ce/main": {
baseRefVersion: "main",
ref: "ce/main",
activeVersions: defaultActiveVersions,
changedFiles: defaultChangedFiles,
skip: false,
},
"main to ce/main with ent and ce only files": {
baseRefVersion: "main",
ref: "ce/main",
activeVersions: defaultActiveVersions,
changedFiles: &ListChangedFilesRes{
Files: changed.Files{
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("e1c10eae02e13f5a090b9c29b0b1a3003e8ca7f6"),
Filename: libgithub.Ptr("go.mod"),
},
Groups: changed.FileGroups{"app", "gotoolchain"},
},
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("a6397662ea1d5fdde744ff3e4246377cf369197a"),
Filename: libgithub.Ptr("vault_ent/go.mod"),
},
Groups: changed.FileGroups{"app", "enterprise", "gotoolchain"},
},
},
Groups: changed.FileGroups{
"app", "enterprise", "gotoolchain",
},
},
skip: false,
},
"main to active release/1.19.x+ent": {
baseRefVersion: "1.19.x",
ref: "release/1.19.x+ent",
activeVersions: defaultActiveVersions,
changedFiles: defaultChangedFiles,
skip: false,
},
"main to release/1.18.x+ent (inactive CE)": {
baseRefVersion: "1.18.x",
ref: "release/1.18.x+ent",
activeVersions: defaultActiveVersions,
changedFiles: defaultChangedFiles,
skip: false,
},
"active release branch with app changes": {
baseRefVersion: "1.19.x",
ref: "ce/release/1.19.x",
activeVersions: defaultActiveVersions,
changedFiles: defaultChangedFiles,
skip: false,
},
"active release branch to CE with only ent changes": {
baseRefVersion: "1.19.x",
ref: "ce/release/1.19.x",
activeVersions: defaultActiveVersions,
changedFiles: &ListChangedFilesRes{
Files: changed.Files{
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr(".github/workflows/build-artifacts-ent.yml"),
},
Groups: changed.FileGroups{"enterprise", "pipeline"},
},
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("vault/vault_ent/go.mod"),
},
Groups: changed.FileGroups{"app", "enterprise", "gotoolchain"},
},
},
Groups: changed.FileGroups{
"app", "enterprise", "gotoolchain", "pipeline",
},
},
skip: true,
},
"inactive ce branch with no allowed group changes": {
baseRefVersion: "1.18.x",
ref: "ce/release/1.18.x",
activeVersions: defaultActiveVersions,
changedFiles: &ListChangedFilesRes{
Files: changed.Files{
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("vault/go.mod"),
},
Groups: changed.FileGroups{"app", "gotoolchain"},
},
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("vault/vault_ent/go.mod"),
},
Groups: changed.FileGroups{"app", "enterprise", "gotoolchain"},
},
},
Groups: changed.FileGroups{
"app", "enterprise", "gotoolchain",
},
},
skip: true,
},
"inactive ce with with pipeline changes": {
baseRefVersion: "1.18.x",
ref: "ce/release/1.18.x",
activeVersions: defaultActiveVersions,
changedFiles: &ListChangedFilesRes{
Files: changed.Files{
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr(".github/workflows/build.yml"),
},
Groups: changed.FileGroups{"pipeline"},
},
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("vault/go.mod"),
},
Groups: changed.FileGroups{"app", "gotoolchain"},
},
},
Groups: changed.FileGroups{
"app", "gotoolchain", "pipeline",
},
},
skip: false,
},
"inactive ce with with docs changes": {
baseRefVersion: "1.17.x",
ref: "ce/release/1.17.x",
activeVersions: defaultActiveVersions,
changedFiles: &ListChangedFilesRes{
Files: changed.Files{
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("website/content/docs/index.mdx"),
},
Groups: changed.FileGroups{"docs"},
},
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("vault/go.mod"),
},
Groups: changed.FileGroups{"app", "gotoolchain"},
},
},
Groups: changed.FileGroups{
"app", "gotoolchain", "pipeline",
},
},
skip: false,
},
"inactive ce with with changelog changes": {
baseRefVersion: "1.17.x",
ref: "ce/release/1.17.x",
activeVersions: defaultActiveVersions,
changedFiles: &ListChangedFilesRes{
Files: changed.Files{
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("changelog/1234.txt"),
},
Groups: changed.FileGroups{"changelog"},
},
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("vault/go.mod"),
},
Groups: changed.FileGroups{"app", "gotoolchain"},
},
},
Groups: changed.FileGroups{
"app", "gotoolchain", "pipeline",
},
},
skip: false,
},
"empty changed files list is skipped": {
baseRefVersion: "1.19.x",
ref: "ce/release/1.19.x",
activeVersions: defaultActiveVersions,
changedFiles: &ListChangedFilesRes{
Files: changed.Files{},
Groups: changed.FileGroups{},
},
skip: true,
},
"nil changed files list is skipped": {
baseRefVersion: "1.19.x",
ref: "ce/release/1.19.x",
activeVersions: defaultActiveVersions,
changedFiles: nil,
skip: true,
},
"release branch with no active versions": {
baseRefVersion: "1.19.x",
ref: "ce/release/1.19.x",
activeVersions: map[string]*releases.Version{},
changedFiles: &ListChangedFilesRes{
Files: changed.Files{
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("vault/go.mod"),
},
Groups: changed.FileGroups{"app", "gotoolchain"},
},
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("vault/vault_ent/go.mod"),
},
Groups: changed.FileGroups{"app", "enterprise", "gotoolchain"},
},
},
Groups: changed.FileGroups{
"app", "enterprise", "gotoolchain",
},
},
skip: true,
},
"release branch with nil active versions": {
baseRefVersion: "1.19.x",
ref: "ce/release/1.19.x",
activeVersions: nil,
changedFiles: &ListChangedFilesRes{
Files: changed.Files{
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("vault/go.mod"),
},
Groups: changed.FileGroups{"app", "gotoolchain"},
},
{
File: &libgithub.CommitFile{
SHA: libgithub.Ptr("84e0b544965861a7c6373e639cb13755512f84f4"),
Filename: libgithub.Ptr("vault/vault_ent/go.mod"),
},
Groups: changed.FileGroups{"app", "enterprise", "gotoolchain"},
},
},
},
skip: true,
},
"missing base ref version": {
baseRefVersion: "",
ref: "ce/main",
activeVersions: defaultActiveVersions,
changedFiles: defaultChangedFiles,
skip: true,
},
"missing ref version": {
baseRefVersion: "main",
ref: "",
activeVersions: defaultActiveVersions,
changedFiles: defaultChangedFiles,
skip: true,
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
req := NewCreateBackportReq()
msg, skip := req.shouldSkipRef(
context.Background(),
test.baseRefVersion,
test.ref,
test.activeVersions,
test.changedFiles,
)
require.Equalf(
t, test.skip, skip, "should have %t but got %t with %s", test.skip, skip, msg)
})
}
}
func TestCreateBackportReq_pullRequestBody(t *testing.T) {
t.Parallel()
for name, test := range map[string]struct {
expectContains []string
expectNotContains []string
origin *libgithub.PullRequest
attempt *CreateBackportAttempt
}{
"no error": {
expectContains: []string{"original body"},
expectNotContains: []string{"error body"},
origin: &libgithub.PullRequest{
Body: libgithub.Ptr("original body"),
Number: libgithub.Ptr(1234),
HTMLURL: libgithub.Ptr("https://github.com/hashicorp/vault-enterprise/pull/1234"),
MergedBy: &libgithub.User{Login: libgithub.Ptr("my-login")},
},
attempt: &CreateBackportAttempt{
TargetRef: "release/1.19.x",
},
},
"error": {
expectContains: []string{"original body", "error body"},
origin: &libgithub.PullRequest{
Body: libgithub.Ptr("original body"),
Number: libgithub.Ptr(1234),
HTMLURL: libgithub.Ptr("https://github.com/hashicorp/vault-enterprise/pull/1234"),
MergedBy: &libgithub.User{Login: libgithub.Ptr("my-login")},
},
attempt: &CreateBackportAttempt{
TargetRef: "release/1.19.x",
Error: errors.New("error body"),
},
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
req := NewCreateBackportReq()
got, err := req.pullRequestBody(test.origin, test.attempt)
require.NoError(t, err)
for _, c := range test.expectContains {
require.Containsf(t, got, c, got)
}
for _, nc := range test.expectNotContains {
require.NotContainsf(t, got, nc, got)
}
})
}
}
func TestCreateBackportRes_Err(t *testing.T) {
t.Parallel()
for name, test := range map[string]struct {
in *CreateBackportRes
failed error
}{
"nil": {
nil,
errors.New("uninitialized"),
},
"no errors": {
&CreateBackportRes{
Attempts: map[string]*CreateBackportAttempt{
"ce/main": {},
"release/1.18.x": {},
"release/1.19.x": {},
},
},
nil,
},
"top level error no attempt errors": {
&CreateBackportRes{
Error: errors.New("top-failed"),
Attempts: map[string]*CreateBackportAttempt{
"ce/main": {},
"release/1.18.x": {},
"release/1.19.x": {},
},
},
errors.New("top-failed"),
},
"no top level error attempt errors": {
&CreateBackportRes{
Attempts: map[string]*CreateBackportAttempt{
"ce/main": {
Error: errors.New("child-failed"),
},
"release/1.18.x": {},
"release/1.19.x": {},
},
},
errors.New("child-failed"),
},
"top level and attempt errors": {
&CreateBackportRes{
Error: errors.New("top-failed"),
Attempts: map[string]*CreateBackportAttempt{
"ce/main": {},
"release/1.18.x": {},
"release/1.19.x": {
Error: errors.New("child-failed"),
},
},
},
errors.New("top-failed\nchild-failed"),
},
"multiple attempt errors": {
&CreateBackportRes{
Error: errors.New("top-failed"),
Attempts: map[string]*CreateBackportAttempt{
"ce/main": {},
"release/1.18.x": {
Error: errors.New("child-2-failed"),
},
"release/1.19.x": {
Error: errors.New("child-3-failed"),
},
},
},
// When multiple attempts fail the errros should be stable
errors.New("top-failed\nchild-2-failed\nchild-3-failed"),
},
} {
t.Run(name, func(t *testing.T) {
if test.failed == nil {
require.Nil(t, test.in.Err())
} else {
require.Equal(t, test.failed.Error(), test.in.Err().Error())
}
})
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package github
import "embed"
//go:embed embeds/*
var embeds embed.FS

View file

@ -0,0 +1,34 @@
## Backport [#{{ .OriginPullRequest.Number }}]({{ .OriginPullRequest.HTMLURL }}) {{ if .Attempt.TargetRef }} into {{ .Attempt.TargetRef }} {{ end }}
{{ if .Attempt.Error }}
:rotating_light:
>**Warning** automatic backport of commits failed. If the first commit failed,
you will see a blank no-op commit below. If at least one commit succeeded, you
will see the backported commits up to, _but not including_, the commit where
the merge conflict occurred.
The person who merged in the original PR is: @{{ .OriginPullRequest.GetMergedBy.GetLogin }}
This person should resolve the merge-conflict(s) by either:
* Manually completing the backports into this branch
* Creating a new branch and manually backporting all commits
Error(s) encountered while attempting the backport:
> {{ .Attempt.Error }}
To continue the backport process, please follow the instructions below:
1. Checkout the branch in this PR locally (or optionally create a new branch off {{ .OriginPullRequest.GetBase.GetRef }})
1. Manually cherry-pick the missing commits from the original PR into this branch:
1. Cherry-pick the commits from the original PR into this branch `git cherry-pick <commit-hash>` (see Overview of unprocessed commits below for the list of commits to cherry-pick)
1. Resolve any conflicts that arise
1. Remove any CE only files if the backport target branch is to ce
1. Push the changes to this branch
1. Update the PR description to reflect the new commit(s)
{{ end }}
The following text was copied from the body of the original pull request
---
{{ .OriginPullRequest.Body }}