feat: Enable hide-prev-plan-comments Feature for BitBucket Cloud (#4495)

Signed-off-by: Simon Heather <32168619+X-Guardian@users.noreply.github.com>
This commit is contained in:
Edward
2025-01-25 14:33:29 +01:00
committed by GitHub
parent 5e4a35b596
commit c864248ec2
7 changed files with 585 additions and 5 deletions

View File

@@ -132,7 +132,7 @@ A new permission for `Actions` has been added, which is required for checking if
* Create an App Password by following [BitBucket Cloud: Create an app password](https://support.atlassian.com/bitbucket-cloud/docs/create-an-app-password/)
* Label the password "atlantis"
* Select **Pull requests**: **Read** and **Write** so that Atlantis can read your pull requests and write comments to them
* Select **Pull requests**: **Read** and **Write** so that Atlantis can read your pull requests and write comments to them. If you want to enable the [hide-prev-plan-comments](./server-configuration#hide-prev-plan-comments) feature and thus delete old comments, please add **Account**: **Read** as well.
* Record the access token
### Bitbucket Server (aka Stash)

View File

@@ -863,9 +863,14 @@ based on the organization or user that triggered the webhook.
```
Hide previous plan comments to declutter PRs. This is only supported in
GitHub and GitLab currently. This is not enabled by default. When using Github App, you need to set `--gh-app-slug` to enable this feature.
For github, ensure the `--gh-user` is set appropriately or comments will not be hidden.
GitHub and GitLab and Bitbucket currently and is not enabled by default.
For Bitbucket, the comments are deleted rather than hidden as Bitbucket does not support hiding comments.
For GitHub, ensure the `--gh-user` is set appropriately or comments will not be hidden.
When using the GitHub App, you need to set `--gh-app-slug` to enable this feature.
### `--hide-unchanged-plan-comments`
```bash

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"unicode/utf8"
validator "github.com/go-playground/validator/v10"
@@ -39,6 +40,8 @@ func NewClient(httpClient *http.Client, username string, password string, atlant
}
}
var MY_UUID = ""
// GetModifiedFiles returns the names of files that were modified in the merge request
// relative to the repo root, e.g. parent/child/file.txt.
func (b *Client) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {
@@ -107,10 +110,92 @@ func (b *Client) ReactToComment(_ logging.SimpleLogging, _ models.Repo, _ int, _
return nil
}
func (b *Client) HidePrevCommandComments(_ logging.SimpleLogging, _ models.Repo, _ int, _ string, _ string) error {
func (b *Client) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, _ string) error {
// there is no way to hide comment, so delete them instead
me, err := b.GetMyUUID()
if err != nil {
return errors.Wrapf(err, "Cannot get my uuid! Please check required scope of the auth token!")
}
logger.Debug("My bitbucket user UUID is: %s", me)
comments, err := b.GetPullRequestComments(repo, pullNum)
if err != nil {
return err
}
for _, c := range comments {
logger.Debug("Comment is %v", c.Content.Raw)
if strings.EqualFold(*c.User.UUID, me) {
// do the same crude filtering as github client does
body := strings.Split(c.Content.Raw, "\n")
logger.Debug("Body is %s", body)
if len(body) == 0 {
continue
}
firstLine := strings.ToLower(body[0])
if strings.Contains(firstLine, strings.ToLower(command)) {
// we found our old comment that references that command
logger.Debug("Deleting comment with id %s", *c.ID)
err = b.DeletePullRequestComment(repo, pullNum, *c.ID)
if err != nil {
return err
}
}
}
}
return nil
}
func (b *Client) DeletePullRequestComment(repo models.Repo, pullNum int, commentId int) error {
path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d/comments/%d", b.BaseURL, repo.FullName, pullNum, commentId)
_, err := b.makeRequest("DELETE", path, nil)
if err != nil {
return err
}
return nil
}
func (b *Client) GetPullRequestComments(repo models.Repo, pullNum int) (comments []PullRequestComment, err error) {
path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d/comments", b.BaseURL, repo.FullName, pullNum)
res, err := b.makeRequest("GET", path, nil)
if err != nil {
return comments, err
}
var pulls PullRequestComments
if err := json.Unmarshal(res, &pulls); err != nil {
return comments, errors.Wrapf(err, "Could not parse response %q", string(res))
}
return pulls.Values, nil
}
func (b *Client) GetMyUUID() (uuid string, err error) {
if MY_UUID == "" {
path := fmt.Sprintf("%s/2.0/user", b.BaseURL)
resp, err := b.makeRequest("GET", path, nil)
if err != nil {
return uuid, err
}
var user User
if err := json.Unmarshal(resp, &user); err != nil {
return uuid, errors.Wrapf(err, "Could not parse response %q", string(resp))
}
if err := validator.New().Struct(user); err != nil {
return uuid, errors.Wrapf(err, "API response %q was missing a field", string(resp))
}
uuid = *user.UUID
MY_UUID = uuid
return uuid, nil
} else {
return MY_UUID, nil
}
}
// PullIsApproved returns true if the merge request was approved.
func (b *Client) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (approvalStatus models.ApprovalStatus, err error) {
path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d", b.BaseURL, repo.FullName, pull.Num)
@@ -254,7 +339,7 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b
defer resp.Body.Close() // nolint: errcheck
requestStr := fmt.Sprintf("%s %s", method, path)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
respBody, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("making request %q unexpected status code: %d, body: %s", requestStr, resp.StatusCode, string(respBody))
}

View File

@@ -6,6 +6,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/runatlantis/atlantis/server/events/models"
@@ -367,3 +368,159 @@ func TestClient_MarkdownPullLink(t *testing.T) {
exp := "#1"
Equals(t, exp, s)
}
func TestClient_GetMyUUID(t *testing.T) {
json, err := os.ReadFile(filepath.Join("testdata", "user.json"))
Ok(t, err)
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/2.0/user":
w.Write(json) // nolint: errcheck
return
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
return
}
}))
defer testServer.Close()
client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io")
client.BaseURL = testServer.URL
v, _ := client.GetMyUUID()
Equals(t, v, "{00000000-0000-0000-0000-000000000001}")
}
func TestClient_GetComment(t *testing.T) {
json, err := os.ReadFile(filepath.Join("testdata", "comments.json"))
Ok(t, err)
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments":
w.Write(json) // nolint: errcheck
return
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
return
}
}))
defer testServer.Close()
client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io")
client.BaseURL = testServer.URL
v, _ := client.GetPullRequestComments(
models.Repo{
FullName: "myorg/myrepo",
Owner: "owner",
Name: "myrepo",
CloneURL: "",
SanitizedCloneURL: "",
VCSHost: models.VCSHost{
Type: models.BitbucketCloud,
Hostname: "bitbucket.org",
},
}, 5)
Equals(t, len(v), 5)
exp := "Plan"
Assert(t, strings.Contains(v[1].Content.Raw, exp), "Comment should contain word \"%s\", has \"%s\"", exp, v[1].Content.Raw)
}
func TestClient_DeleteComment(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments/1":
if r.Method == "DELETE" {
w.WriteHeader(http.StatusNoContent)
}
return
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
return
}
}))
defer testServer.Close()
client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io")
client.BaseURL = testServer.URL
err := client.DeletePullRequestComment(
models.Repo{
FullName: "myorg/myrepo",
Owner: "owner",
Name: "myrepo",
CloneURL: "",
SanitizedCloneURL: "",
VCSHost: models.VCSHost{
Type: models.BitbucketCloud,
Hostname: "bitbucket.org",
},
}, 5, 1)
Ok(t, err)
}
func TestClient_HidePRComments(t *testing.T) {
logger := logging.NewNoopLogger(t)
comments, err := os.ReadFile(filepath.Join("testdata", "comments.json"))
Ok(t, err)
json, err := os.ReadFile(filepath.Join("testdata", "user.json"))
Ok(t, err)
called := 0
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
// we have two comments in the test file
// The code is going to delete them all and then create a new one
case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments/498931882":
if r.Method == "DELETE" {
w.WriteHeader(http.StatusNoContent)
}
w.Write([]byte("")) // nolint: errcheck
called += 1
return
// This is the second one
case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments/498931784":
if r.Method == "DELETE" {
http.Error(w, "", http.StatusNoContent)
}
w.Write([]byte("")) // nolint: errcheck
called += 1
return
case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments/49893111":
Assert(t, r.Method != "DELETE", "Shouldn't delete this one")
return
case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments":
w.Write(comments) // nolint: errcheck
return
case "/2.0/user":
w.Write(json) // nolint: errcheck
return
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
return
}
}))
defer testServer.Close()
client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io")
client.BaseURL = testServer.URL
err = client.HidePrevCommandComments(logger,
models.Repo{
FullName: "myorg/myrepo",
Owner: "owner",
Name: "myrepo",
CloneURL: "",
SanitizedCloneURL: "",
VCSHost: models.VCSHost{
Type: models.BitbucketCloud,
Hostname: "bitbucket.org",
},
}, 5, "plan", "")
Ok(t, err)
Equals(t, 2, called)
}

