1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-07-05 00:59:04 +02:00

feat(github): allow to PR cross-repo (#4053)

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>
This commit is contained in:
Carlos Alexandro Becker
2023-05-29 14:37:10 -03:00
committed by GitHub
parent a3bc051933
commit 773cb91a7a
13 changed files with 241 additions and 26 deletions

View File

@ -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.

View File

@ -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 {
@ -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 != "" {

View File

@ -1,6 +1,7 @@
package client
import (
"encoding/json"
"fmt"
"io"
"net/http"
@ -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()
@ -436,9 +485,62 @@ func TestOpenPullRequestHappyPath(t *testing.T) {
repo := Repo{
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 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 + "/",
},
})
client, err := newGitHub(ctx, "test-token")
require.NoError(t, err)
repo := Repo{
Owner: "someone",
Name: "something",
}
require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{
Branch: "foo",
}, "some title"))
}
func TestOpenPullRequestPRExists(t *testing.T) {
@ -474,9 +576,10 @@ func TestOpenPullRequestPRExists(t *testing.T) {
repo := Repo{
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) {
@ -517,9 +620,10 @@ func TestOpenPullRequestBaseEmpty(t *testing.T) {
repo := Repo{
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) {

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {

View File

@ -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.

View File

@ -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"`
Base PullRequestBase `yaml:"base,omitempty" json:"base,omitempty"`
}
// HomebrewDependency represents Homebrew dependency.

View File

@ -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

View File

@ -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

View File

@ -47,7 +47,7 @@ nix:
#
# Default: default repository branch.
# Templates: allowed
branch: main
branch: foo
# Optionally a token can be provided, if it differs from the token
# provided to GoReleaser
@ -63,10 +63,20 @@ nix:
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

View File

@ -60,10 +60,20 @@ scoops:
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