diff --git a/atlasaction/action.go b/atlasaction/action.go index 8c6936b1..f4d058e8 100644 --- a/atlasaction/action.go +++ b/atlasaction/action.go @@ -14,7 +14,6 @@ import ( "errors" "fmt" "io" - "net/http" "net/url" "os" "path" @@ -51,11 +50,16 @@ type ( // SetOutput sets the value of the output with the given name. SetOutput(string, string) // GetTriggerContext returns the context of the trigger event. - GetTriggerContext() (*TriggerContext, error) - // AddStepSummary adds a summary to the action step. - AddStepSummary(string) + GetTriggerContext(context.Context) (*TriggerContext, error) } + // Reporter is an interface for reporting the status of the actions. + Reporter interface { + MigrateApply(context.Context, *atlasexec.MigrateApply) + MigrateLint(context.Context, *atlasexec.SummaryReport) + SchemaPlan(context.Context, *atlasexec.SchemaPlan) + SchemaApply(context.Context, *atlasexec.SchemaApply) + } // SCMClient contains methods for interacting with SCM platforms (GitHub, Gitlab etc...). SCMClient interface { // UpsertComment posts or updates a pull request comment. @@ -352,11 +356,8 @@ func (a *Actions) MigrateApply(ctx context.Context) error { return nil } for _, run := range runs { - switch summary, err := RenderTemplate("migrate-apply.tmpl", run); { - case err != nil: - a.Errorf("failed to create summary: %v", err) - default: - a.AddStepSummary(summary) + if r, ok := a.Action.(Reporter); ok { + r.MigrateApply(ctx, run) } if run.Error != "" { a.SetOutput("error", run.Error) @@ -441,15 +442,17 @@ func (a *Actions) MigrateDown(ctx context.Context) (err error) { // MigratePush runs the GitHub Action for "ariga/atlas-action/migrate/push" func (a *Actions) MigratePush(ctx context.Context) error { - tc, err := a.GetTriggerContext() + tc, err := a.GetTriggerContext(ctx) if err != nil { return err } + rc := tc.GetRunContext() + rc.Path = a.GetInput("dir") params := &atlasexec.MigratePushParams{ + Context: rc, Name: a.GetInput("dir-name"), DirURL: a.GetInput("dir"), DevURL: a.GetInput("dev-url"), - Context: a.GetRunContext(ctx, tc), ConfigURL: a.GetInput("config"), Env: a.GetInput("env"), Vars: a.GetVarsInput("vars"), @@ -481,7 +484,7 @@ func (a *Actions) MigrateLint(ctx context.Context) error { if dirName == "" { return errors.New("atlasaction: missing required parameter dir-name") } - tc, err := a.GetTriggerContext() + tc, err := a.GetTriggerContext(ctx) if err != nil { return err } @@ -489,13 +492,15 @@ func (a *Actions) MigrateLint(ctx context.Context) error { resp bytes.Buffer isLintErr bool ) + rc := tc.GetRunContext() + rc.Path = a.GetInput("dir") switch err := a.Atlas.MigrateLintError(ctx, &atlasexec.MigrateLintParams{ + Context: rc, DevURL: a.GetInput("dev-url"), DirURL: a.GetInput("dir"), ConfigURL: a.GetInput("config"), Env: a.GetInput("env"), Base: a.GetAtlasURLInput("dir-name", "tag"), - Context: a.GetRunContext(ctx, tc), Vars: a.GetVarsInput("vars"), Web: true, Writer: &resp, @@ -515,11 +520,9 @@ func (a *Actions) MigrateLint(ctx context.Context) error { if err := a.addChecks(&payload); err != nil { return err } - summary, err := RenderTemplate("migrate-lint.tmpl", &payload) - if err != nil { - return err + if r, ok := a.Action.(Reporter); ok { + r.MigrateLint(ctx, &payload) } - a.AddStepSummary(summary) if tc.PullRequest == nil { if isLintErr { return fmt.Errorf("`atlas migrate lint` completed with errors, see report: %s", payload.URL) @@ -532,7 +535,11 @@ func (a *Actions) MigrateLint(ctx context.Context) error { case err != nil: return err default: - if err = c.UpsertComment(ctx, tc.PullRequest, dirName, summary); err != nil { + comment, err := RenderTemplate("migrate-lint.tmpl", &payload) + if err != nil { + return err + } + if err = c.UpsertComment(ctx, tc.PullRequest, dirName, comment); err != nil { a.Errorf("failed to comment on the pull request: %v", err) } if c, ok := c.(SCMSuggestions); ok { @@ -581,18 +588,18 @@ func (a *Actions) MigrateTest(ctx context.Context) error { // SchemaPush runs the GitHub Action for "ariga/atlas-action/schema/push" func (a *Actions) SchemaPush(ctx context.Context) error { - tc, err := a.GetTriggerContext() + tc, err := a.GetTriggerContext(ctx) if err != nil { return err } params := &atlasexec.SchemaPushParams{ + Context: tc.GetRunContext(), Name: a.GetInput("schema-name"), Description: a.GetInput("description"), Version: a.GetInput("version"), URL: a.GetArrayInput("url"), Schema: a.GetArrayInput("schema"), DevURL: a.GetInput("dev-url"), - Context: a.GetRunContext(ctx, tc), ConfigURL: a.GetInput("config"), Env: a.GetInput("env"), Vars: a.GetVarsInput("vars"), @@ -640,7 +647,7 @@ func (a *Actions) SchemaTest(ctx context.Context) error { // SchemaPlan runs the GitHub Action for "ariga/atlas-action/schema/plan" func (a *Actions) SchemaPlan(ctx context.Context) error { - tc, err := a.GetTriggerContext() + tc, err := a.GetTriggerContext(ctx) switch { case err != nil: return fmt.Errorf("unable to get the trigger context: %w", err) @@ -649,10 +656,10 @@ func (a *Actions) SchemaPlan(ctx context.Context) error { } var plan *atlasexec.SchemaPlan params := &atlasexec.SchemaPlanListParams{ + Context: tc.GetRunContext(), ConfigURL: a.GetInput("config"), Env: a.GetInput("env"), Vars: a.GetVarsInput("vars"), - Context: a.GetRunContext(ctx, tc), Repo: a.GetAtlasURLInput("schema-name"), DevURL: a.GetInput("dev-url"), Schema: a.GetArrayInput("schema"), @@ -727,22 +734,24 @@ func (a *Actions) SchemaPlan(ctx context.Context) error { a.SetOutput("link", plan.File.Link) a.SetOutput("plan", plan.File.URL) a.SetOutput("status", plan.File.Status) - // Report the schema plan to the user and add a comment to the PR. - summary, err := RenderTemplate("schema-plan.tmpl", map[string]any{ - "Plan": plan, - "EnvName": params.Env, - "RerunCommand": tc.RerunCmd, - }) - if err != nil { - return fmt.Errorf("failed to generate schema plan comment: %w", err) + if r, ok := a.Action.(Reporter); ok { + r.SchemaPlan(ctx, plan) } - a.AddStepSummary(summary) switch c, err := a.SCM(tc); { case errors.Is(err, ErrNoSCM): case err != nil: return err default: - err = c.UpsertComment(ctx, tc.PullRequest, plan.File.Name, summary) + // Report the schema plan to the user and add a comment to the PR. + comment, err := RenderTemplate("schema-plan.tmpl", map[string]any{ + "Plan": plan, + "EnvName": params.Env, + "RerunCommand": tc.RerunCmd, + }) + if err != nil { + return fmt.Errorf("failed to generate schema plan comment: %w", err) + } + err = c.UpsertComment(ctx, tc.PullRequest, plan.File.Name, comment) if err != nil { // Don't fail the action if the comment fails. // It may be due to the missing permissions. @@ -759,7 +768,7 @@ func (a *Actions) SchemaPlan(ctx context.Context) error { // SchemaPlanApprove runs the GitHub Action for "ariga/atlas-action/schema/plan/approve" func (a *Actions) SchemaPlanApprove(ctx context.Context) error { - tc, err := a.GetTriggerContext() + tc, err := a.GetTriggerContext(ctx) switch { case err != nil: return fmt.Errorf("unable to get the trigger context: %w", err) @@ -775,10 +784,10 @@ func (a *Actions) SchemaPlanApprove(ctx context.Context) error { if params.URL == "" { a.Infof("No plan URL provided, searching for the pending plan") switch planFiles, err := a.Atlas.SchemaPlanList(ctx, &atlasexec.SchemaPlanListParams{ + Context: tc.GetRunContext(), ConfigURL: params.ConfigURL, Env: params.Env, Vars: params.Vars, - Context: a.GetRunContext(ctx, tc), Repo: a.GetAtlasURLInput("schema-name"), DevURL: a.GetInput("dev-url"), Schema: a.GetArrayInput("schema"), @@ -834,11 +843,8 @@ func (a *Actions) SchemaApply(ctx context.Context) error { results = mErr.Result } for _, result := range results { - switch summary, err := RenderTemplate("schema-apply.tmpl", result); { - case err != nil: - a.Errorf("failed to create summary: %v", err) - default: - a.AddStepSummary(summary) + if r, ok := a.Action.(Reporter); ok { + r.SchemaApply(ctx, result) } if result.Error != "" { a.SetOutput("error", result.Error) @@ -1059,26 +1065,6 @@ func (a *Actions) GetArrayInput(name string) []string { }) } -// GetRunContext returns the run context for the action. -func (a *Actions) GetRunContext(_ context.Context, tc *TriggerContext) *atlasexec.RunContext { - u := tc.RepoURL - if tc.PullRequest != nil { - u = tc.PullRequest.URL - } - rc := &atlasexec.RunContext{ - Repo: tc.Repo, - Branch: tc.Branch, - Commit: tc.Commit, - Path: a.GetInput("dir"), - URL: u, - SCMType: tc.SCM.Type, - } - if a := tc.Actor; a != nil { - rc.Username, rc.UserID = a.Name, a.ID - } - return rc -} - // DeployRunContext returns the run context for the `migrate/apply`, and `migrate/down` actions. func (a *Actions) DeployRunContext() *atlasexec.DeployRunContext { return &atlasexec.DeployRunContext{ @@ -1153,6 +1139,24 @@ func (a *Actions) addChecks(lint *atlasexec.SummaryReport) error { return nil } +// GetRunContext returns the run context for the action. +func (tc *TriggerContext) GetRunContext() *atlasexec.RunContext { + rc := &atlasexec.RunContext{ + URL: tc.RepoURL, + Repo: tc.Repo, + Branch: tc.Branch, + Commit: tc.Commit, + SCMType: tc.SCM.Type, + } + if pr := tc.PullRequest; pr != nil { + rc.URL = pr.URL + } + if a := tc.Actor; a != nil { + rc.Username, rc.UserID = a.Name, a.ID + } + return rc +} + type Suggestion struct { ID string // Unique identifier for the suggestion. Path string // File path. @@ -1344,6 +1348,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(``, id) +} + type coloredLogger struct { w io.Writer } @@ -1375,300 +1384,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(``, id) -} diff --git a/atlasaction/action_test.go b/atlasaction/action_test.go index 1d66e943..aba0b3d1 100644 --- a/atlasaction/action_test.go +++ b/atlasaction/action_test.go @@ -2211,7 +2211,7 @@ func TestSchemaPlan(t *testing.T) { return a } require.ErrorContains(t, newActs().SchemaPlan(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, "No summaries generated") require.Equal(t, 0, commentCounter, "No more comments generated") require.Equal(t, 0, commentEdited, "No comment should be edited") @@ -2220,7 +2220,7 @@ func TestSchemaPlan(t *testing.T) { planFiles = nil act.resetOutputs() require.NoError(t, newActs().SchemaPlan(ctx)) - require.Len(t, act.summary, 0, "No summaries generated") + require.Equal(t, 0, act.summary, "No summaries generated") require.Equal(t, 0, commentCounter, "Expected 1 comment generated") require.Equal(t, 0, commentEdited, "No comment should be edited") @@ -2229,7 +2229,7 @@ func TestSchemaPlan(t *testing.T) { planFiles = nil act.resetOutputs() require.NoError(t, newActs().SchemaPlan(ctx)) - require.Len(t, act.summary, 1, "Expected 1 summary") + require.Equal(t, 1, act.summary, "Expected 1 summary") require.Equal(t, 1, commentCounter, "Expected 1 comment generated") require.Equal(t, 0, commentEdited, "No comment should be edited") require.EqualValues(t, map[string]string{ @@ -2241,7 +2241,7 @@ func TestSchemaPlan(t *testing.T) { act.trigger.PullRequest.Body = "Text\n/atlas:txmode: none\nText" act.resetOutputs() require.NoError(t, newActs().SchemaPlan(ctx)) - require.Len(t, act.summary, 2, "Expected 1 summary") + require.Equal(t, 2, act.summary, "Expected 2 summary") require.Equal(t, []string{"atlas:txmode: none"}, planprams.Directives) act.trigger.PullRequest.Body = "" @@ -2249,7 +2249,7 @@ func TestSchemaPlan(t *testing.T) { planFiles = []atlasexec.SchemaPlanFile{*planFile} act.resetOutputs() require.NoError(t, newActs().SchemaPlan(ctx)) - require.Len(t, act.summary, 3, "Expected 2 summaries") + require.Equal(t, 3, act.summary, "Expected 3 summaries") require.Equal(t, 1, commentCounter, "No more comments generated") require.Equal(t, 2, commentEdited, "Expected comment to be edited") require.EqualValues(t, map[string]string{ @@ -2351,7 +2351,7 @@ 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} @@ -2359,7 +2359,7 @@ func TestSchemaPlanApprove(t *testing.T) { 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", @@ -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 @@ -2390,7 +2390,7 @@ type ( scm *mockSCM // scm client inputs map[string]string // input values output map[string]string // step's output - summary []string // step summaries + summary int // step summaries logger *slog.Logger // logger fatal bool // fatal called } @@ -2400,7 +2400,28 @@ type ( } ) +// MigrateApply implements atlasaction.Reporter. +func (m *mockAction) MigrateApply(context.Context, *atlasexec.MigrateApply) { + m.summary++ +} + +// MigrateLint implements atlasaction.Reporter. +func (m *mockAction) MigrateLint(context.Context, *atlasexec.SummaryReport) { + m.summary++ +} + +// SchemaApply implements atlasaction.Reporter. +func (m *mockAction) SchemaApply(context.Context, *atlasexec.SchemaApply) { + m.summary++ +} + +// SchemaPlan implements atlasaction.Reporter. +func (m *mockAction) SchemaPlan(context.Context, *atlasexec.SchemaPlan) { + m.summary++ +} + var _ atlasaction.Action = (*mockAction)(nil) +var _ atlasaction.Reporter = (*mockAction)(nil) var _ atlasaction.SCMClient = (*mockSCM)(nil) func (m *mockAction) resetOutputs() { @@ -2413,7 +2434,7 @@ func (m *mockAction) GetType() atlasexec.TriggerType { } // GetTriggerContext implements Action. -func (m *mockAction) GetTriggerContext() (*atlasaction.TriggerContext, error) { +func (m *mockAction) GetTriggerContext(context.Context) (*atlasaction.TriggerContext, error) { return m.trigger, nil } @@ -2430,11 +2451,6 @@ func (m *mockAction) SetOutput(name, value string) { m.output[name] = value } -// AddStepSummary implements Action. -func (m *mockAction) AddStepSummary(s string) { - m.summary = append(m.summary, s) -} - // Infof implements Action. func (m *mockAction) Infof(msg string, args ...interface{}) { m.logger.Info(fmt.Sprintf(msg, args...)) diff --git a/atlasaction/bitbucket.go b/atlasaction/bitbucket.go index 5ef3f39b..ecbce214 100644 --- a/atlasaction/bitbucket.go +++ b/atlasaction/bitbucket.go @@ -5,6 +5,7 @@ package atlasaction import ( + "context" "fmt" "io" "net/url" @@ -37,7 +38,7 @@ func (a *bbPipe) GetType() atlasexec.TriggerType { } // GetTriggerContext implements Action. -func (a *bbPipe) GetTriggerContext() (*TriggerContext, error) { +func (a *bbPipe) GetTriggerContext(context.Context) (*TriggerContext, error) { tc := &TriggerContext{ Branch: a.getenv("BITBUCKET_BRANCH"), Commit: a.getenv("BITBUCKET_COMMIT"), @@ -100,6 +101,4 @@ func (a *bbPipe) SetOutput(name, value string) { } } -func (a *bbPipe) AddStepSummary(string) {} - var _ Action = (*bbPipe)(nil) diff --git a/atlasaction/circleci_action.go b/atlasaction/circleci_action.go index ce517cd2..3ebae7d5 100644 --- a/atlasaction/circleci_action.go +++ b/atlasaction/circleci_action.go @@ -8,12 +8,9 @@ import ( "context" "fmt" "io" - "net/http" "strings" - "time" "ariga.io/atlas-go-sdk/atlasexec" - "golang.org/x/oauth2" ) // circleciOrb is an implementation of the Action interface for GitHub Actions. @@ -54,69 +51,52 @@ func (a *circleCIOrb) SetOutput(name, value string) { // GetTriggerContext implements the Action interface. // https://circleci.com/docs/variables/#built-in-environment-variables -func (a *circleCIOrb) GetTriggerContext() (*TriggerContext, error) { - ctx := &TriggerContext{} - if ctx.Repo = a.getenv("CIRCLE_PROJECT_REPONAME"); ctx.Repo == "" { +func (a *circleCIOrb) GetTriggerContext(ctx context.Context) (*TriggerContext, error) { + tc := &TriggerContext{ + RepoURL: a.getenv("CIRCLE_REPOSITORY_URL"), + Repo: a.getenv("CIRCLE_PROJECT_REPONAME"), + Branch: a.getenv("CIRCLE_BRANCH"), + Commit: a.getenv("CIRCLE_SHA1"), + } + if tc.Repo == "" { return nil, fmt.Errorf("missing CIRCLE_PROJECT_REPONAME environment variable") } - ctx.RepoURL = a.getenv("CIRCLE_REPOSITORY_URL") - ctx.Branch = a.getenv("CIRCLE_BRANCH") - if ctx.Commit = a.getenv("CIRCLE_SHA1"); ctx.Commit == "" { + if tc.Commit == "" { return nil, fmt.Errorf("missing CIRCLE_SHA1 environment variable") } // Detect SCM provider based on Token. switch ghToken := a.getenv("GITHUB_TOKEN"); { case ghToken != "": - ctx.SCM = SCM{ + tc.SCM = SCM{ Type: atlasexec.SCMTypeGithub, - APIURL: defaultGHApiUrl, - } - if v := a.getenv("GITHUB_API_URL"); v != "" { - ctx.SCM.APIURL = v + APIURL: a.getenv("GITHUB_API_URL"), } // Used to change the location that the linting results are posted to. // If GITHUB_REPOSITORY is not set, we default to the CIRCLE_PROJECT_REPONAME repo. if v := a.getenv("GITHUB_REPOSITORY"); v != "" { - ctx.Repo = v + tc.Repo = v } // CIRCLE_REPOSITORY_URL will be empty for some reason, causing ctx.RepoURL to be empty. // In this case, we default to the GitHub Cloud URL. - if ctx.RepoURL == "" { - ctx.RepoURL = fmt.Sprintf("https://github.com/%s", ctx.Repo) + if tc.RepoURL == "" { + tc.RepoURL = fmt.Sprintf("https://github.com/%s", tc.Repo) } // CIRCLE_BRANCH will be empty when the event is triggered by a tag. // In this case, we can use CIRCLE_TAG as the branch. - if ctx.Branch == "" { + if tc.Branch == "" { tag := a.getenv("CIRCLE_TAG") if tag == "" { return nil, fmt.Errorf("cannot determine branch due to missing CIRCLE_BRANCH and CIRCLE_TAG environment variables") } - ctx.Branch = tag - return ctx, nil - } - // get open pull requests for the branch. - c := &githubAPI{ - client: &http.Client{ - Timeout: time.Second * 30, - Transport: &oauth2.Transport{ - Source: oauth2.StaticTokenSource(&oauth2.Token{ - AccessToken: ghToken, - }), - }, - }, - baseURL: ctx.SCM.APIURL, - repo: ctx.Repo, + tc.Branch = tag + return tc, nil } + c := githubClient(tc.Repo, tc.SCM.APIURL, ghToken) var err error - ctx.PullRequest, err = c.OpeningPullRequest(context.Background(), ctx.Branch) + tc.PullRequest, err = c.OpeningPullRequest(ctx, tc.Branch) if err != nil { return nil, fmt.Errorf("failed to get open pull requests: %w", err) } } - return ctx, nil -} - -// AddStepSummary implements the Action interface. -func (a *circleCIOrb) AddStepSummary(summary string) { - // unsupported + return tc, nil } diff --git a/atlasaction/circleci_action_test.go b/atlasaction/circleci_action_test.go index 8f8aadbd..997db883 100644 --- a/atlasaction/circleci_action_test.go +++ b/atlasaction/circleci_action_test.go @@ -5,6 +5,7 @@ package atlasaction_test import ( + "context" "net/http" "net/http/httptest" "os" @@ -19,13 +20,13 @@ import ( func Test_circleCIOrb_GetTriggerContext(t *testing.T) { orb := atlasaction.NewCircleCIOrb(os.Getenv, os.Stdout) - _, err := orb.GetTriggerContext() + _, err := orb.GetTriggerContext(context.Background()) require.EqualError(t, err, "missing CIRCLE_PROJECT_REPONAME environment variable") t.Setenv("CIRCLE_PROJECT_REPONAME", "atlas-orb") - _, err = orb.GetTriggerContext() + _, err = orb.GetTriggerContext(context.Background()) require.EqualError(t, err, "missing CIRCLE_SHA1 environment variable") t.Setenv("CIRCLE_SHA1", "1234567890") - ctx, err := orb.GetTriggerContext() + ctx, err := orb.GetTriggerContext(context.Background()) require.NoError(t, err) require.Equal(t, &atlasaction.TriggerContext{ Repo: "atlas-orb", @@ -33,7 +34,7 @@ func Test_circleCIOrb_GetTriggerContext(t *testing.T) { }, ctx) t.Setenv("GITHUB_TOKEN", "1234567890") t.Setenv("GITHUB_REPOSITORY", "ariga/atlas-orb") - _, err = orb.GetTriggerContext() + _, err = orb.GetTriggerContext(context.Background()) require.EqualError(t, err, "cannot determine branch due to missing CIRCLE_BRANCH and CIRCLE_TAG environment variables") t.Setenv("CIRCLE_BRANCH", "main") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -48,7 +49,7 @@ func Test_circleCIOrb_GetTriggerContext(t *testing.T) { })) defer server.Close() t.Setenv("GITHUB_API_URL", server.URL) - ctx, err = orb.GetTriggerContext() + ctx, err = orb.GetTriggerContext(context.Background()) require.NoError(t, err) require.Equal(t, &atlasaction.PullRequest{ Number: 1, diff --git a/atlasaction/gh_action.go b/atlasaction/gh_action.go index 449c975d..09a22867 100644 --- a/atlasaction/gh_action.go +++ b/atlasaction/gh_action.go @@ -5,9 +5,14 @@ package atlasaction import ( + "bytes" + "context" + "encoding/json" "fmt" "io" "net/http" + "slices" + "strings" "time" "golang.org/x/oauth2" @@ -17,22 +22,10 @@ import ( "github.com/sethvargo/go-githubactions" ) -const defaultGHApiUrl = "https://api.github.com" - -var _ Action = (*ghAction)(nil) - -type ( - // ghAction is an implementation of the Action interface for GitHub Actions. - ghAction struct { - *githubactions.Action - } - // ghAPI is an implementation of the SCMClient interface for GitHub Actions. - githubAPI struct { - baseURL string - repo string - client *http.Client - } -) +// ghAction is an implementation of the Action interface for GitHub Actions. +type ghAction struct { + *githubactions.Action +} // NewGHAction returns a new Action for GitHub Actions. func NewGHAction(getenv func(string) string, w io.Writer) *ghAction { @@ -44,13 +37,57 @@ func NewGHAction(getenv func(string) string, w io.Writer) *ghAction { } } +// MigrateApply implements Reporter. +func (a *ghAction) MigrateApply(_ context.Context, r *atlasexec.MigrateApply) { + summary, err := RenderTemplate("migrate-apply.tmpl", r) + if err != nil { + a.Errorf("failed to create summary: %v", err) + return + } + a.AddStepSummary(summary) +} + +// MigrateLint implements Reporter. +func (a *ghAction) MigrateLint(_ context.Context, r *atlasexec.SummaryReport) { + summary, err := RenderTemplate("migrate-lint.tmpl", r) + if err != nil { + a.Errorf("failed to create summary: %v", err) + return + } + a.AddStepSummary(summary) +} + +// SchemaApply implements Reporter. +func (a *ghAction) SchemaApply(_ context.Context, r *atlasexec.SchemaApply) { + summary, err := RenderTemplate("schema-apply.tmpl", r) + if err != nil { + a.Errorf("failed to create summary: %v", err) + return + } + a.AddStepSummary(summary) +} + +// SchemaPlan implements Reporter. +func (a *ghAction) SchemaPlan(_ context.Context, r *atlasexec.SchemaPlan) { + summary, err := RenderTemplate("schema-plan.tmpl", map[string]any{ + "Plan": r, + "EnvName": a.GetInput("env"), + "RerunCommand": fmt.Sprintf("gh run rerun %s", a.Getenv("GITHUB_RUN_ID")), + }) + if err != nil { + a.Errorf("failed to create summary: %v", err) + return + } + a.AddStepSummary(summary) +} + // GetType implements the Action interface. func (a *ghAction) GetType() atlasexec.TriggerType { return atlasexec.TriggerTypeGithubAction } // GetTriggerContext returns the context of the action. -func (a *ghAction) GetTriggerContext() (*TriggerContext, error) { +func (a *ghAction) GetTriggerContext(context.Context) (*TriggerContext, error) { ctx, err := a.Action.Context() if err != nil { return nil, err @@ -83,9 +120,22 @@ func (a *ghAction) GetTriggerContext() (*TriggerContext, error) { return tc, nil } +// WithFieldsMap return a new Logger with the given fields. +func (a *ghAction) WithFieldsMap(m map[string]string) Logger { + return &ghAction{a.Action.WithFieldsMap(m)} +} + +var _ Action = (*ghAction)(nil) +var _ Reporter = (*ghAction)(nil) + +const defaultGHApiUrl = "https://api.github.com" + // githubClient returns a new GitHub client for the given repository. // If the GITHUB_TOKEN is set, it will be used for authentication. func githubClient(repo, baseURL string, token string) *githubAPI { + if baseURL == "" { + baseURL = defaultGHApiUrl + } httpClient := &http.Client{Timeout: time.Second * 30} if token != "" { httpClient.Transport = &oauth2.Transport{ @@ -95,9 +145,6 @@ func githubClient(repo, baseURL string, token string) *githubAPI { }), } } - if baseURL == "" { - baseURL = defaultGHApiUrl - } return &githubAPI{ baseURL: baseURL, repo: repo, @@ -105,9 +152,302 @@ func githubClient(repo, baseURL string, token string) *githubAPI { } } -// WithFieldsMap return a new Logger with the given fields. -func (a *ghAction) WithFieldsMap(m map[string]string) Logger { - return &ghAction{a.Action.WithFieldsMap(m)} +type ( + // ghAPI is an implementation of the SCMClient interface for GitHub Actions. + githubAPI struct { + baseURL string + repo string + client *http.Client + } + 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 } // githubTriggerEvent is the structure of the GitHub trigger event. diff --git a/atlasaction/gitlab_ci.go b/atlasaction/gitlab_ci.go index b4328893..04bc387d 100644 --- a/atlasaction/gitlab_ci.go +++ b/atlasaction/gitlab_ci.go @@ -53,7 +53,7 @@ func (g *gitlabCI) SetOutput(name, value string) { } // GetTriggerContext implements the Action interface. -func (g *gitlabCI) GetTriggerContext() (*TriggerContext, error) { +func (g *gitlabCI) GetTriggerContext(context.Context) (*TriggerContext, error) { ctx := &TriggerContext{ SCM: SCM{ Type: atlasexec.SCMTypeGitlab, @@ -80,18 +80,12 @@ func (g *gitlabCI) GetTriggerContext() (*TriggerContext, error) { return ctx, nil } -// AddStepSummary implements the Action interface. -func (g *gitlabCI) AddStepSummary(summary string) { - // unsupported -} - type gitlabTransport struct { Token string } func (t *gitlabTransport) RoundTrip(req *http.Request) (*http.Response, error) { req.Header.Set("PRIVATE-TOKEN", t.Token) - req.Header.Set("Content-Type", "application/json") return http.DefaultTransport.RoundTrip(req) } @@ -127,6 +121,7 @@ func (g *gitlabAPI) UpsertComment(ctx context.Context, pr *PullRequest, id, comm if err != nil { return err } + req.Header.Set("Content-Type", "application/json") res, err := g.client.Do(req) if err != nil { return fmt.Errorf("error querying gitlab comments with %v/%v, %w", g.project, pr.Number, err) @@ -162,6 +157,7 @@ func (g *gitlabAPI) createComment(ctx context.Context, pr *PullRequest, comment if err != nil { return err } + req.Header.Set("Content-Type", "application/json") res, err := g.client.Do(req) if err != nil { return err @@ -184,6 +180,7 @@ func (g *gitlabAPI) updateComment(ctx context.Context, pr *PullRequest, NoteId i if err != nil { return err } + req.Header.Set("Content-Type", "application/json") res, err := g.client.Do(req) if err != nil { return err