Skip to content

Commit

Permalink
chore: move gh client to its file
Browse files Browse the repository at this point in the history
  • Loading branch information
giautm committed Dec 14, 2024
1 parent 4b19a2f commit 75d2370
Show file tree
Hide file tree
Showing 3 changed files with 330 additions and 329 deletions.
303 changes: 5 additions & 298 deletions atlasaction/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
Expand Down Expand Up @@ -1347,6 +1346,11 @@ func writeBashEnv(path, name, value string) error {
return nil
}

// commentMarker creates a hidden marker to identify the comment as one created by this action.
func commentMarker(id string) string {
return fmt.Sprintf(`<!-- generated by ariga/atlas-action for %v -->`, id)
}

type coloredLogger struct {
w io.Writer
}
Expand Down Expand Up @@ -1378,300 +1382,3 @@ func (l *coloredLogger) WithFieldsMap(map[string]string) Logger {
}

var _ Logger = (*coloredLogger)(nil)

type (
githubIssueComment struct {
ID int `json:"id"`
Body string `json:"body"`
}
pullRequestComment struct {
ID int `json:"id,omitempty"`
Body string `json:"body"`
Path string `json:"path"`
CommitID string `json:"commit_id,omitempty"`
StartLine int `json:"start_line,omitempty"`
Line int `json:"line,omitempty"`
}
pullRequestFile struct {
Name string `json:"filename"`
}
)

func (g *githubAPI) UpsertComment(ctx context.Context, pr *PullRequest, id, comment string) error {
comments, err := g.getIssueComments(ctx, pr)
if err != nil {
return err
}
var (
marker = commentMarker(id)
body = strings.NewReader(fmt.Sprintf(`{"body": %q}`, comment+"\n"+marker))
)
if found := slices.IndexFunc(comments, func(c githubIssueComment) bool {
return strings.Contains(c.Body, marker)
}); found != -1 {
return g.updateIssueComment(ctx, comments[found].ID, body)
}
return g.createIssueComment(ctx, pr, body)
}

func (g *githubAPI) getIssueComments(ctx context.Context, pr *PullRequest) ([]githubIssueComment, error) {
url := fmt.Sprintf("%v/repos/%v/issues/%v/comments", g.baseURL, g.repo, pr.Number)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
res, err := g.client.Do(req)
if err != nil {
return nil, fmt.Errorf("error querying github comments with %v/%v, %w", g.repo, pr.Number, err)
}
defer res.Body.Close()
buf, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("error reading PR issue comments from %v/%v, %v", g.repo, pr.Number, err)
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code %v when calling GitHub API", res.StatusCode)
}
var comments []githubIssueComment
if err = json.Unmarshal(buf, &comments); err != nil {
return nil, fmt.Errorf("error parsing github comments with %v/%v from %v, %w", g.repo, pr.Number, string(buf), err)
}
return comments, nil
}

func (g *githubAPI) createIssueComment(ctx context.Context, pr *PullRequest, content io.Reader) error {
url := fmt.Sprintf("%v/repos/%v/issues/%v/comments", g.baseURL, g.repo, pr.Number)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, content)
if err != nil {
return err
}
res, err := g.client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
b, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("unexpected status code %v: unable to read body %v", res.StatusCode, err)
}
return fmt.Errorf("unexpected status code %v: with body: %v", res.StatusCode, string(b))
}
return err
}

// updateIssueComment updates issue comment with the given id.
func (g *githubAPI) updateIssueComment(ctx context.Context, id int, content io.Reader) error {
url := fmt.Sprintf("%v/repos/%v/issues/comments/%v", g.baseURL, g.repo, id)
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, url, content)
if err != nil {
return err
}
res, err := g.client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
b, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("unexpected status code %v: unable to read body %v", res.StatusCode, err)
}
return fmt.Errorf("unexpected status code %v: with body: %v", res.StatusCode, string(b))
}
return err
}

// UpsertSuggestion creates or updates a suggestion review comment on trigger event pull request.
func (g *githubAPI) UpsertSuggestion(ctx context.Context, pr *PullRequest, s *Suggestion) error {
marker := commentMarker(s.ID)
body := fmt.Sprintf("%s\n%s", s.Comment, marker)
// TODO: Listing the comments only once and updating the comment in the same call.
comments, err := g.listReviewComments(ctx, pr)
if err != nil {
return err
}
// Search for the comment marker in the comments list.
// If found, update the comment with the new suggestion.
// If not found, create a new suggestion comment.
found := slices.IndexFunc(comments, func(c pullRequestComment) bool {
return c.Path == s.Path && strings.Contains(c.Body, marker)
})
if found != -1 {
if err := g.updateReviewComment(ctx, comments[found].ID, body); err != nil {
return err
}
return nil
}
buf, err := json.Marshal(pullRequestComment{
Body: body,
Path: s.Path,
CommitID: pr.Commit,
Line: s.Line,
StartLine: s.StartLine,
})
if err != nil {
return err
}
url := fmt.Sprintf("%v/repos/%v/pulls/%v/comments", g.baseURL, g.repo, pr.Number)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(buf))
if err != nil {
return err
}
res, err := g.client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
b, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("unexpected status code %v: unable to read body %v", res.StatusCode, err)
}
return fmt.Errorf("unexpected status code %v: with body: %v", res.StatusCode, string(b))
}
return err
}

