mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
[VAULT-39424] pipeline(close-origin-pr): add support for closing the origin of copied PRs (#9907) (#10029)
* [VAULT-39424] pipeline(close-origin-pr): add support for closing the origin of copied PRs
When we copy a community contributed Pull Request to Enterprise the
source PR is effectively orphaned, leaving the original PR still
opened, the author unsure of what state the copied PR is in, and any
issues associated with it open.
When the copied PR is closed we ought to close the origin PR if it's
still open, and any other issues that might be associated with either
the origin PR or the copied PR.
We can also add comments to both PRs that include links to each other
and the squash commit to make discovery of the work visible to those
with access to both repos. Unfortunately there is no way to know what
the SHA will be when it's synced so we have to rely on the
'Co-Authored-By:' trailers in commit message.
There are some challenges to this:
- The automation should only execute when copied PRs are closed
- How to determine the origin PR from only the copied PR
- How to determine the PR's linked issues (which the v3 REST API does not expose)
We solved them by:
- Requiring the PR HEAD ref to start with `copy/`
- Encoding the origin PR information in the PR HEAD ref.
e.g. `copy/hashicorp/vault/31580/ryan/VAULT-39424-test-ce`
- Using the V4 GraphQL API to determine "closed issue references"
The result is a new `pipeline` CLI command that can close the origin PR,
all of the issues, and write status comments on each PR with links to
everything to establish omnidirectional linking in the Github UI.
```bash
pipeline github close origin-pull-request 9903
```
* fix feedback
---------
Signed-off-by: Ryan Cragun <me@ryan.ec>
Co-authored-by: Ryan Cragun <me@ryan.ec>
This commit is contained in:
parent
fe0cc5bf32
commit
e781da5a29
21 changed files with 823 additions and 58 deletions
|
|
@ -1,6 +1,6 @@
|
|||
module github.com/hashicorp/vault/tools/pipeline
|
||||
|
||||
go 1.23.2
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
|
|
@ -9,10 +9,12 @@ require (
|
|||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/hashicorp/releases-api v0.2.3
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.8
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/veqryn/slog-context v0.8.0
|
||||
github.com/zclconf/go-cty v1.16.4
|
||||
golang.org/x/oauth2 v0.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -57,6 +59,7 @@ require (
|
|||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
|
|
|
|||
|
|
@ -171,6 +171,10 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
|
|||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
|
|
@ -209,6 +213,8 @@ golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
|||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
|
|||
|
|
@ -4,23 +4,28 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/go-github/v74/github"
|
||||
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/git"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type githubCommandState struct {
|
||||
Github *github.Client
|
||||
Git *git.Client
|
||||
Git *git.Client
|
||||
GithubV3 *github.Client
|
||||
GithubV4 *githubv4.Client
|
||||
}
|
||||
|
||||
var githubCmdState = &githubCommandState{
|
||||
Github: github.NewClient(nil),
|
||||
Git: git.NewClient(git.WithLoadTokenFromEnv()),
|
||||
GithubV3: github.NewClient(nil),
|
||||
GithubV4: githubv4.NewClient(nil),
|
||||
Git: git.NewClient(git.WithLoadTokenFromEnv()),
|
||||
}
|
||||
|
||||
func newGithubCmd() *cobra.Command {
|
||||
|
|
@ -28,16 +33,24 @@ func newGithubCmd() *cobra.Command {
|
|||
Use: "github",
|
||||
Short: "Github commands",
|
||||
Long: "Github commands",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if token, set := os.LookupEnv("GITHUB_TOKEN"); set {
|
||||
githubCmdState.GithubV3 = githubCmdState.GithubV3.WithAuthToken(token)
|
||||
githubCmdState.GithubV4 = githubv4.NewClient(
|
||||
oauth2.NewClient(context.Background(),
|
||||
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
fmt.Println("\x1b[1;33;49mWARNING\x1b[0m: GITHUB_TOKEN has not been set. While not always required for read actions on public repositories you're likely to get throttled without it")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
githubCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
if token, set := os.LookupEnv("GITHUB_TOKEN"); set {
|
||||
githubCmdState.Github = githubCmdState.Github.WithAuthToken(token)
|
||||
} else {
|
||||
fmt.Println("\x1b[1;33;49mWARNING\x1b[0m: GITHUB_TOKEN has not been set. While not always required for read actions on public repositories you're likely to get throttled without it")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
githubCmd.AddCommand(newGithubCheckCmd())
|
||||
githubCmd.AddCommand(newGithubCloseCmd())
|
||||
githubCmd.AddCommand(newGithubCopyCmd())
|
||||
githubCmd.AddCommand(newGithubCreateCmd())
|
||||
githubCmd.AddCommand(newGithubFindCmd())
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ func newCheckGithubCommitStatusCmd() *cobra.Command {
|
|||
func runCheckGithubCommitStatusCmd(cmd *cobra.Command, args []string) error {
|
||||
cmd.SilenceUsage = true // Don't spam the usage on failure
|
||||
|
||||
res, err := checkGithubCommitStatusReq.Run(context.TODO(), githubCmdState.Github)
|
||||
res, err := checkGithubCommitStatusReq.Run(context.TODO(), githubCmdState.GithubV3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
19
tools/pipeline/internal/cmd/github_close.go
Normal file
19
tools/pipeline/internal/cmd/github_close.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newGithubCloseCmd() *cobra.Command {
|
||||
closeCmd := &cobra.Command{
|
||||
Use: "close",
|
||||
Short: "Github close commands",
|
||||
Long: "Github close commands",
|
||||
}
|
||||
closeCmd.AddCommand(newCloseGithubCopiedPullRequestCmd())
|
||||
|
||||
return closeCmd
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/github"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var closeCopiedOriginPullRequestReq = github.CloseCopiedOriginPullRequestReq{}
|
||||
|
||||
func newCloseGithubCopiedPullRequestCmd() *cobra.Command {
|
||||
closeCopiedOriginPRCmd := &cobra.Command{
|
||||
Use: "origin-pull-request [number]",
|
||||
Short: "Close the origin pull request of a copied pull request",
|
||||
RunE: runCloseGithubCopiedPullRequestCmd,
|
||||
Args: argsOnlyPRNumber(&closeCopiedOriginPullRequestReq.PullNumber),
|
||||
}
|
||||
|
||||
closeCopiedOriginPRCmd.PersistentFlags().StringVarP(&closeCopiedOriginPullRequestReq.Owner, "owner", "o", "hashicorp", "The Github organization")
|
||||
closeCopiedOriginPRCmd.PersistentFlags().StringVarP(&closeCopiedOriginPullRequestReq.Repo, "repo", "r", "vault-enterprise", "The Github repository. Private repositories require auth via a GITHUB_TOKEN env var")
|
||||
|
||||
return closeCopiedOriginPRCmd
|
||||
}
|
||||
|
||||
func runCloseGithubCopiedPullRequestCmd(cmd *cobra.Command, args []string) error {
|
||||
cmd.SilenceUsage = true // Don't spam the usage on failure
|
||||
|
||||
res, err := closeCopiedOriginPullRequestReq.Run(context.TODO(), githubCmdState.GithubV3, githubCmdState.GithubV4)
|
||||
switch rootCfg.format {
|
||||
case "json":
|
||||
b, err1 := res.ToJSON()
|
||||
if err != nil {
|
||||
return errors.Join(err, err1)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
case "markdown":
|
||||
tbl := res.ToTable(err)
|
||||
tbl.SetTitle("Close Origin Pull Request")
|
||||
fmt.Println(tbl.RenderMarkdown())
|
||||
default:
|
||||
fmt.Println(res.ToTable(err).Render())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -22,25 +22,7 @@ func newCopyGithubPullRequestCmd() *cobra.Command {
|
|||
Short: "Copy a pull request",
|
||||
Long: "Copy a pull request from the Community repository to the Enterprise repository",
|
||||
RunE: runCopyGithubPullRequestCmd,
|
||||
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 {
|
||||
copyGithubPullRequestReq.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)
|
||||
}
|
||||
},
|
||||
Args: argsOnlyPRNumber(©GithubPullRequestReq.PullNumber),
|
||||
}
|
||||
|
||||
copyPRCmd.PersistentFlags().StringVar(©GithubPullRequestReq.FromOrigin, "from-origin", "ce", "The name to use for the base remote origin")
|
||||
|
|
@ -58,7 +40,7 @@ func newCopyGithubPullRequestCmd() *cobra.Command {
|
|||
func runCopyGithubPullRequestCmd(cmd *cobra.Command, args []string) error {
|
||||
cmd.SilenceUsage = true // Don't spam the usage on failure
|
||||
|
||||
res, err := copyGithubPullRequestReq.Run(context.TODO(), githubCmdState.Github, githubCmdState.Git)
|
||||
res, err := copyGithubPullRequestReq.Run(context.TODO(), githubCmdState.GithubV3, githubCmdState.Git)
|
||||
|
||||
switch rootCfg.format {
|
||||
case "json":
|
||||
|
|
@ -77,3 +59,25 @@ func runCopyGithubPullRequestCmd(cmd *cobra.Command, args []string) error {
|
|||
|
||||
return err
|
||||
}
|
||||
|
||||
func argsOnlyPRNumber(dest *uint) func(cmd *cobra.Command, args []string) error {
|
||||
return 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 {
|
||||
*dest = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ func runCreateGithubBackportCmd(cmd *cobra.Command, args []string) error {
|
|||
createGithubBackportState.req.CEExclude = createGithubBackportState.req.CEExclude.Add(changed.FileGroup(eg))
|
||||
}
|
||||
|
||||
res := createGithubBackportState.req.Run(context.TODO(), githubCmdState.Github, githubCmdState.Git)
|
||||
res := createGithubBackportState.req.Run(context.TODO(), githubCmdState.GithubV3, githubCmdState.Git)
|
||||
if res == nil {
|
||||
res = &github.CreateBackportRes{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func newGithubFindWorkflowArtifactCmd() *cobra.Command {
|
|||
func runFindGithubWorkflowArtifactCmd(cmd *cobra.Command, args []string) error {
|
||||
cmd.SilenceUsage = true // Don't spam the usage on failure
|
||||
|
||||
res, err := findWorkflowArtifact.Run(context.TODO(), githubCmdState.Github)
|
||||
res, err := findWorkflowArtifact.Run(context.TODO(), githubCmdState.GithubV3)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding workflow artifact: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ func newGithubListChangedFilesCmd() *cobra.Command {
|
|||
func runListGithubChangedFilesCmd(cmd *cobra.Command, args []string) error {
|
||||
cmd.SilenceUsage = true // Don't spam the usage on failure
|
||||
|
||||
res, err := listGithubChangedFiles.Run(context.TODO(), githubCmdState.Github)
|
||||
res, err := listGithubChangedFiles.Run(context.TODO(), githubCmdState.GithubV3)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing changed files: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func newGithubListCommitStatusesCmd() *cobra.Command {
|
|||
func runListGithubCommitStatuses(cmd *cobra.Command, args []string) error {
|
||||
cmd.SilenceUsage = true // Don't spam the usage on failure
|
||||
|
||||
res, err := listGithubCommitStatuses.Run(context.TODO(), githubCmdState.Github)
|
||||
res, err := listGithubCommitStatuses.Run(context.TODO(), githubCmdState.GithubV3)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing github commit statuses: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ func newGithubListWorkflowRunsCmd() *cobra.Command {
|
|||
func runListGithubWorkflowsCmd(cmd *cobra.Command, args []string) error {
|
||||
cmd.SilenceUsage = true // Don't spam the usage on failure
|
||||
|
||||
res, err := listGithubWorkflowRuns.Run(context.TODO(), githubCmdState.Github)
|
||||
res, err := listGithubWorkflowRuns.Run(context.TODO(), githubCmdState.GithubV3)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing github workflow runs: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ func newSyncGithubBranchCmd() *cobra.Command {
|
|||
func runSyncGithubBranchCmd(cmd *cobra.Command, args []string) error {
|
||||
cmd.SilenceUsage = true // Don't spam the usage on failure
|
||||
|
||||
res, err := syncGithubBranchReq.Run(context.TODO(), githubCmdState.Github, githubCmdState.Git)
|
||||
res, err := syncGithubBranchReq.Run(context.TODO(), githubCmdState.GithubV3, githubCmdState.Git)
|
||||
|
||||
switch rootCfg.format {
|
||||
case "json":
|
||||
|
|
|
|||
|
|
@ -0,0 +1,366 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
libgithub "github.com/google/go-github/v74/github"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/shurcooL/githubv4"
|
||||
slogctx "github.com/veqryn/slog-context"
|
||||
)
|
||||
|
||||
// CloseCopiedOriginPullRequestReq is a request to copy a pull request from the CE repo to
|
||||
// the Ent repo.
|
||||
type CloseCopiedOriginPullRequestReq struct {
|
||||
Owner string
|
||||
Repo string
|
||||
PullNumber uint
|
||||
}
|
||||
|
||||
// CloseCopiedOriginPullRequestRes is a copy pull request response.
|
||||
type CloseCopiedOriginPullRequestRes struct {
|
||||
CopiedClosingIssues []*ClosingIssueRef `json:"copied_closing_issues,omitempty"`
|
||||
CopiedComment *libgithub.IssueComment `json:"copy_comment,omitempty"`
|
||||
CopiedPullRequest *libgithub.PullRequest `json:"copy_pull_request,omitempty"`
|
||||
OriginClosingIssues []*ClosingIssueRef `json:"origin_associated_issues,omitempty"`
|
||||
OriginComment *libgithub.IssueComment `json:"origin_comment,omitempty"`
|
||||
OriginPullRequest *libgithub.PullRequest `json:"origin_pull_request,omitempty"`
|
||||
}
|
||||
|
||||
// ClosingIssueRefs represents our Github GraphQL query for finding issues
|
||||
// associated with our Pull Request that ought to be automatically closed.
|
||||
//
|
||||
// The raw query looks something like:
|
||||
//
|
||||
// {
|
||||
// repository(owner: $owner, name: $repo) {
|
||||
// pullRequest(number: $number) {
|
||||
// repository {
|
||||
// nameWithOwner
|
||||
// }
|
||||
// number
|
||||
// closingIssuesReferences(first: 100) {
|
||||
// edges {
|
||||
// node {
|
||||
// url
|
||||
// number
|
||||
// title
|
||||
// closed
|
||||
// repository {
|
||||
// nameWithOwner
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
type ClosingIssueRefs struct {
|
||||
Repository struct {
|
||||
PullRequest struct {
|
||||
Repository struct {
|
||||
NameWithOwner string `json:"name_with_owner,omitempty"`
|
||||
} `json:"repository"`
|
||||
Number int `json:"number,omitempty"`
|
||||
ClosingIssuesReferences struct {
|
||||
Edges []struct {
|
||||
Node *ClosingIssueRef `json:"node"`
|
||||
} `json:"edges"`
|
||||
} `json:"closing_issues_references" graphql:"closingIssuesReferences(first: 100)"`
|
||||
} `json:"pull_request" graphql:"pullRequest(number: $number)"`
|
||||
} `json:"repository" graphql:"repository(owner: $owner, name: $repo)"`
|
||||
}
|
||||
|
||||
// ClosingIssueRef is an issue that is associated with a pull request.
|
||||
type ClosingIssueRef struct {
|
||||
URL string `json:"url,omitempty" graphql:"url"`
|
||||
Number int `json:"number,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Closed bool `json:"closed,omitempty"`
|
||||
Repository struct {
|
||||
NameWithOwner string `json:"name_with_owner,omitempty"`
|
||||
} `json:"repository"`
|
||||
}
|
||||
|
||||
// Run runs the request to copy a pull request from the CE repo to the Ent repo.
|
||||
func (r *CloseCopiedOriginPullRequestReq) Run(
|
||||
ctx context.Context,
|
||||
githubV3 *libgithub.Client,
|
||||
githubV4 *githubv4.Client,
|
||||
) (*CloseCopiedOriginPullRequestRes, error) {
|
||||
slog.Default().DebugContext(slogctx.Append(ctx,
|
||||
slog.String("owner", r.Owner),
|
||||
slog.String("repo", r.Repo),
|
||||
slog.Uint64("pull-number", uint64(r.PullNumber)),
|
||||
), "closing copied pull request")
|
||||
|
||||
res := &CloseCopiedOriginPullRequestRes{
|
||||
OriginClosingIssues: []*ClosingIssueRef{},
|
||||
CopiedClosingIssues: []*ClosingIssueRef{},
|
||||
}
|
||||
var err error
|
||||
originOwner, originRepo := "", ""
|
||||
var originNumber uint = 0
|
||||
|
||||
// Whenever possible we try to update base pull request with a status update
|
||||
// on how the copying has gone.
|
||||
createComments := func() {
|
||||
// Make sure we return a response even if we fail
|
||||
if res == nil {
|
||||
res = &CloseCopiedOriginPullRequestRes{
|
||||
OriginClosingIssues: []*ClosingIssueRef{},
|
||||
CopiedClosingIssues: []*ClosingIssueRef{},
|
||||
}
|
||||
}
|
||||
|
||||
var err1, err2 error
|
||||
res.CopiedComment, err1 = createPullRequestComment(
|
||||
ctx,
|
||||
githubV3,
|
||||
r.Owner,
|
||||
r.Repo,
|
||||
int(r.PullNumber),
|
||||
res.copiedCommentBody(err),
|
||||
)
|
||||
|
||||
res.OriginComment, err2 = createPullRequestComment(
|
||||
ctx,
|
||||
githubV3,
|
||||
originOwner,
|
||||
originRepo,
|
||||
int(originNumber),
|
||||
res.originCommentBody(err),
|
||||
)
|
||||
|
||||
// Set our finalized error on our response and also update our returned error
|
||||
err = errors.Join(err, err1, err2)
|
||||
}
|
||||
defer createComments()
|
||||
|
||||
err = r.validate()
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Get the pull details of the copied PR
|
||||
res.CopiedPullRequest, err = getPullRequest(ctx, githubV3, r.Owner, r.Repo, int(r.PullNumber))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Determine the origin PR from the copied PR branch name
|
||||
originOwner, originRepo, originNumber, _, err = decodeCopyPullRequestBranch(res.CopiedPullRequest.GetHead().GetRef())
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
slog.Default().DebugContext(slogctx.Append(ctx,
|
||||
slog.String("origin-owner", originOwner),
|
||||
slog.String("origin-repo", originRepo),
|
||||
slog.Uint64("origin-pull-number", uint64(originNumber)),
|
||||
), "decoded origin pull request information from copied pull request")
|
||||
|
||||
// Get the pull details of the origin PR
|
||||
res.OriginPullRequest, err = getPullRequest(ctx, githubV3, originOwner, originRepo, int(originNumber))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Close the origin PR if it's not closed already
|
||||
if res.OriginPullRequest.GetState() != "closed" {
|
||||
err = closePullRequest(ctx, githubV3, originOwner, originRepo, int(originNumber))
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("unable to close origin pull request: %w", err)
|
||||
}
|
||||
} else {
|
||||
slog.Default().DebugContext(ctx, "origin pull request has already been closed")
|
||||
}
|
||||
|
||||
// Close any issues associated with either the origin or copied PRs
|
||||
res.OriginClosingIssues, err = listPullRequestClosingIssues(
|
||||
ctx, githubV4, originOwner, originRepo, int(originNumber),
|
||||
)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.CopiedClosingIssues, err = listPullRequestClosingIssues(
|
||||
ctx, githubV4, r.Owner, r.Repo, int(r.PullNumber),
|
||||
)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
slog.Default().DebugContext(ctx, "closing any open associated issues")
|
||||
for _, issue := range slices.CompactFunc(
|
||||
append(res.OriginClosingIssues, res.CopiedClosingIssues...),
|
||||
closingIssueRefEqual,
|
||||
) {
|
||||
if issue.Closed {
|
||||
slog.Default().DebugContext(slogctx.Append(ctx,
|
||||
slog.String("repository", issue.Repository.NameWithOwner),
|
||||
slog.Int("number", issue.Number),
|
||||
), "associated issue is already closed")
|
||||
continue
|
||||
}
|
||||
|
||||
nwo := issue.Repository.NameWithOwner
|
||||
parts := strings.SplitN(nwo, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return res, fmt.Errorf("could not determine repo and owner from associated issue %d, got: %s",
|
||||
issue.Number,
|
||||
nwo,
|
||||
)
|
||||
}
|
||||
|
||||
err = closeIssue(ctx, githubV3, parts[0], parts[1], issue.Number)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("unable to close associated issue: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// validate ensures that we've been given all required fields necessary to
|
||||
// perform the request.
|
||||
func (r *CloseCopiedOriginPullRequestReq) validate() error {
|
||||
if r == nil {
|
||||
return errors.New("failed to initialize request")
|
||||
}
|
||||
|
||||
if r.Owner == "" {
|
||||
return errors.New("no github owner has been provided")
|
||||
}
|
||||
|
||||
if r.Repo == "" {
|
||||
return errors.New("no github repository has been provided")
|
||||
}
|
||||
|
||||
if r.PullNumber == 0 {
|
||||
return errors.New("no github pull request number has been provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copiedCommentBody is the markdown comment body that we'll attempt to set on
|
||||
// the copied pull request.
|
||||
func (r *CloseCopiedOriginPullRequestRes) copiedCommentBody(err error) string {
|
||||
if r == nil {
|
||||
return "no close copied origin pull request response has been initialized"
|
||||
}
|
||||
|
||||
t := r.ToTable(err)
|
||||
if err == nil {
|
||||
t.SetTitle("Origin pull request and all associated issues have been closed!")
|
||||
return t.RenderMarkdown()
|
||||
}
|
||||
|
||||
if t.Length() == 0 {
|
||||
// If we don't have any rows in our table then there's no need to render a
|
||||
// table so we'll just return an error
|
||||
return "## Closing origin pull request failed!\n\nError: " + err.Error()
|
||||
}
|
||||
|
||||
// Render out our table but put the error message in the caption
|
||||
t.SetTitle("Closing origin pull request failed!")
|
||||
// Set the caption to the top-level error only as any attempt errors are
|
||||
// nested in the table.
|
||||
t.SetCaption("Error: " + err.Error())
|
||||
|
||||
return t.RenderMarkdown()
|
||||
}
|
||||
|
||||
// originCommentBody is the markdown comment body that we'll attempt to set on
|
||||
// the origin pull request.
|
||||
func (r *CloseCopiedOriginPullRequestRes) originCommentBody(err error) string {
|
||||
if r == nil {
|
||||
return "no close copied origin pull request response has been initialized"
|
||||
}
|
||||
|
||||
t := r.ToTable(err)
|
||||
if err == nil {
|
||||
t.SetTitle("Copied pull request has been merged!")
|
||||
return t.RenderMarkdown()
|
||||
}
|
||||
|
||||
if t.Length() == 0 {
|
||||
// If we don't have any rows in our table then there's no need to render a
|
||||
// table so we'll just return an error
|
||||
return "## Copied pull request has been merged!\n\nError: " + err.Error()
|
||||
}
|
||||
|
||||
// Render out our table but put the error message in the caption
|
||||
t.SetTitle("Copy pull request failed!")
|
||||
// Set the caption to the top-level error only as any attempt errors are
|
||||
// nested in the table.
|
||||
t.SetCaption("Error: " + err.Error())
|
||||
|
||||
return t.RenderMarkdown()
|
||||
}
|
||||
|
||||
// ToJSON marshals the response to JSON.
|
||||
func (r *CloseCopiedOriginPullRequestRes) ToJSON() ([]byte, error) {
|
||||
b, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling list changed files to JSON: %w", err)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// ToTable marshals the response to a text table.
|
||||
func (r *CloseCopiedOriginPullRequestRes) ToTable(err error) table.Writer {
|
||||
t := table.NewWriter()
|
||||
t.Style().Options.DrawBorder = false
|
||||
t.Style().Options.SeparateColumns = false
|
||||
t.Style().Options.SeparateFooter = false
|
||||
t.Style().Options.SeparateHeader = false
|
||||
t.Style().Options.SeparateRows = false
|
||||
|
||||
t.AppendHeader(table.Row{
|
||||
"Origin Pull Request", "Copied Pull Request", "Commit SHA", "Error",
|
||||
})
|
||||
row := table.Row{
|
||||
r.OriginPullRequest.GetHTMLURL(),
|
||||
r.CopiedPullRequest.GetHTMLURL(),
|
||||
fmt.Sprintf(
|
||||
"https://github.com/%s/commit/%s",
|
||||
r.CopiedPullRequest.GetHead().GetRepo().GetFullName(),
|
||||
r.CopiedPullRequest.GetMergeCommitSHA(),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
row = append(row, err.Error())
|
||||
}
|
||||
|
||||
t.AppendRow(row)
|
||||
|
||||
closedIssues := slices.CompactFunc(
|
||||
append(r.OriginClosingIssues, r.CopiedClosingIssues...),
|
||||
closingIssueRefEqual,
|
||||
)
|
||||
if len(closedIssues) > 0 {
|
||||
urls := []string{}
|
||||
for _, ci := range closedIssues {
|
||||
urls = append(urls, ci.URL)
|
||||
}
|
||||
t.AppendFooter(table.Row{
|
||||
"", "Closed Issues", strings.Join(urls, "\n"),
|
||||
})
|
||||
}
|
||||
|
||||
t.SuppressEmptyColumns()
|
||||
t.SuppressTrailingSpaces()
|
||||
|
||||
return t
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// TestAcc_AssociatedIssues does an actual request against a known PR to check
|
||||
// whether or not or query works as expected.
|
||||
func TestAcc_AssociatedIssues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
token, setToken := os.LookupEnv("GITHUB_TOKEN")
|
||||
_, setACC := os.LookupEnv("PIPELINE_ACC")
|
||||
if !setACC && setToken {
|
||||
t.Skip("GITHUB_TOKEN and PIPELINE_ACC are not set")
|
||||
}
|
||||
|
||||
github := githubv4.NewClient(
|
||||
oauth2.NewClient(context.Background(),
|
||||
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}),
|
||||
),
|
||||
)
|
||||
|
||||
ai := &ClosingIssueRefs{}
|
||||
require.NoError(t, github.Query(t.Context(), ai, map[string]any{
|
||||
"owner": githubv4.String("hashicorp"),
|
||||
"repo": githubv4.String("vault-enterprise"),
|
||||
"number": githubv4.Int(9484),
|
||||
}))
|
||||
|
||||
require.Equal(t, "hashicorp/vault-enterprise", ai.Repository.PullRequest.Repository.NameWithOwner)
|
||||
require.Equal(t, 9484, ai.Repository.PullRequest.Number)
|
||||
require.Len(t, ai.Repository.PullRequest.ClosingIssuesReferences.Edges, 1)
|
||||
require.True(t, ai.Repository.PullRequest.ClosingIssuesReferences.Edges[0].Node.Closed)
|
||||
require.Equal(t, 31545, ai.Repository.PullRequest.ClosingIssuesReferences.Edges[0].Node.Number)
|
||||
require.Equal(t,
|
||||
"https://github.com/hashicorp/vault/issues/31545",
|
||||
ai.Repository.PullRequest.ClosingIssuesReferences.Edges[0].Node.URL,
|
||||
)
|
||||
}
|
||||
|
|
@ -165,8 +165,10 @@ func (r *CopyPullRequestReq) Run(
|
|||
return res, err
|
||||
}
|
||||
|
||||
// Create a new branch for our copied changes.
|
||||
branchName := r.copyBranchNameForRef(baseRef, prBranch)
|
||||
// Create a new branch for our copied changes. Encode the details of our origin
|
||||
// pull request into the branch name so that future post-merge operations can
|
||||
// determine the origin PR using only the branch name.
|
||||
branchName := encodeCopyPullRequestBranch(r.FromOwner, r.FromRepo, r.PullNumber, prBranch)
|
||||
// We don't have local references so create a new branch from our tracking branch
|
||||
baseBranch := "remotes/" + r.ToOrigin + "/" + baseRef
|
||||
slog.Default().DebugContext(ctx, "checking out new copy branch")
|
||||
|
|
@ -338,21 +340,6 @@ func (r *CopyPullRequestReq) Run(
|
|||
return res, nil
|
||||
}
|
||||
|
||||
// copyBranchNameForRef returns then branch name to use for our PR copy operation.
|
||||
// e.g. copy/release/1.19.x+ent/my-feature-branch
|
||||
func (r CopyPullRequestReq) copyBranchNameForRef(
|
||||
ref string,
|
||||
prBranch string,
|
||||
) string {
|
||||
name := fmt.Sprintf("copy/%s/%s", ref, prBranch)
|
||||
if len(name) > 250 {
|
||||
// Handle Githubs branch name max length
|
||||
name = name[:250]
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// Validate ensures that we've been given the minimum filter arguments necessary to complete a
|
||||
// request. It is always recommended that additional fitlers be given to reduce the response size
|
||||
// and not exhaust API limits.
|
||||
|
|
|
|||
126
tools/pipeline/internal/pkg/github/copy_pull_request_branch.go
Normal file
126
tools/pipeline/internal/pkg/github/copy_pull_request_branch.go
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package github
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// copyBranchPrefix is the prefix for copied pull request branches. It is used
|
||||
// both for copying pull requests and for closing copied pull requests.
|
||||
const copyBranchPrefix = "copy"
|
||||
|
||||
// encodeCopyPullRequestBranch returns the branch name to use for our PR copy operation.
|
||||
// The branch name is important because we trigger post-merge automation based
|
||||
// on the branch prefix. We also encode origin PR information for use in
|
||||
// post-merge operations.
|
||||
//
|
||||
// The format is: <copy-branch-prefix>/<owner>/<repo>/<pr>/<pr-branch-name>
|
||||
// eg: copy/hashicorp/vault/99999/my-feature-branch
|
||||
//
|
||||
// This informs the post-merge close operations that the Pull Request was originally
|
||||
// copied from hashicorp/vault's Pull Request number 99999 and that the branch
|
||||
// name of the pull request is my-feature-branch.
|
||||
//
|
||||
// It's imporant that both the encodeCopyPullRequestBranch and decodeCopyPullRequestBranch
|
||||
// stay in sync.
|
||||
func encodeCopyPullRequestBranch(
|
||||
owner string,
|
||||
repo string,
|
||||
number uint,
|
||||
prBranch string,
|
||||
) string {
|
||||
prNumber := strconv.Itoa(int(number))
|
||||
name := strings.Join([]string{
|
||||
copyBranchPrefix,
|
||||
owner,
|
||||
repo,
|
||||
prNumber,
|
||||
prBranch,
|
||||
}, "/")
|
||||
if len(name) > 250 {
|
||||
// Handle Githubs branch name max length
|
||||
name = name[:250]
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// decodeCopyPullRequestBranch returns the encoded origin PR details from the
|
||||
// copied Pull Request branch name.
|
||||
//
|
||||
// The format must be the same as what is described in encodeCopyPullRequestBranch()
|
||||
func decodeCopyPullRequestBranch(ref string) (
|
||||
owner string,
|
||||
repo string,
|
||||
number uint,
|
||||
branch string,
|
||||
err error,
|
||||
) {
|
||||
if ref == "" {
|
||||
err = errors.New("no copy branch provided")
|
||||
return owner, repo, number, branch, err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(ref, copyBranchPrefix) {
|
||||
err = fmt.Errorf("invalid copy branch: branch does not start with %s", copyBranchPrefix)
|
||||
return owner, repo, number, branch, err
|
||||
}
|
||||
|
||||
// eg: copy/hashicorp/vault/99999/my-feature-branch
|
||||
parts := strings.SplitN(ref, "/", 5)
|
||||
if len(parts) < 5 {
|
||||
err = fmt.Errorf("invalid copy branch: expected 5 parts, got %d", len(parts))
|
||||
return owner, repo, number, branch, err
|
||||
}
|
||||
owner = parts[1]
|
||||
repo = parts[2]
|
||||
|
||||
var signedNumber int
|
||||
signedNumber, err = strconv.Atoi(parts[3])
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid copy branch: pull request number is not a number: %w", err)
|
||||
return owner, repo, number, branch, err
|
||||
}
|
||||
if signedNumber < 0 {
|
||||
err = fmt.Errorf("invalid copy branch: number must be positive, got %d", signedNumber)
|
||||
return owner, repo, number, branch, err
|
||||
}
|
||||
number = uint(signedNumber)
|
||||
|
||||
branch = parts[4]
|
||||
|
||||
return owner, repo, number, branch, err
|
||||
}
|
||||
|
||||
func closingIssueRefEqual(a, b *ClosingIssueRef) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if (a == nil && b != nil) || (a != nil && b == nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.URL != b.URL {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.Number != b.Number {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.Title != b.Title {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.Repository.NameWithOwner != b.Repository.NameWithOwner {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -91,7 +91,7 @@ type CreateBackportReq struct {
|
|||
// NewCreateBackportPRReq()
|
||||
type NewCreateBackportReqOpt func(*CreateBackportReq)
|
||||
|
||||
// CreateBackportPRReq is a respose of creating a backport pull request
|
||||
// CreateBackportRes is a respose of creating a backport pull request
|
||||
type CreateBackportRes struct {
|
||||
OriginPullRequest *libgithub.PullRequest `json:"origin_pull_request,omitempty"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
|
|
|
|||
34
tools/pipeline/internal/pkg/github/issue.go
Normal file
34
tools/pipeline/internal/pkg/github/issue.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
libgithub "github.com/google/go-github/v74/github"
|
||||
slogctx "github.com/veqryn/slog-context"
|
||||
)
|
||||
|
||||
// closeIssue closes an issue.
|
||||
func closeIssue(
|
||||
ctx context.Context,
|
||||
github *libgithub.Client,
|
||||
owner string,
|
||||
repo string,
|
||||
issueNumber int,
|
||||
) error {
|
||||
ctx = slogctx.Append(ctx,
|
||||
slog.String("owner", owner),
|
||||
slog.String("repo", repo),
|
||||
slog.Int("issue-number", issueNumber),
|
||||
)
|
||||
slog.Default().DebugContext(ctx, "closing issue")
|
||||
|
||||
_, _, err := github.Issues.Edit(ctx, owner, repo, issueNumber, &libgithub.IssueRequest{
|
||||
State: libgithub.Ptr("closed"),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"log/slog"
|
||||
|
||||
libgithub "github.com/google/go-github/v74/github"
|
||||
"github.com/shurcooL/githubv4"
|
||||
slogctx "github.com/veqryn/slog-context"
|
||||
)
|
||||
|
||||
|
|
@ -64,6 +65,28 @@ func getPullRequest(
|
|||
return pr, nil
|
||||
}
|
||||
|
||||
// closePullRequest closes a pull request.
|
||||
func closePullRequest(
|
||||
ctx context.Context,
|
||||
github *libgithub.Client,
|
||||
owner string,
|
||||
repo string,
|
||||
pullNumber int,
|
||||
) error {
|
||||
ctx = slogctx.Append(ctx,
|
||||
slog.String("owner", owner),
|
||||
slog.String("repo", repo),
|
||||
slog.Int("pull-number", pullNumber),
|
||||
)
|
||||
slog.Default().DebugContext(ctx, "closing pull request")
|
||||
|
||||
_, _, err := github.PullRequests.Edit(ctx, owner, repo, pullNumber, &libgithub.PullRequest{
|
||||
State: libgithub.Ptr("closed"),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// listPullRequestCommits lists all of the commits associated with a pull request.
|
||||
func listPullRequestCommits(
|
||||
ctx context.Context,
|
||||
|
|
@ -127,3 +150,37 @@ func listPullRequestReviews(
|
|||
opts.Page = res.NextPage
|
||||
}
|
||||
}
|
||||
|
||||
// listPullRequestClosingIssues lists all "closing issues" associated with
|
||||
// a pull request. These can be set using keywords associations or manually
|
||||
// using the "development" sidebar on either an issue or pull request.
|
||||
func listPullRequestClosingIssues(
|
||||
ctx context.Context,
|
||||
github *githubv4.Client,
|
||||
owner string,
|
||||
repo string,
|
||||
pullNumber int,
|
||||
) ([]*ClosingIssueRef, error) {
|
||||
slog.Default().DebugContext(slogctx.Append(ctx,
|
||||
slog.String("owner", owner),
|
||||
slog.String("repo", repo),
|
||||
slog.Int("pull-number", pullNumber),
|
||||
), "getting pull request associated issues")
|
||||
|
||||
oai := ClosingIssueRefs{}
|
||||
err := github.Query(ctx, &oai, map[string]any{
|
||||
"owner": githubv4.String(owner),
|
||||
"repo": githubv4.String(repo),
|
||||
"number": githubv4.Int(pullNumber),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ais := []*ClosingIssueRef{}
|
||||
for _, ai := range oai.Repository.PullRequest.ClosingIssuesReferences.Edges {
|
||||
ais = append(ais, ai.Node)
|
||||
}
|
||||
|
||||
return ais, nil
|
||||
}
|
||||
|
|
|
|||
50
tools/pipeline/internal/pkg/github/pull_request_test.go
Normal file
50
tools/pipeline/internal/pkg/github/pull_request_test.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package github
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_encodeCopyPullRequest_Roundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for desc, test := range map[string]struct {
|
||||
owner string
|
||||
repo string
|
||||
number uint
|
||||
prBranch string
|
||||
expectedPRBranch string
|
||||
}{
|
||||
"standard": {
|
||||
owner: "hashicorp",
|
||||
repo: "vault",
|
||||
number: 49689,
|
||||
prBranch: "my-feature-branch",
|
||||
expectedPRBranch: "my-feature-branch",
|
||||
},
|
||||
"super long": {
|
||||
owner: "hashicorp",
|
||||
repo: "vault",
|
||||
number: 49689,
|
||||
prBranch: "Lorem-ipsum-dolor-sit-amet--consectetur-adipiscing-elit--Praesent-accumsan-metus-sed-vehicula-accumsan--Nunc-semper-vehicula-tempor--Vestibulum-fringilla-enim-nec-ipsum-tincidunt-viverra--Etiam-iaculis-metus-ultricies-risus-rutrum--et-lobortis-orci-al",
|
||||
// truncated the branch name as we'll hit our max char limit
|
||||
expectedPRBranch: "Lorem-ipsum-dolor-sit-amet--consectetur-adipiscing-elit--Praesent-accumsan-metus-sed-vehicula-accumsan--Nunc-semper-vehicula-tempor--Vestibulum-fringilla-enim-nec-ipsum-tincidunt-viverra--Etiam-iaculis-metus-ultricies-risus",
|
||||
},
|
||||
} {
|
||||
t.Run(desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodedBranch := encodeCopyPullRequestBranch(test.owner, test.repo, test.number, test.prBranch)
|
||||
owner, repo, number, branch, err := decodeCopyPullRequestBranch(encodedBranch)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.owner, owner)
|
||||
require.Equal(t, test.repo, repo)
|
||||
require.Equal(t, test.number, number)
|
||||
require.Equal(t, test.expectedPRBranch, branch)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue