Skip to content

Commit

Permalink
feat(github): allow to PR cross-repo (#4053)
Browse files Browse the repository at this point in the history
This allows to open pull requests across repositories on nix, brew, krew
and scoop.

closes #4048

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
  • Loading branch information
caarlos0 committed May 29, 2023
1 parent a3bc051 commit 773cb91
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 26 deletions.
2 changes: 1 addition & 1 deletion internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ type ReleaseNotesGenerator interface {

// PullRequestOpener can open pull requests.
type PullRequestOpener interface {
OpenPullRequest(ctx *context.Context, repo Repo, head, title string) error
OpenPullRequest(ctx *context.Context, base, head Repo, title string) error
}

// New creates a new client depending on the token type.
Expand Down
31 changes: 21 additions & 10 deletions internal/client/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,23 +158,35 @@ func (c *githubClient) CloseMilestone(ctx *context.Context, repo Repo, title str
return err
}

func headString(base, head Repo) string {
return strings.Join([]string{
firstNonEmpty(head.Owner, base.Owner),
firstNonEmpty(head.Name, base.Name),
firstNonEmpty(head.Branch, base.Branch),
}, ":")
}

func (c *githubClient) OpenPullRequest(
ctx *context.Context,
repo Repo,
base, title string,
base, head Repo,
title string,
) error {
c.checkRateLimit(ctx)
if base == "" {
def, err := c.getDefaultBranch(ctx, repo)
if base.Branch == "" {
def, err := c.getDefaultBranch(ctx, base)
if err != nil {
return err
}
base = def
base.Branch = def
}
pr, res, err := c.client.PullRequests.Create(ctx, repo.Owner, repo.Name, &github.NewPullRequest{
log := log.
WithField("base", headString(base, Repo{})).
WithField("head", headString(base, head))
log.Info("opening pull request")
pr, res, err := c.client.PullRequests.Create(ctx, base.Owner, base.Name, &github.NewPullRequest{
Title: github.String(title),
Head: github.String(repo.Branch),
Base: github.String(base),
Base: github.String(base.Branch),
Head: github.String(headString(base, head)),
Body: github.String("Automatically generated by [GoReleaser](https://goreleaser.com)"),
})
if err != nil {
Expand Down Expand Up @@ -224,8 +236,7 @@ func (c *githubClient) CreateFile(

log.
WithField("repository", repo.String()).
WithField("name", repo.Name).
WithField("name", repo.Name).
WithField("branch", repo.Branch).
Info("pushing")

if defBranch != branch && branch != "" {
Expand Down
118 changes: 111 additions & 7 deletions internal/client/github_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"encoding/json"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -404,6 +405,54 @@ func TestCloseMilestone(t *testing.T) {
require.NoError(t, client.CloseMilestone(ctx, repo, "v1.13.0"))
}

func TestOpenPullRequestCrossRepo(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()

if r.URL.Path == "/repos/someone/something/pulls" {
got, err := io.ReadAll(r.Body)
require.NoError(t, err)
var pr github.NewPullRequest
require.NoError(t, json.Unmarshal(got, &pr))
require.Equal(t, "main", pr.GetBase())
require.Equal(t, "someoneelse:something:foo", pr.GetHead())
r, err := os.Open("testdata/github/pull.json")
require.NoError(t, err)
_, err = io.Copy(w, r)
require.NoError(t, err)
return
}

if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}

t.Error("unhandled request: " + r.URL.Path)
}))
defer srv.Close()

ctx := testctx.NewWithCfg(config.Project{
GitHubURLs: config.GitHubURLs{
API: srv.URL + "/",
},
})
client, err := newGitHub(ctx, "test-token")
require.NoError(t, err)
base := Repo{
Owner: "someone",
Name: "something",
Branch: "main",
}
head := Repo{
Owner: "someoneelse",
Name: "something",
Branch: "foo",
}
require.NoError(t, client.OpenPullRequest(ctx, base, head, "some title"))
}

func TestOpenPullRequestHappyPath(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
Expand All @@ -426,6 +475,57 @@ func TestOpenPullRequestHappyPath(t *testing.T) {
}))
defer srv.Close()

ctx := testctx.NewWithCfg(config.Project{
GitHubURLs: config.GitHubURLs{
API: srv.URL + "/",
},
})
client, err := newGitHub(ctx, "test-token")
require.NoError(t, err)
repo := Repo{
Owner: "someone",
Name: "something",
Branch: "main",
}

require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{}, "some title"))
}

func TestOpenPullRequestNoBaseBranch(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()

if r.URL.Path == "/repos/someone/something/pulls" {
got, err := io.ReadAll(r.Body)
require.NoError(t, err)
var pr github.NewPullRequest
require.NoError(t, json.Unmarshal(got, &pr))
require.Equal(t, "main", pr.GetBase())
require.Equal(t, "someone:something:foo", pr.GetHead())

r, err := os.Open("testdata/github/pull.json")
require.NoError(t, err)
_, err = io.Copy(w, r)
require.NoError(t, err)
return
}

if r.URL.Path == "/repos/someone/something" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"default_branch": "main"}`)
return
}

if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}

t.Error("unhandled request: " + r.URL.Path)
}))
defer srv.Close()

ctx := testctx.NewWithCfg(config.Project{
GitHubURLs: config.GitHubURLs{
API: srv.URL + "/",
Expand All @@ -438,7 +538,9 @@ func TestOpenPullRequestHappyPath(t *testing.T) {
Name: "something",
}

require.NoError(t, client.OpenPullRequest(ctx, repo, "main", "some title"))
require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{
Branch: "foo",
}, "some title"))
}

func TestOpenPullRequestPRExists(t *testing.T) {
Expand Down Expand Up @@ -472,11 +574,12 @@ func TestOpenPullRequestPRExists(t *testing.T) {
client, err := newGitHub(ctx, "test-token")
require.NoError(t, err)
repo := Repo{
Owner: "someone",
Name: "something",
Owner: "someone",
Name: "something",
Branch: "main",
}

require.NoError(t, client.OpenPullRequest(ctx, repo, "main", "some title"))
require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{}, "some title"))
}

func TestOpenPullRequestBaseEmpty(t *testing.T) {
Expand Down Expand Up @@ -515,11 +618,12 @@ func TestOpenPullRequestBaseEmpty(t *testing.T) {
client, err := newGitHub(ctx, "test-token")
require.NoError(t, err)
repo := Repo{
Owner: "someone",
Name: "something",
Owner: "someone",
Name: "something",
Branch: "main",
}

require.NoError(t, client.OpenPullRequest(ctx, repo, "", "some title"))
require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{}, "some title"))
}

func TestGitHubCreateFileHappyPathCreate(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/client/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type Mock struct {
OpenedPullRequest bool
}

func (c *Mock) OpenPullRequest(_ *context.Context, _ Repo, _, _ string) error {
func (c *Mock) OpenPullRequest(_ *context.Context, _, _ Repo, _ string) error {
c.OpenedPullRequest = true
return nil
}
Expand Down
6 changes: 5 additions & 1 deletion internal/pipe/brew/brew.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,11 @@ func doPublish(ctx *context.Context, formula *artifact.Artifact, cl client.Clien
}

title := fmt.Sprintf("Updated %s to %s", ctx.Config.ProjectName, ctx.Version)
return pcl.OpenPullRequest(ctx, repo, brew.Tap.PullRequest.Base, title)
return pcl.OpenPullRequest(ctx, client.Repo{
Name: brew.Tap.PullRequest.Base.Name,
Owner: brew.Tap.PullRequest.Base.Owner,
Branch: brew.Tap.PullRequest.Base.Branch,
}, repo, title)
}

func doRun(ctx *context.Context, brew config.Homebrew, cl client.ReleaserURLTemplater) error {
Expand Down
6 changes: 5 additions & 1 deletion internal/pipe/krew/krew.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,11 @@ func doPublish(ctx *context.Context, manifest *artifact.Artifact, cl client.Clie
}

title := fmt.Sprintf("Updated %s to %s", ctx.Config.ProjectName, ctx.Version)
return pcl.OpenPullRequest(ctx, repo, cfg.Index.PullRequest.Base, title)
return pcl.OpenPullRequest(ctx, client.Repo{
Name: cfg.Index.PullRequest.Base.Name,
Owner: cfg.Index.PullRequest.Base.Owner,
Branch: cfg.Index.PullRequest.Base.Branch,
}, repo, title)
}

func buildManifestPath(folder, filename string) string {
Expand Down
6 changes: 5 additions & 1 deletion internal/pipe/nix/nix.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,11 @@ func doPublish(ctx *context.Context, prefetcher shaPrefetcher, cl client.Client,
}

title := fmt.Sprintf("Updated %s to %s", ctx.Config.ProjectName, ctx.Version)
return pcl.OpenPullRequest(ctx, repo, nix.Repository.PullRequest.Base, title)
return pcl.OpenPullRequest(ctx, client.Repo{
Name: nix.Repository.PullRequest.Base.Name,
Owner: nix.Repository.PullRequest.Base.Owner,
Branch: nix.Repository.PullRequest.Base.Branch,
}, repo, title)
}

func doBuildPkg(ctx *context.Context, data templateData) (string, error) {
Expand Down
6 changes: 5 additions & 1 deletion internal/pipe/scoop/scoop.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,11 @@ func doPublish(ctx *context.Context, manifest *artifact.Artifact, cl client.Clie
}

title := fmt.Sprintf("Updated %s to %s", ctx.Config.ProjectName, ctx.Version)
return pcl.OpenPullRequest(ctx, repo, scoop.Bucket.PullRequest.Base, title)
return pcl.OpenPullRequest(ctx, client.Repo{
Name: scoop.Bucket.PullRequest.Base.Name,
Owner: scoop.Bucket.PullRequest.Base.Owner,
Branch: scoop.Bucket.PullRequest.Base.Branch,
}, repo, title)
}

// Manifest represents a scoop.sh App Manifest.
Expand Down
48 changes: 46 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,53 @@ type GitRepoRef struct {
PrivateKey string `yaml:"private_key,omitempty" json:"private_key,omitempty"`
}

type PullRequestBase struct {
Owner string `yaml:"owner,omitempty" json:"owner,omitempty"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Branch string `yaml:"branch,omitempty" json:"branch,omitempty"`
}

