From 2fba5414e5b124ff8ab64afc3f8da6b673986a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sun, 8 Sep 2024 20:03:19 +0200 Subject: [PATCH 1/5] fix(gitlab): hardcoded project id (#51) The GitLab project ID was still hardcoded to my playground project on GitLab.com. This commit instead reads from the predefined GitLab CI/CD variable for the projects ID (`CI_PROJECT_ID`). --- internal/forge/gitlab/gitlab.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/internal/forge/gitlab/gitlab.go b/internal/forge/gitlab/gitlab.go index 8808773..dde618b 100644 --- a/internal/forge/gitlab/gitlab.go +++ b/internal/forge/gitlab/gitlab.go @@ -6,6 +6,7 @@ import ( "log/slog" "os" "slices" + "strconv" "strings" "github.com/blang/semver/v4" @@ -25,6 +26,7 @@ const ( PRStateMerged = "merged" PRStateEventClose = "close" EnvAPIToken = "GITLAB_TOKEN" // nolint:gosec // Not actually a hardcoded credential + EnvProjectID = "CI_PROJECT_ID" ) type GitLab struct { @@ -390,14 +392,17 @@ func gitlabMRToReleasePullRequest(pr *gitlab.MergeRequest) *releasepr.ReleasePul } } -func (g *Options) autodiscover() { +func (g *Options) autodiscover(log *slog.Logger) { // Read settings from GitLab-CI env vars if apiToken := os.Getenv(EnvAPIToken); apiToken != "" { g.APIToken = apiToken } - // TODO: Replace hardcode project-id with a better alternative - g.ProjectID = 60698565 + if projectID := os.Getenv(EnvProjectID); projectID != "" { + var err error + g.ProjectID, err = strconv.ParseInt(projectID, 10, 64) + log.Error("failed to parse environment variable as integer", "env.name", EnvProjectID, "env.value", projectID, "err", err) + } } type Options struct { @@ -405,14 +410,14 @@ type Options struct { Path string Repo string - ProjectID int + ProjectID int64 APIToken string } func New(log *slog.Logger, options *Options) (*GitLab, error) { log = log.With("forge", "gitlab") - options.autodiscover() + options.autodiscover(log) client, err := gitlab.NewClient(options.APIToken) if err != nil { From ee5c7aa142ee1fd0eb1bbd0aee53e88194b1a670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sun, 8 Sep 2024 21:01:46 +0200 Subject: [PATCH 2/5] fix(cli): command name in help output (#52) --- cmd/rp/cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/rp/cmd/root.go b/cmd/rp/cmd/root.go index dd4f9a7..4da80e5 100644 --- a/cmd/rp/cmd/root.go +++ b/cmd/rp/cmd/root.go @@ -11,7 +11,7 @@ import ( var logger *slog.Logger var rootCmd = &cobra.Command{ - Use: "releaser-pleaser", + Use: "rp", Short: "", Long: ``, Version: version(), From 634eac3b76a190ed240a3d8def464b3def628e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sun, 8 Sep 2024 21:05:18 +0200 Subject: [PATCH 3/5] fix(parser): invalid handling of empty lines (#53) GitLab generates commit messages of the pattern "scope: message\n" if no body is present. This throws up the conventional commits parser we use, and results in the error message "missing a blank line". We now `strings.TrimSpace()` the commit message to avoid this problem. --- .../conventionalcommits/conventionalcommits.go | 3 ++- .../conventionalcommits/conventionalcommits_test.go | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/commitparser/conventionalcommits/conventionalcommits.go b/internal/commitparser/conventionalcommits/conventionalcommits.go index b253354..d11970c 100644 --- a/internal/commitparser/conventionalcommits/conventionalcommits.go +++ b/internal/commitparser/conventionalcommits/conventionalcommits.go @@ -3,6 +3,7 @@ package conventionalcommits import ( "fmt" "log/slog" + "strings" "github.com/leodido/go-conventionalcommits" "github.com/leodido/go-conventionalcommits/parser" @@ -32,7 +33,7 @@ func (c *Parser) Analyze(commits []git.Commit) ([]commitparser.AnalyzedCommit, e analyzedCommits := make([]commitparser.AnalyzedCommit, 0, len(commits)) for _, commit := range commits { - msg, err := c.machine.Parse([]byte(commit.Message)) + msg, err := c.machine.Parse([]byte(strings.TrimSpace(commit.Message))) if err != nil { c.logger.Warn("failed to parse message of commit, skipping", "commit.hash", commit.Hash, "err", err) continue diff --git a/internal/commitparser/conventionalcommits/conventionalcommits_test.go b/internal/commitparser/conventionalcommits/conventionalcommits_test.go index bc969de..ddfbaf4 100644 --- a/internal/commitparser/conventionalcommits/conventionalcommits_test.go +++ b/internal/commitparser/conventionalcommits/conventionalcommits_test.go @@ -33,6 +33,19 @@ func TestAnalyzeCommits(t *testing.T) { expectedCommits: []commitparser.AnalyzedCommit{}, wantErr: assert.NoError, }, + { + // GitLab seems to create commits with pattern "scope: message\n" if no body is added. + // This has previously caused a parser error "missing a blank line". + // We added a workaround with `strings.TrimSpace()` and this test make sure that it does not break again. + name: "handles title with new line", + commits: []git.Commit{ + { + Message: "aksdjaklsdjka", + }, + }, + expectedCommits: []commitparser.AnalyzedCommit{}, + wantErr: assert.NoError, + }, { name: "drops unreleasable", commits: []git.Commit{ From ee83cec0494a3e756bdac5f769f6a910f66e2546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sun, 8 Sep 2024 21:07:03 +0200 Subject: [PATCH 4/5] fix(gitlab): use project path wherever possible (#54) Turns out that all we need is the path, and not the project id. The path is way more user friendly, and we can easily get it from a CI variable or combine it from the namespace & project name. --- cmd/rp/cmd/run.go | 3 +-- internal/forge/gitlab/gitlab.go | 47 +++++++++++++++------------------ 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/cmd/rp/cmd/run.go b/cmd/rp/cmd/run.go index 55ca419..8fe818f 100644 --- a/cmd/rp/cmd/run.go +++ b/cmd/rp/cmd/run.go @@ -62,8 +62,7 @@ func run(cmd *cobra.Command, _ []string) error { logger.DebugContext(ctx, "using forge GitLab") f, err = gitlab.New(logger, &gitlab.Options{ Options: forgeOptions, - Path: flagOwner, - Repo: flagRepo, + Path: fmt.Sprintf("%s/%s", flagOwner, flagRepo), }) if err != nil { logger.ErrorContext(ctx, "failed to create client", "err", err) diff --git a/internal/forge/gitlab/gitlab.go b/internal/forge/gitlab/gitlab.go index dde618b..1394898 100644 --- a/internal/forge/gitlab/gitlab.go +++ b/internal/forge/gitlab/gitlab.go @@ -6,7 +6,6 @@ import ( "log/slog" "os" "slices" - "strconv" "strings" "github.com/blang/semver/v4" @@ -26,7 +25,7 @@ const ( PRStateMerged = "merged" PRStateEventClose = "close" EnvAPIToken = "GITLAB_TOKEN" // nolint:gosec // Not actually a hardcoded credential - EnvProjectID = "CI_PROJECT_ID" + EnvProjectPath = "CI_PROJECT_PATH" ) type GitLab struct { @@ -37,19 +36,19 @@ type GitLab struct { } func (g *GitLab) RepoURL() string { - return fmt.Sprintf("https://gitlab.com/%s", g.options.Repository) + return fmt.Sprintf("https://gitlab.com/%s", g.options.Path) } func (g *GitLab) CloneURL() string { - return fmt.Sprintf("https://gitlab.com/%s/%s.git", g.options.Path, g.options.Repo) + return fmt.Sprintf("https://gitlab.com/%s.git", g.options.Path) } func (g *GitLab) ReleaseURL(version string) string { - return fmt.Sprintf("https://gitlab.com/%s/%s/-/releases/%s", g.options.Path, g.options.Repo, version) + return fmt.Sprintf("https://gitlab.com/%s/-/releases/%s", g.options.Path, version) } func (g *GitLab) PullRequestURL(id int) string { - return fmt.Sprintf("https://gitlab.com/%s/%s/-/merge_requests/%d", g.options.Path, g.options.Repo, id) + return fmt.Sprintf("https://gitlab.com/%s/-/merge_requests/%d", g.options.Path, id) } func (g *GitLab) GitAuth() transport.AuthMethod { @@ -64,7 +63,7 @@ func (g *GitLab) LatestTags(ctx context.Context) (git.Releases, error) { g.log.DebugContext(ctx, "listing all tags in gitlab repository") tags, err := all(func(listOptions gitlab.ListOptions) ([]*gitlab.Tag, *gitlab.Response, error) { - return g.client.Tags.ListTags(g.options.ProjectID, &gitlab.ListTagsOptions{ + return g.client.Tags.ListTags(g.options.Path, &gitlab.ListTagsOptions{ OrderBy: pointer.Pointer("updated"), ListOptions: listOptions, }, gitlab.WithContext(ctx)) @@ -122,7 +121,7 @@ func (g *GitLab) CommitsSince(ctx context.Context, tag *git.Tag) ([]git.Commit, log.Debug("listing commits", "ref.name", refName) gitLabCommits, err := all(func(listOptions gitlab.ListOptions) ([]*gitlab.Commit, *gitlab.Response, error) { - return g.client.Commits.ListCommits(g.options.ProjectID, &gitlab.ListCommitsOptions{ + return g.client.Commits.ListCommits(g.options.Path, &gitlab.ListCommitsOptions{ RefName: &refName, ListOptions: listOptions, }, gitlab.WithContext(ctx)) @@ -158,7 +157,7 @@ func (g *GitLab) prForCommit(ctx context.Context, commit git.Commit) (*git.PullR log.Debug("fetching pull requests associated with commit") associatedMRs, _, err := g.client.Commits.ListMergeRequestsByCommit( - g.options.ProjectID, commit.Hash, + g.options.Path, commit.Hash, gitlab.WithContext(ctx), ) if err != nil { @@ -184,7 +183,7 @@ func (g *GitLab) prForCommit(ctx context.Context, commit git.Commit) (*git.PullR func (g *GitLab) EnsureLabelsExist(ctx context.Context, labels []releasepr.Label) error { g.log.Debug("fetching labels on repo") glLabels, err := all(func(listOptions gitlab.ListOptions) ([]*gitlab.Label, *gitlab.Response, error) { - return g.client.Labels.ListLabels(g.options.ProjectID, &gitlab.ListLabelsOptions{ + return g.client.Labels.ListLabels(g.options.Path, &gitlab.ListLabelsOptions{ ListOptions: listOptions, }, gitlab.WithContext(ctx)) }) @@ -197,7 +196,7 @@ func (g *GitLab) EnsureLabelsExist(ctx context.Context, labels []releasepr.Label return glLabel.Name == label.Name }) { g.log.Info("creating label in repository", "label.name", label) - _, _, err := g.client.Labels.CreateLabel(g.options.ProjectID, &gitlab.CreateLabelOptions{ + _, _, err := g.client.Labels.CreateLabel(g.options.Path, &gitlab.CreateLabelOptions{ Name: pointer.Pointer(label.Name), Color: pointer.Pointer("#" + label.Color), Description: pointer.Pointer(label.Description), @@ -215,7 +214,7 @@ func (g *GitLab) EnsureLabelsExist(ctx context.Context, labels []releasepr.Label func (g *GitLab) PullRequestForBranch(ctx context.Context, branch string) (*releasepr.ReleasePullRequest, error) { // There should only be a single open merge request from branch into g.options.BaseBranch at any given moment. // We can skip pagination and just return the first result. - mrs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.options.ProjectID, &gitlab.ListProjectMergeRequestsOptions{ + mrs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.options.Path, &gitlab.ListProjectMergeRequestsOptions{ State: pointer.Pointer(PRStateOpen), SourceBranch: pointer.Pointer(branch), TargetBranch: pointer.Pointer(g.options.BaseBranch), @@ -241,7 +240,7 @@ func (g *GitLab) CreatePullRequest(ctx context.Context, pr *releasepr.ReleasePul labels = append(labels, label.Name) } - glMR, _, err := g.client.MergeRequests.CreateMergeRequest(g.options.ProjectID, &gitlab.CreateMergeRequestOptions{ + glMR, _, err := g.client.MergeRequests.CreateMergeRequest(g.options.Path, &gitlab.CreateMergeRequestOptions{ Title: &pr.Title, Description: &pr.Description, SourceBranch: &pr.Head, @@ -258,7 +257,7 @@ func (g *GitLab) CreatePullRequest(ctx context.Context, pr *releasepr.ReleasePul } func (g *GitLab) UpdatePullRequest(ctx context.Context, pr *releasepr.ReleasePullRequest) error { - _, _, err := g.client.MergeRequests.UpdateMergeRequest(g.options.ProjectID, pr.ID, &gitlab.UpdateMergeRequestOptions{ + _, _, err := g.client.MergeRequests.UpdateMergeRequest(g.options.Path, pr.ID, &gitlab.UpdateMergeRequestOptions{ Title: &pr.Title, Description: &pr.Description, }, gitlab.WithContext(ctx)) @@ -281,7 +280,7 @@ func (g *GitLab) SetPullRequestLabels(ctx context.Context, pr *releasepr.Release addLabels = append(addLabels, label.Name) } - _, _, err := g.client.MergeRequests.UpdateMergeRequest(g.options.ProjectID, pr.ID, &gitlab.UpdateMergeRequestOptions{ + _, _, err := g.client.MergeRequests.UpdateMergeRequest(g.options.Path, pr.ID, &gitlab.UpdateMergeRequestOptions{ RemoveLabels: &removeLabels, AddLabels: &addLabels, }, gitlab.WithContext(ctx)) @@ -294,7 +293,7 @@ func (g *GitLab) SetPullRequestLabels(ctx context.Context, pr *releasepr.Release } func (g *GitLab) ClosePullRequest(ctx context.Context, pr *releasepr.ReleasePullRequest) error { - _, _, err := g.client.MergeRequests.UpdateMergeRequest(g.options.ProjectID, pr.ID, &gitlab.UpdateMergeRequestOptions{ + _, _, err := g.client.MergeRequests.UpdateMergeRequest(g.options.Path, pr.ID, &gitlab.UpdateMergeRequestOptions{ StateEvent: pointer.Pointer(PRStateEventClose), }, gitlab.WithContext(ctx)) @@ -328,7 +327,7 @@ func (g *GitLab) PendingReleases(ctx context.Context, pendingLabel releasepr.Lab } func (g *GitLab) CreateRelease(ctx context.Context, commit git.Commit, title, changelog string, _, _ bool) error { - _, _, err := g.client.Releases.CreateRelease(g.options.ProjectID, &gitlab.CreateReleaseOptions{ + _, _, err := g.client.Releases.CreateRelease(g.options.Path, &gitlab.CreateReleaseOptions{ Name: &title, TagName: &title, Description: &changelog, @@ -392,32 +391,28 @@ func gitlabMRToReleasePullRequest(pr *gitlab.MergeRequest) *releasepr.ReleasePul } } -func (g *Options) autodiscover(log *slog.Logger) { +func (g *Options) autodiscover() { // Read settings from GitLab-CI env vars if apiToken := os.Getenv(EnvAPIToken); apiToken != "" { g.APIToken = apiToken } - if projectID := os.Getenv(EnvProjectID); projectID != "" { - var err error - g.ProjectID, err = strconv.ParseInt(projectID, 10, 64) - log.Error("failed to parse environment variable as integer", "env.name", EnvProjectID, "env.value", projectID, "err", err) + if projectPath := os.Getenv(EnvProjectPath); projectPath != "" { + g.Path = projectPath } } type Options struct { forge.Options - Path string - Repo string - ProjectID int64 + Path string APIToken string } func New(log *slog.Logger, options *Options) (*GitLab, error) { log = log.With("forge", "gitlab") - options.autodiscover(log) + options.autodiscover() client, err := gitlab.NewClient(options.APIToken) if err != nil { From f3297977993af979c64faf0b455910e3c72d1cbd Mon Sep 17 00:00:00 2001 From: releaser-pleaser <> Date: Sun, 8 Sep 2024 19:07:31 +0000 Subject: [PATCH 5/5] chore(main): release v0.4.0 --- CHANGELOG.md | 12 ++++++++++++ action.yml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb47b20..f7be68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [v0.4.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.4.0) + +### Features + +- add support for GitLab repositories (#49) + +### Bug Fixes + +- **parser**: continue on unparsable commit message (#48) +- **cli**: command name in help output (#52) +- **parser**: invalid handling of empty lines (#53) + ## [v0.3.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.3.0) ### :sparkles: Highlights diff --git a/action.yml b/action.yml index 8e3b8e1..f6f1415 100644 --- a/action.yml +++ b/action.yml @@ -21,7 +21,7 @@ inputs: outputs: {} runs: using: 'docker' - image: ghcr.io/apricote/releaser-pleaser:v0.3.0 # x-releaser-pleaser-version + image: ghcr.io/apricote/releaser-pleaser:v0.4.0 # x-releaser-pleaser-version args: - run - --forge=github