Skip to content

Commit

Permalink
atlasaction: added bitbucket client for SCM
Browse files Browse the repository at this point in the history
  • Loading branch information
giautm committed Dec 17, 2024
1 parent 86db755 commit 380fde7
Show file tree
Hide file tree
Showing 3 changed files with 553 additions and 16 deletions.
42 changes: 26 additions & 16 deletions atlasaction/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,12 @@ func (tc *TriggerContext) SCMClient() (SCMClient, error) {
tc.Act.Warningf("GITLAB_TOKEN is not set, the action may not have all the permissions")
}
return gitlabClient(tc.Act.Getenv("CI_PROJECT_ID"), tc.SCM.APIURL, token), nil
case atlasexec.SCMTypeBitbucket:
return BitbucketClient(
tc.Act.Getenv("BITBUCKET_WORKSPACE"),
tc.Act.Getenv("BITBUCKET_REPO_SLUG"),
tc.Act.Getenv("BITBUCKET_ACCESS_TOKEN"),
)
default:
return nil, ErrNoSCM // Not implemented yet.
}
Expand Down Expand Up @@ -1086,6 +1092,24 @@ func appliedStmts(a *atlasexec.MigrateApply) int {
return total
}

func filterIssues(steps []*atlasexec.StepReport) []*atlasexec.StepReport {
result := make([]*atlasexec.StepReport, 0, len(steps))
for _, s := range steps {
switch {
case s.Error != "":
result = append(result, s)
case s.Result == nil: // No result.
case s.Result.Error != "" || len(s.Result.Reports) > 0:
result = append(result, s)
}
}
return result
}

func stepIsError(s *atlasexec.StepReport) bool {
return s.Error != "" || (s.Result != nil && s.Result.Error != "")
}

