Skip to content

Commit

Permalink
atlasaction: report actions result via the Reports-API on Bitbucket
Browse files Browse the repository at this point in the history
  • Loading branch information
giautm committed Dec 19, 2024
1 parent 47c2e51 commit 755b9ea
Show file tree
Hide file tree
Showing 3 changed files with 630 additions and 0 deletions.
6 changes: 6 additions & 0 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
239 changes: 239 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,238 @@ 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
}
if issues := filterIssues(r.Steps); len(issues) > 0 {
stepSummary := func(s *atlasexec.StepReport) string {
if s.Text == "" {
return s.Name
}
return fmt.Sprintf("%s: %s", s.Name, s.Text)
}
annos := make([]bitbucket.ReportAnnotation, 0, len(issues))
for _, s := range issues {
severity := bitbucket.SeverityMedium
if stepIsError(s) {
severity = bitbucket.SeverityHigh
}
if s.Result == nil {
anno := bitbucket.ReportAnnotation{
Result: bitbucket.ResultFailed,
Summary: stepSummary(s),
Details: s.Error,
Severity: severity,
}
anno.ExternalID, err = hash(cr.ExternalID, s.Name, s.Text)
if err != nil {
a.Errorf("failed to generate external ID: %v", err)
return
}
annos = append(annos, anno)
} else {
for _, rr := range s.Result.Reports {
for _, d := range rr.Diagnostics {
anno := bitbucket.ReportAnnotation{
Result: bitbucket.ResultFailed,
Summary: stepSummary(s),
Details: fmt.Sprintf("%s: %s", rr.Text, d.Text),
Severity: severity,
Path: "", // TODO: add path
Line: 0, // TODO: add line
}
switch {
case d.Code != "":
anno.Details += fmt.Sprintf(" (%s)", d.Code)
anno.AnnotationType = bitbucket.AnnotationTypeBug
anno.Link = fmt.Sprintf("https://atlasgo.io/lint/analyzers#%s", d.Code)
case len(d.SuggestedFixes) != 0:
anno.AnnotationType = bitbucket.AnnotationTypeCodeSmell
// TODO: Add suggested fixes.
default:
anno.AnnotationType = bitbucket.AnnotationTypeVulnerability
}
anno.ExternalID, err = hash(cr.ExternalID, s.Name, s.Text, rr.Text, d.Text)
if err != nil {
a.Errorf("failed to generate external ID: %v", err)
return
}
annos = append(annos, anno)
}
}
}
}
_, err = c.CreateReportAnnotations(ctx, commitID, cr.ExternalID, annos)
if err != nil {
a.Errorf("failed to create commit report annotations: %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) {
if l := r.Lint; l != nil {
a.MigrateLint(ctx, l)
}
}

// 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/md", r)
if err != nil {
return err
}
return c.upsertComment(ctx, tc.PullRequest.Number, 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/md", p)
if err != nil {
return err
}
return c.upsertComment(ctx, tc.PullRequest.Number, p.File.Name, comment)
}

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

// 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 755b9ea

Please sign in to comment.