From 0a8e6641da1a2b7001705478b54a999184de4186 Mon Sep 17 00:00:00 2001 From: Markus Waldheim Date: Fri, 2 Oct 2026 10:04:34 +0200 Subject: [PATCH 1/2] feat: comment on pull requests in releases Post an idempotent release link on pull requests associated with commits in the release. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 4 +- cmd/plugin/main.go | 5 + internal/plugin/releases.go | 187 +++++++++++++++++++++++++++---- internal/plugin/releases_test.go | 140 +++++++++++++++++++++-- 4 files changed, 301 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 54efd11..ce42059 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ plugins: SEMREL_PLUGIN_DRAFT: "false" SEMREL_PLUGIN_PRERELEASE: "false" SEMREL_PLUGIN_ASSETS: "dist/*.tar.gz,dist/*.zip,build/myapp" + SEMREL_PLUGIN_COMMENT_ON_PRS: "true" ``` ## `SEMREL_PLUGIN_*` variables @@ -56,6 +57,7 @@ plugins: | `SEMREL_PLUGIN_DRAFT` | Optional | Create the release as a draft. | false | | `SEMREL_PLUGIN_PRERELEASE` | Optional | Mark the release as a prerelease. | false | | `SEMREL_PLUGIN_ASSETS` | Optional | Comma-separated file paths or glob patterns to upload as GitHub Release assets. | None | +| `SEMREL_PLUGIN_COMMENT_ON_PRS` | Optional | Add an idempotent comment to pull requests included in the release. The token needs pull-request read and issue-comment write permissions. | false | ## `SEMREL_*` release context used @@ -69,7 +71,7 @@ plugins: ## Example behavior -The plugin creates a GitHub release for the current tag, publishes the changelog as release notes, and can upload matching assets listed in `SEMREL_PLUGIN_ASSETS`. +The plugin creates a GitHub release for the current tag, publishes the changelog as release notes, and can upload matching assets listed in `SEMREL_PLUGIN_ASSETS`. When `SEMREL_PLUGIN_COMMENT_ON_PRS` is enabled, it finds pull requests associated with commits since the previous release and comments with a link to the new release. Re-running the same release does not duplicate comments. Examples: diff --git a/cmd/plugin/main.go b/cmd/plugin/main.go index 7a2838c..adda6d5 100644 --- a/cmd/plugin/main.go +++ b/cmd/plugin/main.go @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2026 The provider-github Authors +// This code has been partially generated by an AI. package main @@ -42,6 +43,10 @@ func run( } uploadAssets(ctx, cfg, release, stderr) + if err := plugin.CommentOnPullRequests(ctx, cfg, release); err != nil { + _, _ = fmt.Fprintln(stderr, "provider-github:", err) + return 1 + } _, _ = fmt.Fprintf(stdout, "provider-github: created %s for %s/%s (id=%d) %s\n", cfg.TagName, cfg.Owner, cfg.Repo, release.ID, release.URL) return 0 } diff --git a/internal/plugin/releases.go b/internal/plugin/releases.go index 3a2a0e7..3137495 100644 --- a/internal/plugin/releases.go +++ b/internal/plugin/releases.go @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2026 The provider-github Authors +// This code has been partially generated by an AI. package plugin @@ -7,6 +8,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "mime" @@ -22,17 +24,19 @@ const defaultBaseURL = "https://api.github.com" // Config contains the release request derived from the SemRel environment. type Config struct { - Token string - Owner string - Repo string - BaseURL string - TagName string - Name string - Body string - Draft bool - Prerelease bool - DryRun bool - Assets string + Token string + Owner string + Repo string + BaseURL string + TagName string + Name string + Body string + Draft bool + Prerelease bool + DryRun bool + Assets string + CommentOnPRs bool + CommitHashes []string } // Release is the minimal GitHub release response used by the subprocess entrypoint. @@ -85,17 +89,26 @@ func ConfigFromEnv(getenv func(string) string) (Config, error) { } cfg := Config{ - Token: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_TOKEN"), getenv("GITHUB_TOKEN"))), - Owner: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_OWNER"), repoOwner)), - Repo: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_REPO"), repoName)), - BaseURL: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_BASE_URL"), getenv("SEMREL_PLUGIN_API_URL"), defaultBaseURL)), - TagName: tagName, - Name: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_NAME"), tagName)), - Body: getenv("SEMREL_CHANGELOG"), - Draft: parseBoolValue(getenv("SEMREL_PLUGIN_DRAFT")), - Prerelease: prerelease, - DryRun: parseBoolValue(getenv("SEMREL_DRY_RUN")), - Assets: getenv("SEMREL_PLUGIN_ASSETS"), + Token: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_TOKEN"), getenv("GITHUB_TOKEN"))), + Owner: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_OWNER"), repoOwner)), + Repo: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_REPO"), repoName)), + BaseURL: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_BASE_URL"), getenv("SEMREL_PLUGIN_API_URL"), defaultBaseURL)), + TagName: tagName, + Name: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_NAME"), tagName)), + Body: getenv("SEMREL_CHANGELOG"), + Draft: parseBoolValue(getenv("SEMREL_PLUGIN_DRAFT")), + Prerelease: prerelease, + DryRun: parseBoolValue(getenv("SEMREL_DRY_RUN")), + Assets: getenv("SEMREL_PLUGIN_ASSETS"), + CommentOnPRs: parseBoolValue(getenv("SEMREL_PLUGIN_COMMENT_ON_PRS")), + } + if cfg.CommentOnPRs { + hashes := strings.TrimSpace(getenv("SEMREL_COMMIT_HASHES")) + if hashes != "" { + if err := json.Unmarshal([]byte(hashes), &cfg.CommitHashes); err != nil { + return Config{}, fmt.Errorf("parse SEMREL_COMMIT_HASHES: %w", err) + } + } } if err := validateConfig(cfg); err != nil { @@ -109,6 +122,12 @@ func CreateRelease(ctx context.Context, cfg Config) (*Release, error) { return New(nil).CreateRelease(ctx, cfg) } +// CommentOnPullRequests posts a release comment on every pull request associated +// with a commit in the release range. +func CommentOnPullRequests(ctx context.Context, cfg Config, release *Release) error { + return defaultHTTPClient().CommentOnPullRequests(ctx, cfg, release) +} + // UploadReleaseAssets uploads any configured assets for the provided release. func UploadReleaseAssets(ctx context.Context, cfg Config, release *Release, stderr io.Writer) { defaultHTTPClient().UploadReleaseAssets(ctx, cfg, release, stderr) @@ -189,6 +208,130 @@ func (c *client) CreateRelease(ctx context.Context, cfg Config) (*Release, error return &Release{ID: parsed.ID, URL: parsed.HTMLURL, UploadURL: parsed.UploadURL}, nil } +func (c *client) CommentOnPullRequests(ctx context.Context, cfg Config, release *Release) error { + if !cfg.CommentOnPRs || cfg.DryRun || len(cfg.CommitHashes) == 0 { + return nil + } + if release == nil || strings.TrimSpace(release.URL) == "" { + return errors.New("release URL is required to comment on pull requests") + } + baseURL, err := url.Parse(strings.TrimRight(cfg.BaseURL, "/") + "/") + if err != nil { + return fmt.Errorf("parse github base url: %w", err) + } + + pullRequests := make(map[int64]struct{}) + for _, hash := range cfg.CommitHashes { + hash = strings.TrimSpace(hash) + if hash == "" { + continue + } + endpoint, err := baseURL.Parse(fmt.Sprintf("repos/%s/%s/commits/%s/pulls", + url.PathEscape(cfg.Owner), url.PathEscape(cfg.Repo), url.PathEscape(hash))) + if err != nil { + return fmt.Errorf("build pull requests endpoint for commit %q: %w", hash, err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return fmt.Errorf("build pull requests request for commit %q: %w", hash, err) + } + setGitHubHeaders(req, cfg.Token, "application/json", baseURL) + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("list pull requests for commit %q: %w", hash, err) + } + var associated []struct { + Number int64 `json:"number"` + } + decodeErr := json.NewDecoder(resp.Body).Decode(&associated) + closeErr := resp.Body.Close() + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return fmt.Errorf("list pull requests for commit %q failed: %s", hash, resp.Status) + } + if decodeErr != nil { + return fmt.Errorf("decode pull requests for commit %q: %w", hash, decodeErr) + } + if closeErr != nil { + return fmt.Errorf("close pull requests response for commit %q: %w", hash, closeErr) + } + for _, pullRequest := range associated { + if pullRequest.Number > 0 { + pullRequests[pullRequest.Number] = struct{}{} + } + } + } + + marker := "" + comment := fmt.Sprintf("%s\nThis pull request has been included in release [%s](%s).", marker, cfg.TagName, release.URL) + for number := range pullRequests { + if err := c.commentOnPullRequest(ctx, cfg, baseURL, number, marker, comment); err != nil { + return err + } + } + return nil +} + +func (c *client) commentOnPullRequest(ctx context.Context, cfg Config, baseURL *url.URL, number int64, marker, comment string) error { + commentsURL, err := baseURL.Parse(fmt.Sprintf("repos/%s/%s/issues/%d/comments", + url.PathEscape(cfg.Owner), url.PathEscape(cfg.Repo), number)) + if err != nil { + return fmt.Errorf("build comments endpoint for pull request #%d: %w", number, err) + } + listReq, err := http.NewRequestWithContext(ctx, http.MethodGet, commentsURL.String()+"?per_page=100", nil) + if err != nil { + return fmt.Errorf("build comments request for pull request #%d: %w", number, err) + } + setGitHubHeaders(listReq, cfg.Token, "application/json", baseURL) + resp, err := c.httpClient.Do(listReq) + if err != nil { + return fmt.Errorf("list comments for pull request #%d: %w", number, err) + } + var commentsList []struct { + Body string `json:"body"` + } + decodeErr := json.NewDecoder(resp.Body).Decode(&commentsList) + closeErr := resp.Body.Close() + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return fmt.Errorf("list comments for pull request #%d failed: %s", number, resp.Status) + } + if decodeErr != nil { + return fmt.Errorf("decode comments for pull request #%d: %w", number, decodeErr) + } + if closeErr != nil { + return fmt.Errorf("close comments response for pull request #%d: %w", number, closeErr) + } + for _, existing := range commentsList { + if strings.Contains(existing.Body, marker) { + return nil + } + } + + payload, err := json.Marshal(struct { + Body string `json:"body"` + }{Body: comment}) + if err != nil { + return fmt.Errorf("marshal release comment for pull request #%d: %w", number, err) + } + postReq, err := http.NewRequestWithContext(ctx, http.MethodPost, commentsURL.String(), bytes.NewReader(payload)) + if err != nil { + return fmt.Errorf("build release comment request for pull request #%d: %w", number, err) + } + setGitHubHeaders(postReq, cfg.Token, "application/json", baseURL) + resp, err = c.httpClient.Do(postReq) + if err != nil { + return fmt.Errorf("post release comment to pull request #%d: %w", number, err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + message := readMessage(resp.Body) + if message == "" { + message = resp.Status + } + return fmt.Errorf("post release comment to pull request #%d failed: %s", number, message) + } + return nil +} + func (c *client) UploadReleaseAssets(ctx context.Context, cfg Config, release *Release, stderr io.Writer) { if cfg.DryRun || strings.TrimSpace(cfg.Assets) == "" || release == nil { return diff --git a/internal/plugin/releases_test.go b/internal/plugin/releases_test.go index e2f8611..386a54e 100644 --- a/internal/plugin/releases_test.go +++ b/internal/plugin/releases_test.go @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2026 The provider-github Authors +// This code has been partially generated by an AI. package plugin @@ -22,18 +23,20 @@ func TestConfigFromEnvUsesPluginOverrides(t *testing.T) { t.Parallel() cfg, err := ConfigFromEnv(envMap(map[string]string{ - "SEMREL_PLUGIN_TOKEN": "plugin-token", - "GITHUB_TOKEN": "fallback-token", - "SEMREL_PLUGIN_OWNER": "plugin-owner", - "SEMREL_PLUGIN_REPO": "plugin-repo", - "GITHUB_REPOSITORY": "fallback-owner/fallback-repo", - "SEMREL_TAG_NAME": "v1.2.3", - "SEMREL_PLUGIN_NAME": "Release 1.2.3", - "SEMREL_CHANGELOG": "notes", - "SEMREL_PLUGIN_DRAFT": "true", - "SEMREL_PLUGIN_PRERELEASE": "false", - "SEMREL_PLUGIN_BASE_URL": "https://ghe.example/api/v3", - "SEMREL_PLUGIN_ASSETS": "dist/*.tar.gz, dist/*.zip", + "SEMREL_PLUGIN_TOKEN": "plugin-token", + "GITHUB_TOKEN": "fallback-token", + "SEMREL_PLUGIN_OWNER": "plugin-owner", + "SEMREL_PLUGIN_REPO": "plugin-repo", + "GITHUB_REPOSITORY": "fallback-owner/fallback-repo", + "SEMREL_TAG_NAME": "v1.2.3", + "SEMREL_PLUGIN_NAME": "Release 1.2.3", + "SEMREL_CHANGELOG": "notes", + "SEMREL_PLUGIN_DRAFT": "true", + "SEMREL_PLUGIN_PRERELEASE": "false", + "SEMREL_PLUGIN_BASE_URL": "https://ghe.example/api/v3", + "SEMREL_PLUGIN_ASSETS": "dist/*.tar.gz, dist/*.zip", + "SEMREL_PLUGIN_COMMENT_ON_PRS": "true", + "SEMREL_COMMIT_HASHES": `["abc123","def456"]`, })) if err != nil { t.Fatalf("ConfigFromEnv() error = %v", err) @@ -48,6 +51,9 @@ func TestConfigFromEnvUsesPluginOverrides(t *testing.T) { if cfg.Assets != "dist/*.tar.gz, dist/*.zip" { t.Fatalf("unexpected assets: %+v", cfg) } + if !cfg.CommentOnPRs || strings.Join(cfg.CommitHashes, ",") != "abc123,def456" { + t.Fatalf("unexpected pull request comment config: %+v", cfg) + } if !cfg.Draft || cfg.Prerelease || cfg.DryRun { t.Fatalf("unexpected flags: %+v", cfg) } @@ -78,6 +84,23 @@ func TestConfigFromEnvParsesFallbacks(t *testing.T) { } } +func TestConfigFromEnvAllowsCommentingWithoutCommitHashes(t *testing.T) { + t.Parallel() + + cfg, err := ConfigFromEnv(envMap(map[string]string{ + "GITHUB_TOKEN": "token", + "GITHUB_REPOSITORY": "owner/repo", + "SEMREL_TAG_NAME": "v1.2.3", + "SEMREL_PLUGIN_COMMENT_ON_PRS": "true", + })) + if err != nil { + t.Fatalf("ConfigFromEnv() error = %v", err) + } + if !cfg.CommentOnPRs || len(cfg.CommitHashes) != 0 { + t.Fatalf("unexpected pull request comment config: %+v", cfg) + } +} + func TestConfigFromEnvValidationErrors(t *testing.T) { t.Parallel() @@ -237,6 +260,99 @@ func TestCreateReleaseErrors(t *testing.T) { }) } +func TestCommentOnPullRequestsDeduplicatesAndPostsReleaseComment(t *testing.T) { + t.Parallel() + + var commentPosts atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v3/repos/owner/repo/commits/abc123/pulls", "/api/v3/repos/owner/repo/commits/def456/pulls": + if r.Method != http.MethodGet { + t.Fatalf("method = %s, want GET", r.Method) + } + _, _ = w.Write([]byte(`[{"number":7}]`)) + case "/api/v3/repos/owner/repo/issues/7/comments": + switch r.Method { + case http.MethodGet: + _, _ = w.Write([]byte(`[]`)) + case http.MethodPost: + var payload struct { + Body string `json:"body"` + } + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + t.Fatalf("decode comment: %v", err) + } + if !strings.Contains(payload.Body, "") || + !strings.Contains(payload.Body, "[v1.2.3](https://github.com/owner/repo/releases/tag/v1.2.3)") { + t.Fatalf("unexpected comment body: %q", payload.Body) + } + commentPosts.Add(1) + w.WriteHeader(http.StatusCreated) + default: + t.Fatalf("unexpected method %s", r.Method) + } + default: + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + })) + defer server.Close() + + cli := New(server.Client()).(*client) + err := cli.CommentOnPullRequests(context.Background(), Config{ + Token: "token", + Owner: "owner", + Repo: "repo", + BaseURL: server.URL + "/api/v3", + TagName: "v1.2.3", + CommentOnPRs: true, + CommitHashes: []string{"abc123", "def456"}, + }, &Release{URL: "https://github.com/owner/repo/releases/tag/v1.2.3"}) + if err != nil { + t.Fatalf("CommentOnPullRequests() error = %v", err) + } + if got := commentPosts.Load(); got != 1 { + t.Fatalf("comment POST count = %d, want 1 for a PR associated with two commits", got) + } +} + +func TestCommentOnPullRequestsSkipsExistingReleaseComment(t *testing.T) { + t.Parallel() + + var commentPosts atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/repos/owner/repo/commits/abc123/pulls" { + _, _ = w.Write([]byte(`[{"number":7}]`)) + return + } + if r.URL.Path == "/repos/owner/repo/issues/7/comments" && r.Method == http.MethodGet { + _, _ = w.Write([]byte(`[{"body":" already commented"}]`)) + return + } + if r.Method == http.MethodPost { + commentPosts.Add(1) + } + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + cli := New(server.Client()).(*client) + err := cli.CommentOnPullRequests(context.Background(), Config{ + Token: "token", + Owner: "owner", + Repo: "repo", + BaseURL: server.URL, + TagName: "v1.2.3", + CommentOnPRs: true, + CommitHashes: []string{"abc123"}, + }, &Release{URL: "https://github.com/owner/repo/releases/tag/v1.2.3"}) + if err != nil { + t.Fatalf("CommentOnPullRequests() error = %v", err) + } + if got := commentPosts.Load(); got != 0 { + t.Fatalf("comment POST count = %d, want 0 for existing release comment", got) + } +} + func TestUploadReleaseAssetsNoAssetsConfigured(t *testing.T) { t.Parallel() From bd40cc0504b705a15126e12afda69e574476b0e9 Mon Sep 17 00:00:00 2001 From: Markus Waldheim Date: Fri, 2 Oct 2026 10:14:40 +0200 Subject: [PATCH 2/2] feat: enable release comments by default Make pull request release comments the default behavior while allowing projects to disable them explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 4 ++-- internal/plugin/releases.go | 10 +++++++++- internal/plugin/releases_test.go | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ce42059..ba375d1 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ plugins: | `SEMREL_PLUGIN_DRAFT` | Optional | Create the release as a draft. | false | | `SEMREL_PLUGIN_PRERELEASE` | Optional | Mark the release as a prerelease. | false | | `SEMREL_PLUGIN_ASSETS` | Optional | Comma-separated file paths or glob patterns to upload as GitHub Release assets. | None | -| `SEMREL_PLUGIN_COMMENT_ON_PRS` | Optional | Add an idempotent comment to pull requests included in the release. The token needs pull-request read and issue-comment write permissions. | false | +| `SEMREL_PLUGIN_COMMENT_ON_PRS` | Optional | Add an idempotent comment to pull requests included in the release. Defaults to true; set to `false` to disable. The token needs pull-request read and issue-comment write permissions. | true | ## `SEMREL_*` release context used @@ -71,7 +71,7 @@ plugins: ## Example behavior -The plugin creates a GitHub release for the current tag, publishes the changelog as release notes, and can upload matching assets listed in `SEMREL_PLUGIN_ASSETS`. When `SEMREL_PLUGIN_COMMENT_ON_PRS` is enabled, it finds pull requests associated with commits since the previous release and comments with a link to the new release. Re-running the same release does not duplicate comments. +The plugin creates a GitHub release for the current tag, publishes the changelog as release notes, and can upload matching assets listed in `SEMREL_PLUGIN_ASSETS`. By default it finds pull requests associated with commits since the previous release and comments with a link to the new release. Set `SEMREL_PLUGIN_COMMENT_ON_PRS` to `false` to disable comments. Re-running the same release does not duplicate comments. Examples: diff --git a/internal/plugin/releases.go b/internal/plugin/releases.go index 3137495..7ad7ea5 100644 --- a/internal/plugin/releases.go +++ b/internal/plugin/releases.go @@ -87,6 +87,14 @@ func ConfigFromEnv(getenv func(string) string) (Config, error) { if !hasPrerelease { prerelease = strings.Contains(tagName, "-") } + commentOnPRs := true + if raw := strings.TrimSpace(getenv("SEMREL_PLUGIN_COMMENT_ON_PRS")); raw != "" { + value, ok := parseBool(raw) + if !ok { + return Config{}, fmt.Errorf("SEMREL_PLUGIN_COMMENT_ON_PRS must be a boolean") + } + commentOnPRs = value + } cfg := Config{ Token: strings.TrimSpace(coalesce(getenv("SEMREL_PLUGIN_TOKEN"), getenv("GITHUB_TOKEN"))), @@ -100,7 +108,7 @@ func ConfigFromEnv(getenv func(string) string) (Config, error) { Prerelease: prerelease, DryRun: parseBoolValue(getenv("SEMREL_DRY_RUN")), Assets: getenv("SEMREL_PLUGIN_ASSETS"), - CommentOnPRs: parseBoolValue(getenv("SEMREL_PLUGIN_COMMENT_ON_PRS")), + CommentOnPRs: commentOnPRs, } if cfg.CommentOnPRs { hashes := strings.TrimSpace(getenv("SEMREL_COMMIT_HASHES")) diff --git a/internal/plugin/releases_test.go b/internal/plugin/releases_test.go index 386a54e..786c620 100644 --- a/internal/plugin/releases_test.go +++ b/internal/plugin/releases_test.go @@ -82,6 +82,26 @@ func TestConfigFromEnvParsesFallbacks(t *testing.T) { if !cfg.Prerelease || !cfg.DryRun { t.Fatalf("expected prerelease dry-run config, got %+v", cfg) } + if !cfg.CommentOnPRs { + t.Fatal("expected pull request comments to be enabled by default") + } +} + +func TestConfigFromEnvCanDisablePullRequestComments(t *testing.T) { + t.Parallel() + + cfg, err := ConfigFromEnv(envMap(map[string]string{ + "GITHUB_TOKEN": "token", + "GITHUB_REPOSITORY": "owner/repo", + "SEMREL_TAG_NAME": "v1.2.3", + "SEMREL_PLUGIN_COMMENT_ON_PRS": "false", + })) + if err != nil { + t.Fatalf("ConfigFromEnv() error = %v", err) + } + if cfg.CommentOnPRs { + t.Fatal("expected pull request comments to be disabled") + } } func TestConfigFromEnvAllowsCommentingWithoutCommitHashes(t *testing.T) {