var (
//go:embed comments/*.tmpl
comments embed.FS
Expand All @@ -1094,22 +1118,8 @@ var (
Funcs(template.FuncMap{
"execTime": execTime,
"appliedStmts": appliedStmts,
"filterIssues": func(steps []*atlasexec.StepReport) []*atlasexec.StepReport {
result := make([]*atlasexec.StepReport, 0, len(steps))
for _, s := range steps {
switch {
case s.Error != "":
result = append(result, s)
case s.Result == nil: // No result.
case s.Result.Error != "" || len(s.Result.Reports) > 0:
result = append(result, s)
}
}
return result
},
"stepIsError": func(s *atlasexec.StepReport) bool {
return s.Error != "" || (s.Result != nil && s.Result.Error != "")
},
"filterIssues": filterIssues,
"stepIsError": stepIsError,
"firstUpper": func(s string) string {
if s == "" {
return ""
Expand Down
186 changes: 186 additions & 0 deletions atlasaction/bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ package atlasaction

import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"testing"

"ariga.io/atlas-action/atlasaction/internal/bitbucket"
"ariga.io/atlas-go-sdk/atlasexec"
"github.com/fatih/color"
"golang.org/x/oauth2"
)

type bbPipe struct {
Expand Down Expand Up @@ -107,4 +112,185 @@ func (a *bbPipe) SetOutput(name, value string) {
}
}

// MigrateApply implements Reporter.
func (a *bbPipe) MigrateApply(context.Context, *atlasexec.MigrateApply) {
}

// MigrateLint implements Reporter.
func (a *bbPipe) MigrateLint(ctx context.Context, r *atlasexec.SummaryReport) {
c, err := a.reportClient()
if err != nil {
a.Errorf("failed to create bitbucket client: %v", err)
return
}
commitID := a.getenv("BITBUCKET_COMMIT")
cr, err := LintReport(commitID, r)
if err != nil {
a.Errorf("failed to generate commit report: %v", err)
return
}
if err = c.CreateReport(ctx, commitID, cr); err != nil {
a.Errorf("failed to create commit report: %v", err)
return
}
}

// SchemaApply implements Reporter.
func (a *bbPipe) SchemaApply(context.Context, *atlasexec.SchemaApply) {
}

// SchemaPlan implements Reporter.
func (a *bbPipe) SchemaPlan(ctx context.Context, r *atlasexec.SchemaPlan) {
c, err := a.reportClient()
if err != nil {
a.Errorf("failed to create bitbucket client: %v", err)
return
}
commitID := a.getenv("BITBUCKET_COMMIT")
cr, err := LintReport(commitID, r.Lint)
if err != nil {
a.Errorf("failed to generate commit report: %v", err)
return
}
if err = c.CreateReport(ctx, commitID, cr); err != nil {
a.Errorf("failed to create commit report: %v", err)
return
}
}

// reportClient returns a new Bitbucket client,
// This client only works with the Reports-API.
func (a *bbPipe) reportClient() (*bitbucket.Client, error) {
return bitbucket.NewClient(
a.getenv("BITBUCKET_WORKSPACE"),
a.getenv("BITBUCKET_REPO_SLUG"),
// Proxy the request through the Docker host.
// It allows the pipe submit the report without extra authentication.
//
// https://support.atlassian.com/bitbucket-cloud/docs/code-insights/#Authentication
bitbucket.WithProxy(func() (u *url.URL, err error) {
u = &url.URL{}
if h := a.getenv("DOCKER_HOST"); h != "" {
if u, err = url.Parse(h); err != nil {
return nil, err
}
}
u.Scheme = "http"
u.Host = fmt.Sprintf("%s:29418", u.Hostname())
return u, nil
}),
)
}

// We use our docker image name as the reporter.
// This is used to identify the source of the report.
const bitbucketReporter = "arigaio/atlas-action"

// LintReport generates a commit report for the given commit ID
func LintReport(commit string, r *atlasexec.SummaryReport) (*bitbucket.CommitReport, error) {
// We need ensure the report is unique on Bitbucket.
// So we hash the commit ID and the current schema.
// This way, we can identify the report for a specific commit, and schema state.
externalID, err := hash(commit, r.Schema.Current)
if err != nil {
return nil, fmt.Errorf("bitbucket: failed to generate external ID: %w", err)
}
cr := &bitbucket.CommitReport{
ExternalID: externalID,
Reporter: bitbucketReporter,
ReportType: bitbucket.ReportTypeSecurity,
Title: "Atlas Lint",
Link: r.URL,
LogoURL: "https://ariga.io/assets/favicon.ico",
}
if issues := len(filterIssues(r.Steps)); issues > 0 {
cr.Details = fmt.Sprintf("Found %d issues.", issues)
cr.Result = bitbucket.ResultFailed
steps := len(r.Steps)
cr.AddPercentage("Health Score", float64(steps-issues)/float64(steps)*100)
} else {
cr.Details = "No issues found."
cr.Result = bitbucket.ResultPassed
cr.AddPercentage("Health Score", 100)
}
cr.AddNumber("Diagnostics", int64(r.DiagnosticsCount()))
cr.AddNumber("Files", int64(len(r.Files)))
if d := r.Env.Dir; d != "" {
cr.AddText("Working Directory", d)
}
if r.URL != "" {
u, err := url.Parse(r.URL)
if err != nil {
return nil, fmt.Errorf("bitbucket: failed to parse URL: %w", err)
}
u.Fragment = "erd"
cr.AddLink("ERD", "View Visualization", u)
}
return cr, nil
}

type bbClient struct {
*bitbucket.Client
}

// BitbucketClient returns a new Bitbucket client that implements SCMClient.
func BitbucketClient(workspace, repoSlug, token string) (*bbClient, error) {
c, err := bitbucket.NewClient(
workspace, repoSlug,
bitbucket.WithToken(&oauth2.Token{AccessToken: token}),
)
if err != nil {
return nil, err
}
return &bbClient{Client: c}, nil
}

// CommentLint implements SCMClient.
func (c *bbClient) CommentLint(ctx context.Context, tc *TriggerContext, r *atlasexec.SummaryReport) error {
comment, err := RenderTemplate("migrate-lint.tmpl", r)
if err != nil {
return err
}
return c.comment(ctx, tc.PullRequest, tc.Act.GetInput("dir-name"), comment)
}

// CommentPlan implements SCMClient.
func (c *bbClient) CommentPlan(ctx context.Context, tc *TriggerContext, p *atlasexec.SchemaPlan) error {
comment, err := RenderTemplate("schema-plan.tmpl", p)
if err != nil {
return err
}
return c.comment(ctx, tc.PullRequest, p.File.Name, comment)
}

func (c *bbClient) comment(ctx context.Context, pr *PullRequest, id, comment string) error {
comments, err := c.PullRequestComments(ctx, pr.Number)
if err != nil {
return err
}
marker := commentMarker(id)
bc := &bitbucket.Comment{Content: bitbucket.Content{Raw: comment + "\n\n" + marker}}
if found := slices.IndexFunc(comments, func(c bitbucket.Comment) bool {
return strings.Contains(c.Content.Raw, marker)
}); found != -1 {
bc.ID = comments[found].ID
return c.PullRequestUpdateComment(ctx, pr.Number, bc)
}
return c.PullRequestCreateComment(ctx, pr.Number, bc)
}

// hash returns the SHA-256 hash of the parts.
// The hash is encoded using base64.RawURLEncoding.
func hash(parts ...string) (string, error) {
h := sha256.New()
for _, p := range parts {
if _, err := h.Write([]byte(p)); err != nil {
return "", err
}
}
return base64.URLEncoding.EncodeToString(h.Sum(nil)), nil
}

var _ Action = (*bbPipe)(nil)
var _ Reporter = (*bbPipe)(nil)
var _ SCMClient = (*bbClient)(nil)
Loading

0 comments on commit 380fde7

Please sign in to comment.