From 86e19e048fb13a18ed1dad6201003b0a49a3bafa Mon Sep 17 00:00:00 2001 From: Alexandre Dath Date: Wed, 9 Mar 2022 15:14:42 +0100 Subject: [PATCH 1/5] Add comments object --- comment.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++++ errors.go | 4 ++ run.go | 1 + tfe.go | 2 + 4 files changed, 147 insertions(+) create mode 100644 comment.go diff --git a/comment.go b/comment.go new file mode 100644 index 000000000..1c5b4e669 --- /dev/null +++ b/comment.go @@ -0,0 +1,140 @@ +package tfe + +import ( + "context" + "fmt" + "net/url" +) + +// Compile-time proof of interface implementation. +var _ Comments = (*comments)(nil) + +// Comments describes all the comment related methods that the +// Terraform Enterprise API supports. +// +// TFE API docs: +// https://www.terraform.io/docs/cloud/api/comments.html +type Comments interface { + // List all comments of the given run. + List(ctx context.Context, runID string, options *CommentListOptions) (*CommentList, error) + + // Read a comment by its ID. + Read(ctx context.Context, CommentID string) (*Comment, error) + + // Create a new comment with the given options. + Create(ctx context.Context, runID string, options CommentCreateOptions) (*Comment, error) +} + +// Comments implements Comments. +type comments struct { + client *Client +} + +// CommentList represents a list of comments. +type CommentList struct { + *Pagination + Items []*Comment +} + +// Comment represents a Terraform Enterprise comment.. +type Comment struct { + ID string `jsonapi:"primary,comments"` + Body string `jsonapi:"attr,body"` +} + +// CommentListOptions represents the options for listing comments. +type CommentListOptions struct { + ListOptions +} + +type CommentCreateOptions struct { + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,comments"` + + // Required: Body of the comment. + Body *string `jsonapi:"attr,body"` + + // Optional: Run where the comment is attached + Run *Run `jsonapi:"relation,run"` +} + +// List all comments of the given run. +func (s *comments) List(ctx context.Context, runID string, options *CommentListOptions) (*CommentList, error) { + if !validStringID(&runID) { + return nil, ErrInvalidRunID + } + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("runs/%s/comments", url.QueryEscape(runID)) + req, err := s.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + cl := &CommentList{} + err = s.client.do(ctx, req, cl) + if err != nil { + return nil, err + } + + return cl, nil +} + +// Create a new comment with the given options. +func (s *comments) Create(ctx context.Context, runID string, options CommentCreateOptions) (*Comment, error) { + if !validStringID(&runID) { + return nil, ErrInvalidRunID + } + + if !validString(options.Body) { + return nil, ErrCommentBody + } + + u := fmt.Sprintf("runs/%s/comments", url.QueryEscape(runID)) + req, err := s.client.newRequest("POST", u, &options) + if err != nil { + return nil, err + } + + comm := &Comment{} + err = s.client.do(ctx, req, comm) + if err != nil { + return nil, err + } + + return comm, err +} + +// Read a comment by its ID. +func (s *comments) Read(ctx context.Context, CommentID string) (*Comment, error) { + if !validStringID(&CommentID) { + return nil, ErrInvalidCommentID + } + + u := fmt.Sprintf("comments/%s", url.QueryEscape(CommentID)) + req, err := s.client.newRequest("GET", u, nil) + if err != nil { + return nil, err + } + + comm := &Comment{} + err = s.client.do(ctx, req, comm) + if err != nil { + return nil, err + } + + return comm, nil +} + +func (o *CommentListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + return nil +} diff --git a/errors.go b/errors.go index f15501f30..a3fe08bf9 100644 --- a/errors.go +++ b/errors.go @@ -145,6 +145,8 @@ var ( ErrInvalidVariableID = errors.New("invalid value for variable ID") ErrInvalidNotificationTrigger = errors.New("invalid value for notification trigger") + + ErrInvalidCommentID = errors.New("invalid value for comment ID") ) // Missing values for required field/option @@ -248,4 +250,6 @@ var ( ErrRequiredOnlyOneField = errors.New("only one of usernames or organization membership ids can be provided") ErrRequiredUsernameOrMembershipIds = errors.New("usernames or organization membership ids are required") + + ErrCommentBody = errors.New("comment body is required") ) diff --git a/run.go b/run.go index 9a069efd0..fd3e7db67 100644 --- a/run.go +++ b/run.go @@ -118,6 +118,7 @@ type Run struct { PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"` TaskStages []*TaskStage `jsonapi:"relation,task-stages,omitempty"` Workspace *Workspace `jsonapi:"relation,workspace"` + Comments []*Comment `jsonapi:"relation,comments"` } // RunActions represents the run actions. diff --git a/tfe.go b/tfe.go index c255258c1..f5ec590b1 100644 --- a/tfe.go +++ b/tfe.go @@ -110,6 +110,7 @@ type Client struct { AgentPools AgentPools AgentTokens AgentTokens Applies Applies + Comments Comments ConfigurationVersions ConfigurationVersions CostEstimates CostEstimates NotificationConfigurations NotificationConfigurations @@ -251,6 +252,7 @@ func NewClient(cfg *Config) (*Client, error) { client.AgentPools = &agentPools{client: client} client.AgentTokens = &agentTokens{client: client} client.Applies = &applies{client: client} + client.Comments = &comments{client: client} client.ConfigurationVersions = &configurationVersions{client: client} client.CostEstimates = &costEstimates{client: client} client.NotificationConfigurations = ¬ificationConfigurations{client: client} From 032d00ff35b8b196a216a01557b484b040b180dd Mon Sep 17 00:00:00 2001 From: Alexandre Dath Date: Wed, 9 Mar 2022 23:20:55 +0100 Subject: [PATCH 2/5] Remove options for Comments.List --- comment.go | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/comment.go b/comment.go index 1c5b4e669..a714392d7 100644 --- a/comment.go +++ b/comment.go @@ -16,7 +16,7 @@ var _ Comments = (*comments)(nil) // https://www.terraform.io/docs/cloud/api/comments.html type Comments interface { // List all comments of the given run. - List(ctx context.Context, runID string, options *CommentListOptions) (*CommentList, error) + List(ctx context.Context, runID string) (*CommentList, error) // Read a comment by its ID. Read(ctx context.Context, CommentID string) (*Comment, error) @@ -36,17 +36,12 @@ type CommentList struct { Items []*Comment } -// Comment represents a Terraform Enterprise comment.. +// Comment represents a Terraform Enterprise comment. type Comment struct { ID string `jsonapi:"primary,comments"` Body string `jsonapi:"attr,body"` } -// CommentListOptions represents the options for listing comments. -type CommentListOptions struct { - ListOptions -} - type CommentCreateOptions struct { // Type is a public field utilized by JSON:API to // set the resource type via the field tag. @@ -62,16 +57,13 @@ type CommentCreateOptions struct { } // List all comments of the given run. -func (s *comments) List(ctx context.Context, runID string, options *CommentListOptions) (*CommentList, error) { +func (s *comments) List(ctx context.Context, runID string) (*CommentList, error) { if !validStringID(&runID) { return nil, ErrInvalidRunID } - if err := options.valid(); err != nil { - return nil, err - } u := fmt.Sprintf("runs/%s/comments", url.QueryEscape(runID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.newRequest("GET", u, nil) if err != nil { return nil, err } @@ -130,11 +122,3 @@ func (s *comments) Read(ctx context.Context, CommentID string) (*Comment, error) return comm, nil } - -func (o *CommentListOptions) valid() error { - if o == nil { - return nil // nothing to validate - } - - return nil -} From 2b36c338b7d78b17243abe7aebc569f6d5e8d40e Mon Sep 17 00:00:00 2001 From: Alexandre Dath Date: Wed, 9 Mar 2022 23:21:12 +0100 Subject: [PATCH 3/5] Add Integration test for Comments --- comment_integration_test.go | 77 +++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 comment_integration_test.go diff --git a/comment_integration_test.go b/comment_integration_test.go new file mode 100644 index 000000000..c0c7ee822 --- /dev/null +++ b/comment_integration_test.go @@ -0,0 +1,77 @@ +//go:build integration +// +build integration + +package tfe + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCommentsList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wTest1, wTest1Cleanup := createWorkspace(t, client, orgTest) + defer wTest1Cleanup() + + rTest, rTest1Cleanup := createRun(t, client, wTest1) + defer rTest1Cleanup() + commentBody1 := "1st comment test" + commentBody2 := "2nd comment test" + + t.Run("without comments", func(t *testing.T) { + _, err := client.Comments.List(ctx, rTest.ID) + require.NoError(t, err) + }) + + t.Run("without a valid run", func(t *testing.T) { + cl, err := client.Comments.List(ctx, badIdentifier) + assert.Nil(t, cl) + assert.EqualError(t, err, ErrInvalidRunID.Error()) + }) + + t.Run("create a comment", func(t *testing.T) { + options := CommentCreateOptions{ + Body: String(commentBody1), + } + cl, err := client.Comments.Create(ctx, rTest.ID, options) + require.NoError(t, err) + assert.Equal(t, commentBody1, cl.Body) + }) + + t.Run("create 2nd comment", func(t *testing.T) { + options := CommentCreateOptions{ + Body: String(commentBody2), + } + cl, err := client.Comments.Create(ctx, rTest.ID, options) + require.NoError(t, err) + assert.Equal(t, commentBody2, cl.Body) + }) + + t.Run("list comments", func(t *testing.T) { + commentsList, err := client.Comments.List(ctx, rTest.ID) + require.NoError(t, err) + assert.Len(t, commentsList.Items, 2) + assert.Equal(t, true, commentItemsContainsBody(commentsList.Items, commentBody1)) + assert.Equal(t, true, commentItemsContainsBody(commentsList.Items, commentBody2)) + }) +} + +func commentItemsContainsBody(items []*Comment, body string) bool { + hasBody := false + for _, item := range items { + if item.Body == body { + hasBody = true + break + } + } + + return hasBody +} From 8414b93fbbd881ab43128560fd41c7e52d6ff56d Mon Sep 17 00:00:00 2001 From: Alexandre Dath Date: Wed, 16 Mar 2022 23:27:05 +0100 Subject: [PATCH 4/5] changed due to some patterns established for the upcoming 1.0 release --- comment.go | 29 +++++++++++++++++------------ comment_integration_test.go | 4 ++-- errors.go | 2 ++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/comment.go b/comment.go index a714392d7..0f1a069a8 100644 --- a/comment.go +++ b/comment.go @@ -19,7 +19,7 @@ type Comments interface { List(ctx context.Context, runID string) (*CommentList, error) // Read a comment by its ID. - Read(ctx context.Context, CommentID string) (*Comment, error) + Read(ctx context.Context, commentID string) (*Comment, error) // Create a new comment with the given options. Create(ctx context.Context, runID string, options CommentCreateOptions) (*Comment, error) @@ -50,10 +50,7 @@ type CommentCreateOptions struct { Type string `jsonapi:"primary,comments"` // Required: Body of the comment. - Body *string `jsonapi:"attr,body"` - - // Optional: Run where the comment is attached - Run *Run `jsonapi:"relation,run"` + Body string `jsonapi:"attr,body"` } // List all comments of the given run. @@ -79,12 +76,12 @@ func (s *comments) List(ctx context.Context, runID string) (*CommentList, error) // Create a new comment with the given options. func (s *comments) Create(ctx context.Context, runID string, options CommentCreateOptions) (*Comment, error) { - if !validStringID(&runID) { - return nil, ErrInvalidRunID + if err := options.valid(); err != nil { + return nil, err } - if !validString(options.Body) { - return nil, ErrCommentBody + if !validStringID(&runID) { + return nil, ErrInvalidRunID } u := fmt.Sprintf("runs/%s/comments", url.QueryEscape(runID)) @@ -103,12 +100,12 @@ func (s *comments) Create(ctx context.Context, runID string, options CommentCrea } // Read a comment by its ID. -func (s *comments) Read(ctx context.Context, CommentID string) (*Comment, error) { - if !validStringID(&CommentID) { +func (s *comments) Read(ctx context.Context, commentID string) (*Comment, error) { + if !validStringID(&commentID) { return nil, ErrInvalidCommentID } - u := fmt.Sprintf("comments/%s", url.QueryEscape(CommentID)) + u := fmt.Sprintf("comments/%s", url.QueryEscape(commentID)) req, err := s.client.newRequest("GET", u, nil) if err != nil { return nil, err @@ -122,3 +119,11 @@ func (s *comments) Read(ctx context.Context, CommentID string) (*Comment, error) return comm, nil } + +func (o CommentCreateOptions) valid() error { + if !validString(&o.Body) { + return ErrInvalidCommentBody + } + + return nil +} diff --git a/comment_integration_test.go b/comment_integration_test.go index c0c7ee822..35aa56d30 100644 --- a/comment_integration_test.go +++ b/comment_integration_test.go @@ -39,7 +39,7 @@ func TestCommentsList(t *testing.T) { t.Run("create a comment", func(t *testing.T) { options := CommentCreateOptions{ - Body: String(commentBody1), + Body: commentBody1, } cl, err := client.Comments.Create(ctx, rTest.ID, options) require.NoError(t, err) @@ -48,7 +48,7 @@ func TestCommentsList(t *testing.T) { t.Run("create 2nd comment", func(t *testing.T) { options := CommentCreateOptions{ - Body: String(commentBody2), + Body: commentBody2, } cl, err := client.Comments.Create(ctx, rTest.ID, options) require.NoError(t, err) diff --git a/errors.go b/errors.go index a3fe08bf9..466514661 100644 --- a/errors.go +++ b/errors.go @@ -147,6 +147,8 @@ var ( ErrInvalidNotificationTrigger = errors.New("invalid value for notification trigger") ErrInvalidCommentID = errors.New("invalid value for comment ID") + + ErrInvalidCommentBody = errors.New("invalid value for comment body") ) // Missing values for required field/option From 25251512ae7877dde1f2da068a1b0f62161130a0 Mon Sep 17 00:00:00 2001 From: Luces Huayhuaca <21225410+uturunku1@users.noreply.github.com> Date: Tue, 22 Mar 2022 08:30:45 -0700 Subject: [PATCH 5/5] add github action workflow (#362) --- .github/workflows/merged-pr.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/merged-pr.yml diff --git a/.github/workflows/merged-pr.yml b/.github/workflows/merged-pr.yml new file mode 100644 index 000000000..0424b8283 --- /dev/null +++ b/.github/workflows/merged-pr.yml @@ -0,0 +1,24 @@ +name: Merged Pull Request +permissions: + pull-requests: write + +# only trigger on pull request closed events +on: + pull_request_target: + types: [ closed ] + +jobs: + merge_job: + # this job will only run if the PR has been merged + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v5 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Reminder to the contributor that merged this PR: if your changes have added important functionality or fixed a relevant bug, open a follow-up PR to update CHANGELOG.md with a note on your changes." + })