// listReviewComments for the trigger event pull request.
func (g *githubAPI) listReviewComments(ctx context.Context, pr *PullRequest) ([]pullRequestComment, error) {
url := fmt.Sprintf("%v/repos/%v/pulls/%v/comments", g.baseURL, g.repo, pr.Number)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
res, err := g.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("unexpected status code %v: unable to read body %v", res.StatusCode, err)
}
return nil, fmt.Errorf("unexpected status code %v: with body: %v", res.StatusCode, string(b))
}
var comments []pullRequestComment
if err = json.NewDecoder(res.Body).Decode(&comments); err != nil {
return nil, err
}
return comments, nil
}

// updateReviewComment updates the review comment with the given id.
func (g *githubAPI) updateReviewComment(ctx context.Context, id int, body string) error {
type pullRequestUpdate struct {
Body string `json:"body"`
}
b, err := json.Marshal(pullRequestUpdate{Body: body})
if err != nil {
return err
}
url := fmt.Sprintf("%v/repos/%v/pulls/comments/%v", g.baseURL, g.repo, id)
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, url, bytes.NewReader(b))
if err != nil {
return err
}
res, err := g.client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
b, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("unexpected status code %v: unable to read body %v", res.StatusCode, err)
}
return fmt.Errorf("unexpected status code %v: with body: %v", res.StatusCode, string(b))
}
return err
}

// ListPullRequestFiles return paths of the files in the trigger event pull request.
func (g *githubAPI) ListPullRequestFiles(ctx context.Context, pr *PullRequest) ([]string, error) {
url := fmt.Sprintf("%v/repos/%v/pulls/%v/files", g.baseURL, g.repo, pr.Number)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
res, err := g.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("unexpected status code %v: unable to read body %v", res.StatusCode, err)
}
return nil, fmt.Errorf("unexpected status code %v: with body: %v", res.StatusCode, string(b))
}
var files []pullRequestFile
if err = json.NewDecoder(res.Body).Decode(&files); err != nil {
return nil, err
}
paths := make([]string, len(files))
for i := range files {
paths[i] = files[i].Name
}
return paths, nil
}

// OpeningPullRequest returns the latest open pull request for the given branch.
func (g *githubAPI) OpeningPullRequest(ctx context.Context, branch string) (*PullRequest, error) {
owner, _, err := g.ownerRepo()
if err != nil {
return nil, err
}
// Get open pull requests for the branch.
url := fmt.Sprintf("%s/repos/%s/pulls?state=open&head=%s:%s&sort=created&direction=desc&per_page=1&page=1",
g.baseURL, g.repo, owner, branch)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
res, err := g.client.Do(req)
if err != nil {
return nil, fmt.Errorf("error calling GitHub API: %w", err)
}
defer res.Body.Close()
switch buf, err := io.ReadAll(res.Body); {
case err != nil:
return nil, fmt.Errorf("error reading open pull requests: %w", err)
case res.StatusCode != http.StatusOK:
return nil, fmt.Errorf("unexpected status code: %d when calling GitHub API", res.StatusCode)
default:
var resp []struct {
Url string `json:"url"`
Number int `json:"number"`
Head struct {
Sha string `json:"sha"`
} `json:"head"`
}
if err = json.Unmarshal(buf, &resp); err != nil {
return nil, err
}
if len(resp) == 0 {
return nil, nil
}
return &PullRequest{
Number: resp[0].Number,
URL: resp[0].Url,
Commit: resp[0].Head.Sha,
}, nil
}
}

func (g *githubAPI) ownerRepo() (string, string, error) {
s := strings.Split(g.repo, "/")
if len(s) != 2 {
return "", "", fmt.Errorf("GITHUB_REPOSITORY must be in the format of 'owner/repo'")
}
return s[0], s[1], nil
}

// commentMarker creates a hidden marker to identify the comment as one created by this action.
func commentMarker(id string) string {
return fmt.Sprintf(`<!-- generated by ariga/atlas-action for %v -->`, id)
}
6 changes: 3 additions & 3 deletions atlasaction/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2351,15 +2351,15 @@ func TestSchemaPlanApprove(t *testing.T) {
return a
}
require.ErrorContains(t, newActs().SchemaPlanApprove(ctx), "found multiple schema plans, please approve or delete the existing plans")
require.Len(t, act.summary, 0, "Expected 1 summary")
require.Equal(t, 0, act.summary, "Expected 0 summary")

// Trigger with no pull request, master branch
planFiles = []atlasexec.SchemaPlanFile{*planFile}
act.trigger.PullRequest = nil
act.trigger.Branch = "master"
act.resetOutputs()
require.NoError(t, newActs().SchemaPlanApprove(ctx))
require.Len(t, act.summary, 0, "No more summaries generated")
require.Equal(t, 0, act.summary, "No more summaries generated")
require.EqualValues(t, map[string]string{
"plan": "atlas://atlas-action/plans/pr-1-Rl4lBdMk",
"status": "APPROVED",
Expand All @@ -2370,7 +2370,7 @@ func TestSchemaPlanApprove(t *testing.T) {
planFiles = nil
act.resetOutputs()
require.NoError(t, newActs().SchemaPlanApprove(ctx))
require.Len(t, act.summary, 0, "No more summaries generated")
require.Equal(t, 0, act.summary, "No more summaries generated")
require.EqualValues(t, map[string]string{}, act.output, "expected output with plan URL")

// Check all logs output
Expand Down
Loading

0 comments on commit 75d2370

Please sign in to comment.