mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-03-19 20:57:53 +02:00
Some changelogers were always returning the short sha instead of the full one, so setting an abbrev had no effect. This fixes these changelogers, which should now always return the full sha. Note that github usually still shortens the SHAs rendered in markdown, so the changes might not even be visible in most cases. closes #4829
676 lines
17 KiB
Go
676 lines
17 KiB
Go
package client
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/caarlos0/log"
|
|
"github.com/charmbracelet/x/exp/ordered"
|
|
"github.com/goreleaser/goreleaser/v2/internal/artifact"
|
|
"github.com/goreleaser/goreleaser/v2/internal/tmpl"
|
|
"github.com/goreleaser/goreleaser/v2/pkg/config"
|
|
"github.com/goreleaser/goreleaser/v2/pkg/context"
|
|
"github.com/xanzy/go-gitlab"
|
|
)
|
|
|
|
const DefaultGitLabDownloadURL = "https://gitlab.com"
|
|
|
|
var (
|
|
_ Client = &gitlabClient{}
|
|
_ PullRequestOpener = &gitlabClient{}
|
|
)
|
|
|
|
type gitlabClient struct {
|
|
client *gitlab.Client
|
|
authType gitlab.AuthType
|
|
}
|
|
|
|
// newGitLab returns a gitlab client implementation.
|
|
func newGitLab(ctx *context.Context, token string) (*gitlabClient, error) {
|
|
transport := &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
TLSClientConfig: &tls.Config{
|
|
//nolint:gosec
|
|
InsecureSkipVerify: ctx.Config.GitLabURLs.SkipTLSVerify,
|
|
},
|
|
}
|
|
options := []gitlab.ClientOptionFunc{
|
|
gitlab.WithHTTPClient(&http.Client{
|
|
Transport: transport,
|
|
}),
|
|
}
|
|
if ctx.Config.GitLabURLs.API != "" {
|
|
apiURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.API)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("templating GitLab API URL: %w", err)
|
|
}
|
|
|
|
options = append(options, gitlab.WithBaseURL(apiURL))
|
|
}
|
|
|
|
var client *gitlab.Client
|
|
var authType gitlab.AuthType
|
|
var err error
|
|
if checkUseJobToken(*ctx, token) {
|
|
client, err = gitlab.NewJobClient(token, options...)
|
|
authType = gitlab.JobToken
|
|
} else {
|
|
client, err = gitlab.NewClient(token, options...)
|
|
authType = gitlab.PrivateToken
|
|
}
|
|
if err != nil {
|
|
return &gitlabClient{}, err
|
|
}
|
|
return &gitlabClient{
|
|
client: client,
|
|
authType: authType,
|
|
}, nil
|
|
}
|
|
|
|
func (c *gitlabClient) checkIsPrivateToken() error {
|
|
if c.authType == gitlab.PrivateToken {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("the necessary APIs are not available when using CI_JOB_TOKEN")
|
|
}
|
|
|
|
func (c *gitlabClient) Changelog(_ *context.Context, repo Repo, prev, current string) ([]ChangelogItem, error) {
|
|
if err := c.checkIsPrivateToken(); err != nil {
|
|
return nil, fmt.Errorf("changelog: %w", err)
|
|
}
|
|
cmpOpts := &gitlab.CompareOptions{
|
|
From: &prev,
|
|
To: ¤t,
|
|
}
|
|
result, _, err := c.client.Repositories.Compare(repo.String(), cmpOpts)
|
|
var log []ChangelogItem
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, commit := range result.Commits {
|
|
log = append(log, ChangelogItem{
|
|
SHA: commit.ID,
|
|
Message: strings.Split(commit.Message, "\n")[0],
|
|
AuthorName: commit.AuthorName,
|
|
AuthorEmail: commit.AuthorEmail,
|
|
})
|
|
}
|
|
return log, nil
|
|
}
|
|
|
|
// getDefaultBranch get the default branch
|
|
func (c *gitlabClient) getDefaultBranch(_ *context.Context, repo Repo) (string, error) {
|
|
if branch := os.Getenv("CI_DEFAULT_BRANCH"); branch != "" {
|
|
return branch, nil
|
|
}
|
|
if err := c.checkIsPrivateToken(); err != nil {
|
|
return "", fmt.Errorf("get default branch: %w", err)
|
|
}
|
|
projectID := repo.String()
|
|
p, res, err := c.client.Projects.GetProject(projectID, nil)
|
|
if err != nil {
|
|
log := log.WithField("projectID", projectID)
|
|
if res != nil {
|
|
log = log.WithField("statusCode", res.StatusCode)
|
|
}
|
|
log.WithError(err).Warn("error checking for default branch")
|
|
return "", err
|
|
}
|
|
return p.DefaultBranch, nil
|
|
}
|
|
|
|
// checkBranchExists checks if a branch exists
|
|
func (c *gitlabClient) checkBranchExists(_ *context.Context, repo Repo, branch string) (bool, error) {
|
|
projectID := repo.Name
|
|
if repo.Owner != "" {
|
|
projectID = repo.Owner + "/" + projectID
|
|
}
|
|
|
|
// Verify if branch exists
|
|
_, res, err := c.client.Branches.GetBranch(projectID, branch)
|
|
if err != nil && res.StatusCode != 404 {
|
|
log.WithError(err).
|
|
Error("error verify branch existence")
|
|
return false, err
|
|
}
|
|
|
|
return res.StatusCode != 404, nil
|
|
}
|
|
|
|
// CloseMilestone closes a given milestone.
|
|
func (c *gitlabClient) CloseMilestone(_ *context.Context, repo Repo, title string) error {
|
|
milestone, err := c.getMilestoneByTitle(repo, title)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if milestone == nil {
|
|
return ErrNoMilestoneFound{Title: title}
|
|
}
|
|
|
|
closeStateEvent := "close"
|
|
|
|
opts := &gitlab.UpdateMilestoneOptions{
|
|
Description: &milestone.Description,
|
|
DueDate: milestone.DueDate,
|
|
StartDate: milestone.StartDate,
|
|
StateEvent: &closeStateEvent,
|
|
Title: &milestone.Title,
|
|
}
|
|
|
|
_, _, err = c.client.Milestones.UpdateMilestone(
|
|
repo.String(),
|
|
milestone.ID,
|
|
opts,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// CreateFile gets a file in the repository at a given path
|
|
// and updates if it exists or creates it for later pipes in the pipeline.
|
|
func (c *gitlabClient) CreateFile(
|
|
ctx *context.Context,
|
|
commitAuthor config.CommitAuthor,
|
|
repo Repo,
|
|
content []byte, // the content of the formula.rb
|
|
fileName, // the path to the formula.rb
|
|
message string, // the commit msg
|
|
) error {
|
|
if err := c.checkIsPrivateToken(); err != nil {
|
|
return fmt.Errorf("create file: %w", err)
|
|
}
|
|
|
|
projectID := repo.Name
|
|
if repo.Owner != "" {
|
|
projectID = repo.Owner + "/" + projectID
|
|
}
|
|
|
|
log.
|
|
WithField("projectID", projectID).
|
|
Debug("project id")
|
|
|
|
var branch, defaultBranch string
|
|
var branchExists bool
|
|
var err error
|
|
// Use the branch if given one
|
|
if repo.Branch != "" {
|
|
branch = repo.Branch
|
|
branchExists, err = c.checkBranchExists(ctx, repo, branch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Retrieving default branch because we need it for `start_branch`
|
|
if !branchExists {
|
|
defaultBranch, err = c.getDefaultBranch(ctx, repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
log.
|
|
WithField("projectID", projectID).
|
|
WithField("branch", branch).
|
|
WithField("branchExists", branchExists).
|
|
Debug("using given branch")
|
|
} else {
|
|
// Try to get the default branch from the Git provider
|
|
branch, err = c.getDefaultBranch(ctx, repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defaultBranch = branch
|
|
branchExists = true
|
|
|
|
log.
|
|
WithField("projectID", projectID).
|
|
WithField("branch", branch).
|
|
Debug("no branch given, using default branch")
|
|
}
|
|
|
|
// If the branch doesn't exist, we need to check the default branch
|
|
// because that's what we use as `start_branch` later if the file needs
|
|
// to be created.
|
|
opts := &gitlab.GetFileOptions{Ref: &defaultBranch}
|
|
if branchExists {
|
|
opts.Ref = &branch
|
|
}
|
|
|
|
// Check if the file already exists
|
|
_, res, err := c.client.RepositoryFiles.GetFile(projectID, fileName, opts)
|
|
if err != nil && (res == nil || res.StatusCode != 404) {
|
|
log := log.
|
|
WithField("fileName", fileName).
|
|
WithField("branch", branch).
|
|
WithField("projectID", projectID)
|
|
if res != nil {
|
|
log = log.WithField("statusCode", res.StatusCode)
|
|
}
|
|
log.WithError(err).
|
|
Error("could not get file")
|
|
return err
|
|
}
|
|
|
|
log.
|
|
WithField("projectID", projectID).
|
|
WithField("branch", branch).
|
|
WithField("fileName", fileName).
|
|
Info("pushing file")
|
|
|
|
stringContents := string(content)
|
|
|
|
if res.StatusCode == 404 {
|
|
// Create a new file because it's not already there
|
|
log.
|
|
WithField("projectID", projectID).
|
|
WithField("branch", branch).
|
|
WithField("fileName", fileName).
|
|
Debug("file doesn't exist, creating it")
|
|
|
|
createOpts := &gitlab.CreateFileOptions{
|
|
AuthorName: &commitAuthor.Name,
|
|
AuthorEmail: &commitAuthor.Email,
|
|
Content: &stringContents,
|
|
Branch: &branch,
|
|
CommitMessage: &message,
|
|
}
|
|
|
|
// Branch not found, thus Gitlab requires a "start branch" to create the file
|
|
if !branchExists {
|
|
createOpts.StartBranch = &defaultBranch
|
|
}
|
|
|
|
fileInfo, res, err := c.client.RepositoryFiles.CreateFile(projectID, fileName, createOpts)
|
|
if err != nil {
|
|
log := log.
|
|
WithField("fileName", fileName).
|
|
WithField("branch", branch).
|
|
WithField("projectID", projectID)
|
|
if res != nil {
|
|
log = log.WithField("statusCode", res.StatusCode)
|
|
}
|
|
log.WithError(err).
|
|
Error("could not create file")
|
|
return err
|
|
}
|
|
|
|
log.
|
|
WithField("fileName", fileName).
|
|
WithField("branch", branch).
|
|
WithField("projectID", projectID).
|
|
WithField("filePath", fileInfo.FilePath).
|
|
Debug("created file")
|
|
return nil
|
|
}
|
|
|
|
// Update the existing file
|
|
log.
|
|
WithField("fileName", fileName).
|
|
WithField("branch", branch).
|
|
WithField("projectID", projectID).
|
|
Debug("file exists, updating it")
|
|
|
|
updateOpts := &gitlab.UpdateFileOptions{
|
|
AuthorName: &commitAuthor.Name,
|
|
AuthorEmail: &commitAuthor.Email,
|
|
Content: &stringContents,
|
|
Branch: &branch,
|
|
CommitMessage: &message,
|
|
}
|
|
|
|
// Branch not found, thus Gitlab requires a "start branch" to update the file
|
|
if !branchExists {
|
|
updateOpts.StartBranch = &defaultBranch
|
|
}
|
|
|
|
updateFileInfo, res, err := c.client.RepositoryFiles.UpdateFile(projectID, fileName, updateOpts)
|
|
if err != nil {
|
|
log := log.
|
|
WithField("fileName", fileName).
|
|
WithField("branch", branch).
|
|
WithField("projectID", projectID)
|
|
if res != nil {
|
|
log = log.WithField("statusCode", res.StatusCode)
|
|
}
|
|
log.WithError(err).
|
|
Error("error updating file")
|
|
return err
|
|
}
|
|
|
|
log := log.
|
|
WithField("fileName", fileName).
|
|
WithField("branch", branch).
|
|
WithField("projectID", projectID).
|
|
WithField("filePath", updateFileInfo.FilePath)
|
|
if res != nil {
|
|
log = log.WithField("statusCode", res.StatusCode)
|
|
}
|
|
log.Debug("updated file")
|
|
return nil
|
|
}
|
|
|
|
// CreateRelease creates a new release or updates it by keeping
|
|
// the release notes if it exists.
|
|
func (c *gitlabClient) CreateRelease(ctx *context.Context, body string) (releaseID string, err error) {
|
|
title, err := tmpl.New(ctx).Apply(ctx.Config.Release.NameTemplate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
projectID := gitlabName
|
|
if ctx.Config.Release.GitLab.Owner != "" {
|
|
projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID
|
|
}
|
|
log.
|
|
WithField("owner", ctx.Config.Release.GitLab.Owner).
|
|
WithField("name", gitlabName).
|
|
WithField("projectID", projectID).
|
|
Debug("projectID")
|
|
|
|
name := title
|
|
tagName := ctx.Git.CurrentTag
|
|
release, resp, err := c.client.Releases.GetRelease(projectID, tagName)
|
|
if err != nil && (resp == nil || (resp.StatusCode != 403 && resp.StatusCode != 404)) {
|
|
return "", err
|
|
}
|
|
|
|
if resp.StatusCode == 403 || resp.StatusCode == 404 {
|
|
log.WithError(err).Debug("get release")
|
|
|
|
description := body
|
|
ref := ctx.Git.Commit
|
|
gitURL := ctx.Git.URL
|
|
|
|
log.
|
|
WithField("name", name).
|
|
WithField("description", description).
|
|
WithField("ref", ref).
|
|
WithField("url", gitURL).
|
|
Debug("creating release")
|
|
release, _, err = c.client.Releases.CreateRelease(projectID, &gitlab.CreateReleaseOptions{
|
|
Name: &name,
|
|
Description: &description,
|
|
Ref: &ref,
|
|
TagName: &tagName,
|
|
})
|
|
if err != nil {
|
|
log.WithError(err).Debug("error creating release")
|
|
return "", err
|
|
}
|
|
log.WithField("name", release.Name).Info("release created")
|
|
} else {
|
|
desc := body
|
|
if release != nil {
|
|
desc = getReleaseNotes(release.Description, body, ctx.Config.Release.ReleaseNotesMode)
|
|
}
|
|
|
|
release, _, err = c.client.Releases.UpdateRelease(projectID, tagName, &gitlab.UpdateReleaseOptions{
|
|
Name: &name,
|
|
Description: &desc,
|
|
})
|
|
if err != nil {
|
|
log.WithError(err).Debug("error updating release")
|
|
return "", err
|
|
}
|
|
|
|
log.WithField("name", release.Name).Info("release updated")
|
|
}
|
|
|
|
return tagName, err // gitlab references a tag in a repo by its name
|
|
}
|
|
|
|
func (c *gitlabClient) PublishRelease(_ *context.Context, _ string /* releaseID */) (err error) {
|
|
// GitLab doesn't support draft releases. So a created release is already published.
|
|
return nil
|
|
}
|
|
|
|
func (c *gitlabClient) ReleaseURLTemplate(ctx *context.Context) (string, error) {
|
|
var urlTemplate string
|
|
gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if ctx.Config.Release.GitLab.Owner != "" {
|
|
urlTemplate = fmt.Sprintf(
|
|
"%s/%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
|
|
downloadURL,
|
|
ctx.Config.Release.GitLab.Owner,
|
|
gitlabName,
|
|
)
|
|
} else {
|
|
urlTemplate = fmt.Sprintf(
|
|
"%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
|
|
downloadURL,
|
|
gitlabName,
|
|
)
|
|
}
|
|
return urlTemplate, nil
|
|
}
|
|
|
|
// Upload uploads a file into a release repository.
|
|
func (c *gitlabClient) Upload(
|
|
ctx *context.Context,
|
|
releaseID string,
|
|
artifact *artifact.Artifact,
|
|
file *os.File,
|
|
) error {
|
|
// create new template and apply name field
|
|
gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
projectID := gitlabName
|
|
// check if owner is empty
|
|
if ctx.Config.Release.GitLab.Owner != "" {
|
|
projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID
|
|
}
|
|
|
|
var baseLinkURL string
|
|
var linkURL string
|
|
if ctx.Config.GitLabURLs.UsePackageRegistry || c.authType == gitlab.JobToken {
|
|
log.WithField("file", file.Name()).Debug("uploading file as generic package")
|
|
if _, _, err := c.client.GenericPackages.PublishPackageFile(
|
|
projectID,
|
|
ctx.Config.ProjectName,
|
|
ctx.Version,
|
|
artifact.Name,
|
|
file,
|
|
nil,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
baseLinkURL, err = c.client.GenericPackages.FormatPackageURL(
|
|
projectID,
|
|
ctx.Config.ProjectName,
|
|
ctx.Version,
|
|
artifact.Name,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
linkURL = c.client.BaseURL().String() + baseLinkURL
|
|
} else {
|
|
log.WithField("file", file.Name()).Debug("uploading file as attachment")
|
|
projectFile, _, err := c.client.Projects.UploadFile(
|
|
projectID,
|
|
file,
|
|
filepath.Base(file.Name()),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
baseLinkURL = projectFile.URL
|
|
gitlabBaseURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download)
|
|
if err != nil {
|
|
return fmt.Errorf("templating GitLab Download URL: %w", err)
|
|
}
|
|
|
|
// search for project details based on projectID
|
|
projectDetails, _, err := c.client.Projects.GetProject(projectID, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
linkURL = gitlabBaseURL + "/" + projectDetails.PathWithNamespace + baseLinkURL
|
|
}
|
|
|
|
log.WithField("file", file.Name()).
|
|
WithField("url", baseLinkURL).
|
|
Debug("uploaded file")
|
|
|
|
name := artifact.Name
|
|
filename := "/" + name
|
|
releaseLink, _, err := c.client.ReleaseLinks.CreateReleaseLink(
|
|
projectID,
|
|
releaseID,
|
|
&gitlab.CreateReleaseLinkOptions{
|
|
Name: &name,
|
|
URL: &linkURL,
|
|
FilePath: &filename,
|
|
})
|
|
if err != nil {
|
|
return RetriableError{err}
|
|
}
|
|
|
|
log.WithField("id", releaseLink.ID).
|
|
WithField("url", releaseLink.DirectAssetURL).
|
|
Debug("created release link")
|
|
|
|
// for checksums.txt the field is nil, so we initialize it
|
|
if artifact.Extra == nil {
|
|
artifact.Extra = make(map[string]interface{})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getMilestoneByTitle returns a milestone by title.
|
|
func (c *gitlabClient) getMilestoneByTitle(repo Repo, title string) (*gitlab.Milestone, error) {
|
|
opts := &gitlab.ListMilestonesOptions{
|
|
Title: &title,
|
|
}
|
|
|
|
for {
|
|
milestones, resp, err := c.client.Milestones.ListMilestones(repo.String(), opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, milestone := range milestones {
|
|
if milestone != nil && milestone.Title == title {
|
|
return milestone, nil
|
|
}
|
|
}
|
|
|
|
if resp.NextPage == 0 {
|
|
break
|
|
}
|
|
|
|
opts.Page = resp.NextPage
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// checkUseJobToken examines the context and given token, and determines if we
|
|
// should use NewJobClient vs NewClient
|
|
func checkUseJobToken(ctx context.Context, token string) bool {
|
|
// The CI_JOB_TOKEN env var is set automatically in all GitLab runners.
|
|
// If this comes back as empty, we aren't in a functional GitLab runner
|
|
ciToken := os.Getenv("CI_JOB_TOKEN")
|
|
if ciToken == "" {
|
|
return false
|
|
}
|
|
|
|
// We only want to use the JobToken client if we have specified
|
|
// UseJobToken. Older versions of GitLab don't work with this, so we
|
|
// want to be specific
|
|
if ctx.Config.GitLabURLs.UseJobToken {
|
|
// We may be creating a new client with a non-CI_JOB_TOKEN, for
|
|
// things like Homebrew publishing. We can't use the
|
|
// CI_JOB_TOKEN there
|
|
return token == ciToken
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *gitlabClient) OpenPullRequest(
|
|
ctx *context.Context,
|
|
base, head Repo,
|
|
title string,
|
|
draft bool,
|
|
) error {
|
|
if err := c.checkIsPrivateToken(); err != nil {
|
|
return fmt.Errorf("open merge request: %w", err)
|
|
}
|
|
var targetProjectID int
|
|
if base.Owner != "" {
|
|
fullProjectPath := fmt.Sprintf("%s/%s", base.Owner, base.Name)
|
|
|
|
p, res, err := c.client.Projects.GetProject(fullProjectPath, nil)
|
|
if err != nil {
|
|
log := log.WithField("project", fullProjectPath)
|
|
if res != nil {
|
|
log = log.WithField("statusCode", res.StatusCode)
|
|
}
|
|
log.WithError(err).Warn("error getting base project id")
|
|
return err
|
|
}
|
|
targetProjectID = p.ID
|
|
}
|
|
|
|
base.Owner = ordered.First(base.Owner, head.Owner)
|
|
base.Name = ordered.First(base.Name, head.Name)
|
|
|
|
if base.Branch == "" {
|
|
def, err := c.getDefaultBranch(ctx, base)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
base.Branch = def
|
|
}
|
|
|
|
if draft {
|
|
title = fmt.Sprintf("Draft: %s", title)
|
|
}
|
|
|
|
log.WithField("base", headString(base, Repo{})).
|
|
WithField("head", headString(base, head)).
|
|
WithField("draft", draft).
|
|
Info("opening pull request")
|
|
|
|
mrOptions := &gitlab.CreateMergeRequestOptions{
|
|
SourceBranch: &head.Branch,
|
|
TargetBranch: &base.Branch,
|
|
Title: &title,
|
|
}
|
|
|
|
if targetProjectID != 0 {
|
|
mrOptions.TargetProjectID = &targetProjectID
|
|
}
|
|
|
|
pr, _, err := c.client.MergeRequests.CreateMergeRequest(fmt.Sprintf("%s/%s", head.Owner, head.Name), mrOptions)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create pull request: %w", err)
|
|
}
|
|
log.WithField("url", pr.WebURL).Info("pull request created")
|
|
return nil
|
|
}
|