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 15, 2024
1 parent 3993e3a commit f81fdf5
Show file tree
Hide file tree
Showing 3 changed files with 492 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 @@ -1098,6 +1098,12 @@ func (a *Actions) SCM(tc *TriggerContext) (SCMClient, error) {
a.Warningf("GITLAB_TOKEN is not set, the action may not have all the permissions")
}
return gitlabClient(a.Getenv("CI_PROJECT_ID"), tc.SCM.APIURL, token), nil
case atlasexec.SCMTypeBitbucket:
return BitbucketClient(
a.Getenv("BITBUCKET_WORKSPACE"),
a.Getenv("BITBUCKET_REPO_SLUG"),
a.Getenv("BITBUCKET_ACCESS_TOKEN"),
)
default:
return nil, ErrNoSCM // Not implemented yet.
}
Expand Down Expand Up @@ -1243,6 +1249,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 @@ -1251,22 +1275,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
145 changes: 145 additions & 0 deletions atlasaction/bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,34 @@ 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"
)

// proxyURL is the URL to the proxy server.
// This is used to authenticate the Bitbucket client,
// then the pipe is run in the Docker container.
//
// https://support.atlassian.com/bitbucket-cloud/docs/code-insights/#Authentication
var proxyURL = &url.URL{
Scheme: "http",
Host: "host.docker.internal:29418",
}

type bbPipe struct {
*coloredLogger
getenv func(string) string
Expand Down Expand Up @@ -101,4 +116,134 @@ 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) {
commitID := a.getenv("BITBUCKET_COMMIT")
cr, err := MigrateLintReport(commitID, r)
if err != nil {
a.Errorf("failed to generate commit report: %v", err)
return
}
c, err := bitbucket.NewClient(
a.getenv("BITBUCKET_WORKSPACE"),
a.getenv("BITBUCKET_REPO_SLUG"),
bitbucket.WithProxyAuthenticate(proxyURL),
)
if err != nil {
a.Errorf("failed to create bitbucket client: %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(_ context.Context, r *atlasexec.SchemaPlan) {
commitID := a.getenv("BITBUCKET_COMMIT")
cr, err := MigrateLintReport(commitID, r.Lint)
if err != nil {
a.Errorf("failed to generate commit report: %v", err)
return
}
_ = cr
}

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

// MigrateLintReport generates a commit report for the given commit ID
func MigrateLintReport(commit string, r *atlasexec.SummaryReport) (*bitbucket.CommitReport, error) {
externalID, err := hash(commit, r.Env.Dir, r.Env.URL.Redacted())
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 Migrate Lint",
Link: r.URL,
LogoURL: "https://ariga.io/assets/favicon.ico",
// Build the report.
Details: "",
Result: bitbucket.ResultPassed,
}
cr.AddText("Working Directory", r.Env.Dir)
cr.AddNumber("Diagnostics", int64(r.DiagnosticsCount()))
cr.AddNumber("Files", int64(len(r.Files)))
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)
}
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))
} else {
cr.Details = "No issues found."
cr.Result = bitbucket.ResultPassed
cr.AddPercentage("Health Score", 1)
}
return cr, nil
}

type bbClient struct {
*bitbucket.Client
}

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
}

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

// 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 f81fdf5

Please sign in to comment.