View File

@@ -45,6 +45,34 @@ type Repository struct {
FullName *string `json:"full_name,omitempty" validate:"required"`
Links Links `json:"links,omitempty" validate:"required"`
}
type User struct {
Type *string `json:"type,omitempty" validate:"required"`
CreateOn *string `json:"created_on" validate:"required"`
DisplayName *string `json:"display_name" validate:"required"`
Username *string `json:"username" validate:"required"`
UUID *string `json:"uuid" validate:"required"`
}
type UserInComment struct {
Type *string `json:"type,omitempty" validate:"required"`
Nickname *string `json:"nickname" validate:"required"`
DisplayName *string `json:"display_name" validate:"required"`
UUID *string `json:"uuid" validate:"required"`
}
type PullRequestComment struct {
ID *int `json:"id,omitempty" validate:"required"`
User *UserInComment `json:"user" validate:"required"`
Content *struct {
Raw string `json:"raw"`
} `json:"content" validate:"required"`
}
type PullRequestComments struct {
Values []PullRequestComment `json:"values,omitempty"`
}
type PullRequest struct {
ID *int `json:"id,omitempty" validate:"required"`
Source *BranchMeta `json:"source,omitempty" validate:"required"`

View File

@@ -0,0 +1,272 @@
{
"values": [
{
"id": 498931784,
"created_on": "2024-05-07T12:21:45.858898+00:00",
"updated_on": "2024-05-07T12:21:45.859011+00:00",
"content": {
"type": "rendered",
"raw": "atlantis plan",
"markup": "markdown",
"html": "<p>atlantis plan</p>"
},
"user": {
"display_name": "Ragne",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/users/%7B00000000-0000-0000-0000-000000000001%7D"
},
"avatar": {
"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/EM-3.png"
},
"html": {
"href": "https://bitbucket.org/%7B00000000-0000-0000-0000-000000000001%7D/"
}
},
"type": "user",
"uuid": "{00000000-0000-0000-0000-000000000001}",
"account_id": "000000:00000000-0000-0000-0000-000000000001",
"nickname": "Ragne"
},
"deleted": false,
"pending": false,
"type": "pullrequest_comment",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931784"
},
"html": {
"href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931784"
}
},
"pullrequest": {
"type": "pullrequest",
"id": 5,
"title": "for test",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5"
},
"html": {
"href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5"
}
}
}
},
{
"id": 498931802,
"created_on": "2024-05-07T12:21:48.737851+00:00",
"updated_on": "2024-05-07T12:21:48.737927+00:00",
"content": {
"type": "rendered",
"raw": "Ran Plan for 0 projects:",
"markup": "markdown",
"html": "<p>Ran Plan for 0 projects:</p>"
},
"user": {
"display_name": "bb bot",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/users/%7B600000000-0000-0000-0000-000000000000%7D"
},
"avatar": {
"href": "https://secure.gravatar.com/avatar/d5c4bac76953df92f47d1dea43fcdba0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FKB-4.png"
},
"html": {
"href": "https://bitbucket.org/%7B600000000-0000-0000-0000-000000000000%7D/"
}
},
"type": "user",
"uuid": "{600000000-0000-0000-0000-000000000000}",
"account_id": "00000000-0000-0000-0000-000000000000",
"nickname": "bb bot"
},
"deleted": false,
"pending": false,
"type": "pullrequest_comment",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931802"
},
"html": {
"href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931802"
}
},
"pullrequest": {
"type": "pullrequest",
"id": 5,
"title": "for test",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5"
},
"html": {
"href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5"
}
}
}
},
{
"id": 498931882,
"created_on": "2024-05-07T12:22:01.870344+00:00",
"updated_on": "2024-05-07T12:22:01.870462+00:00",
"content": {
"type": "rendered",
"raw": "atlantis plan",
"markup": "markdown",
"html": "<p>atlantis plan</p>"
},
"user": {
"display_name": "Ragne",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/users/%7B00000000-0000-0000-0000-000000000001%7D"
},
"avatar": {
"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/EM-3.png"
},
"html": {
"href": "https://bitbucket.org/%7B00000000-0000-0000-0000-000000000001%7D/"
}
},
"type": "user",
"uuid": "{00000000-0000-0000-0000-000000000001}",
"account_id": "000000:00000000-0000-0000-0000-000000000001",
"nickname": "Ragne"
},
"deleted": false,
"pending": false,
"type": "pullrequest_comment",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931882"
},
"html": {
"href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931882"
}
},
"pullrequest": {
"type": "pullrequest",
"id": 5,
"title": "for test",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5"
},
"html": {
"href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5"
}
}
}
},
{
"id": 498931901,
"created_on": "2024-05-07T12:22:04.981415+00:00",
"updated_on": "2024-05-07T12:22:04.981490+00:00",
"content": {
"type": "rendered",
"raw": "Ran Plan for 0 projects:",
"markup": "markdown",
"html": "<p>Ran Plan for 0 projects:</p>"
},
"user": {
"display_name": "bb bot",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/users/%7B600000000-0000-0000-0000-000000000000%7D"
},
"avatar": {
"href": "https://secure.gravatar.com/avatar/d5c4bac76953df92f47d1dea43fcdba0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FKB-4.png"
},
"html": {
"href": "https://bitbucket.org/%7B600000000-0000-0000-0000-000000000000%7D/"
}
},
"type": "user",
"uuid": "{600000000-0000-0000-0000-000000000000}",
"account_id": "00000000-0000-0000-0000-000000000000",
"nickname": "bb bot"
},
"deleted": false,
"pending": false,
"type": "pullrequest_comment",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931901"
},
"html": {
"href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931901"
}
},
"pullrequest": {
"type": "pullrequest",
"id": 5,
"title": "for test",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5"
},
"html": {
"href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5"
}
}
}
},
{
"id": 49893111,
"created_on": "2024-05-07T12:22:05.981415+00:00",
"updated_on": "2024-05-07T12:22:05.981490+00:00",
"content": {
"type": "rendered",
"raw": "Ran Apply for 0 projects:",
"markup": "markdown",
"html": "<p>Ran Apply for 0 projects:</p>"
},
"user": {
"display_name": "bb bot",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/users/%7B600000000-0000-0000-0000-000000000000%7D"
},
"avatar": {
"href": "https://secure.gravatar.com/avatar/d5c4bac76953df92f47d1dea43fcdba0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FKB-4.png"
},
"html": {
"href": "https://bitbucket.org/%7B600000000-0000-0000-0000-000000000000%7D/"
}
},
"type": "user",
"uuid": "{600000000-0000-0000-0000-000000000000}",
"account_id": "00000000-0000-0000-0000-000000000000",
"nickname": "bb bot"
},
"deleted": false,
"pending": false,
"type": "pullrequest_comment",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931901"
},
"html": {
"href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931901"
}
},
"pullrequest": {
"type": "pullrequest",
"id": 5,
"title": "for test",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5"
},
"html": {
"href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5"
}
}
}
}
],
"pagelen": 10,
"size": 4,
"page": 1
}

View File

@@ -0,0 +1,33 @@
{
"display_name": "bb bot",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/users/%7B00000000-0000-0000-0000-000000000001%7D"
},
"avatar": {
"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/RR-3.png"
},
"repositories": {
"href": "https://api.bitbucket.org/2.0/repositories/%7B00000000-0000-0000-0000-000000000001%7D"
},
"snippets": {
"href": "https://api.bitbucket.org/2.0/snippets/%7B00000000-0000-0000-0000-000000000001%7D"
},
"html": {
"href": "https://bitbucket.org/%7B00000000-0000-0000-0000-000000000001%7D/"
},
"hooks": {
"href": "https://api.bitbucket.org/2.0/workspaces/%7B00000000-0000-0000-0000-000000000001%7D/hooks"
}
},
"created_on": "2024-02-01T12:08:46.355300+00:00",
"type": "user",
"uuid": "{00000000-0000-0000-0000-000000000001}",
"has_2fa_enabled": null,
"username": "bb-bot",
"is_staff": false,
"account_id": "000000:00000000-0000-0000-0000-000000000001",
"nickname": "bb bot",
"account_status": "active",
"location": null
}