// type alias to prevent stack overflowing in the custom unmarshaler.
type pullRequestBase PullRequestBase

// UnmarshalYAML is a custom unmarshaler that accept brew deps in both the old and new format.
func (a *PullRequestBase) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err == nil {
a.Branch = str
return nil
}

var base pullRequestBase
if err := unmarshal(&base); err != nil {
return err
}

a.Branch = base.Branch
a.Owner = base.Owner
a.Name = base.Name

return nil
}

func (a PullRequestBase) JSONSchema() *jsonschema.Schema {
reflector := jsonschema.Reflector{
ExpandedStruct: true,
}
schema := reflector.Reflect(&pullRequestBase{})
return &jsonschema.Schema{
OneOf: []*jsonschema.Schema{
{
Type: "string",
},
schema,
},
}
}

type PullRequest struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Base string `yaml:"base,omitempty" json:"base,omitempty"`
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Base PullRequestBase `yaml:"base,omitempty" json:"base,omitempty"`
}

// HomebrewDependency represents Homebrew dependency.
Expand Down
10 changes: 10 additions & 0 deletions www/docs/customization/homebrew.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,20 @@ brews:
enabled: true

# Base branch of the PR.
# If base is a string, the PR will be opened into the same repository.
#
# Default: default repository branch.
base: main

# Base can also be another repository, in which case the owner and name
# above will be used as HEAD, allowing cross-repository pull requests.
#
# Since: v1.19
base:
owner: org
name: nur
branch: main

# Clone, create the file, commit and push, to a regular Git repository.
#
# Notice that this will only have any effect if the given URL is not
Expand Down
10 changes: 10 additions & 0 deletions www/docs/customization/krew.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,20 @@ krews:
enabled: true

# Base branch of the PR.
# If base is a string, the PR will be opened into the same repository.
#
# Default: default repository branch.
base: main

# Base can also be another repository, in which case the owner and name
# above will be used as HEAD, allowing cross-repository pull requests.
#
# Since: v1.19
base:
owner: org
name: nur
branch: main

# Clone, create the file, commit and push, to a regular Git repository.
#
# Notice that this will only have any effect if the given URL is not
Expand Down

0 comments on commit 773cb91

Please sign in to comment.