diff --git a/api/step/plan.go b/api/step/plan.go index 02f5546f0..bfacc5136 100644 --- a/api/step/plan.go +++ b/api/step/plan.go @@ -64,17 +64,18 @@ func planStep(ctx context.Context, database database.Interface, scm scm.Service, s.SetReportAs(c.ReportAs) s.SetCreated(time.Now().UTC().Unix()) - id, err := scm.CreateChecks(ctx, r, b.GetCommit(), s.GetName()) - if err != nil { - // TODO: make this error more meaningful - return nil, err - } + if c.ReportStatus { + id, err := scm.CreateChecks(ctx, r, b.GetCommit(), s.GetName(), b.GetEvent()) + if err != nil { + // TODO: make this error more meaningful + return nil, err + } - // TODO: have to store the check ID somewhere - s.SetCheckID(id) + s.SetCheckID(id) + } // send API call to create the step - s, err = database.CreateStep(ctx, s) + s, err := database.CreateStep(ctx, s) if err != nil { return nil, fmt.Errorf("unable to create step %s: %w", s.GetName(), err) } diff --git a/api/step/update.go b/api/step/update.go index dca493625..7d3ebcddd 100644 --- a/api/step/update.go +++ b/api/step/update.go @@ -154,13 +154,17 @@ func UpdateStep(c *gin.Context) { return } - err = scm.FromContext(c).UpdateChecks(ctx, r, s, b.GetCommit()) - if err != nil { - retErr := fmt.Errorf("unable to set step check %s: %w", entry, err) + if s.GetCheckID() != 0 { + s.SetReport(input.GetReport()) - util.HandleError(c, http.StatusInternalServerError, retErr) + err = scm.FromContext(c).UpdateChecks(ctx, r, s, b.GetCommit(), b.GetEvent()) + if err != nil { + retErr := fmt.Errorf("unable to set step check %s: %w", entry, err) - return + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } } c.JSON(http.StatusOK, s) diff --git a/api/types/repo.go b/api/types/repo.go index bfad93623..01e9f03ea 100644 --- a/api/types/repo.go +++ b/api/types/repo.go @@ -32,6 +32,7 @@ type Repo struct { PipelineType *string `json:"pipeline_type,omitempty"` PreviousName *string `json:"previous_name,omitempty"` ApproveBuild *string `json:"approve_build,omitempty"` + InstallID *int64 `json:"install_id,omitempty"` } // Environment returns a list of environment variables @@ -345,6 +346,19 @@ func (r *Repo) GetApproveBuild() string { return *r.ApproveBuild } +// GetInstallID returns the InstallID field. +// +// When the provided Repo type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *Repo) GetInstallID() int64 { + // return zero value if Repo type or InstallID field is nil + if r == nil || r.InstallID == nil { + return 0 + } + + return *r.InstallID +} + // SetID sets the ID field. // // When the provided Repo type is nil, it @@ -618,6 +632,19 @@ func (r *Repo) SetApproveBuild(v string) { r.ApproveBuild = &v } +// SetInstallID sets the InstallID field. +// +// When the provided Repo type is nil, it +// will set nothing and immediately return. +func (r *Repo) SetInstallID(v int64) { + // return if Repo type is nil + if r == nil { + return + } + + r.InstallID = &v +} + // String implements the Stringer interface for the Repo type. func (r *Repo) String() string { return fmt.Sprintf(`{ diff --git a/database/repo/table.go b/database/repo/table.go index 65bd21fb2..ec033f48b 100644 --- a/database/repo/table.go +++ b/database/repo/table.go @@ -35,6 +35,7 @@ repos ( pipeline_type TEXT, previous_name VARCHAR(100), approve_build VARCHAR(20), + install_id INTEGER, UNIQUE(full_name) ); ` @@ -65,6 +66,7 @@ repos ( pipeline_type TEXT, previous_name TEXT, approve_build TEXT, + install_id INTEGER, UNIQUE(full_name) ); ` diff --git a/database/step/table.go b/database/step/table.go index a7cd7eb74..900734de4 100644 --- a/database/step/table.go +++ b/database/step/table.go @@ -30,6 +30,7 @@ steps ( host VARCHAR(250), runtime VARCHAR(250), distribution VARCHAR(250), + check_id INTEGER, report_as VARCHAR(250), UNIQUE(build_id, number) ); @@ -56,6 +57,7 @@ steps ( host TEXT, runtime TEXT, distribution TEXT, + check_id INTEGER, report_as TEXT, UNIQUE(build_id, number) ); diff --git a/scm/github/github.go b/scm/github/github.go index e33cb5711..5db5f45b2 100644 --- a/scm/github/github.go +++ b/scm/github/github.go @@ -4,6 +4,9 @@ package github import ( "context" + "crypto/x509" + "encoding/base64" + "encoding/pem" "fmt" "net/http" "net/url" @@ -118,11 +121,24 @@ func New(opts ...ClientOpt) (*client, error) { if c.config.GithubAppID != 0 && len(c.config.GithubAppPrivateKey) > 0 { c.Logger.Infof("sourcing private key from path: %s", c.config.GithubAppPrivateKey) - transport, err := ghinstallation.NewAppsTransportKeyFromFile(http.DefaultTransport, c.config.GithubAppID, c.config.GithubAppPrivateKey) + + decodedPEM, err := base64.StdEncoding.DecodeString(c.config.GithubAppPrivateKey) if err != nil { - return nil, err + return nil, fmt.Errorf("error decoding base64: %w", err) + } + + block, _ := pem.Decode(decodedPEM) + if block == nil { + return nil, fmt.Errorf("failed to parse PEM block containing the key") } + privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse RSA private key: %w", err) + } + + transport := ghinstallation.NewAppsTransportFromPrivateKey(http.DefaultTransport, c.config.GithubAppID, privateKey) + transport.BaseURL = c.config.API c.AppsTransport = transport } @@ -184,17 +200,28 @@ func (c *client) newClientToken(token string) *github.Client { } // helper function to return the GitHub App token. -func (c *client) newGithubAppToken(r *api.Repo) *github.Client { +func (c *client) newGithubAppToken(r *api.Repo) (*github.Client, error) { // create a github client based off the existing GitHub App configuration client, err := github.NewClient(&http.Client{Transport: c.AppsTransport}).WithEnterpriseURLs(c.config.API, c.config.API) if err != nil { - panic(err) + return nil, err + } + + // if repo has an install ID, use it to create an installation token + if r.GetInstallID() != 0 { + // create installation token for the repo + t, _, err := client.Apps.CreateInstallationToken(context.Background(), r.GetInstallID(), &github.InstallationTokenOptions{}) + if err != nil { + panic(err) + } + + return c.newClientToken(t.GetToken()), nil } // list all installations (a.k.a. orgs) where the GitHub App is installed installations, _, err := client.Apps.ListInstallations(context.Background(), &github.ListOptions{}) if err != nil { - panic(err) + return nil, err } var id int64 @@ -208,7 +235,7 @@ func (c *client) newGithubAppToken(r *api.Repo) *github.Client { // failsafe in case the repo does not belong to an org where the GitHub App is installed if id == 0 { - panic(err) + return nil, err } // create installation token for the repo @@ -217,5 +244,5 @@ func (c *client) newGithubAppToken(r *api.Repo) *github.Client { panic(err) } - return c.newClientToken(t.GetToken()) + return c.newClientToken(t.GetToken()), nil } diff --git a/scm/github/repo.go b/scm/github/repo.go index c80968390..daf59cc88 100644 --- a/scm/github/repo.go +++ b/scm/github/repo.go @@ -662,12 +662,15 @@ func (c *client) GetBranch(ctx context.Context, r *api.Repo, branch string) (str } // CreateChecks defines a function that does stuff... -func (c *client) CreateChecks(ctx context.Context, r *api.Repo, commit, step string) (int64, error) { +func (c *client) CreateChecks(ctx context.Context, r *api.Repo, commit, step, event string) (int64, error) { // create client from GitHub App - client := c.newGithubAppToken(r) + client, err := c.newGithubAppToken(r) + if err != nil { + return 0, err + } opts := github.CreateCheckRunOptions{ - Name: fmt.Sprintf("vela-%s-%s", commit, step), + Name: fmt.Sprintf("vela-%s-%s", event, step), HeadSHA: commit, } @@ -680,9 +683,12 @@ func (c *client) CreateChecks(ctx context.Context, r *api.Repo, commit, step str } // UpdateChecks defines a function that does stuff... -func (c *client) UpdateChecks(ctx context.Context, r *api.Repo, s *library.Step, commit string) error { +func (c *client) UpdateChecks(ctx context.Context, r *api.Repo, s *library.Step, commit, event string) error { // create client from GitHub App - client := c.newGithubAppToken(r) + client, err := c.newGithubAppToken(r) + if err != nil { + return err + } var ( conclusion string @@ -719,13 +725,41 @@ func (c *client) UpdateChecks(ctx context.Context, r *api.Repo, s *library.Step, status = "completed" } + var annotations []*github.CheckRunAnnotation + + for _, reportAnnotation := range s.GetReport().GetAnnotations() { + annotation := &github.CheckRunAnnotation{ + Path: github.String(reportAnnotation.GetPath()), + StartLine: github.Int(reportAnnotation.GetStartLine()), + EndLine: github.Int(reportAnnotation.GetEndLine()), + StartColumn: github.Int(reportAnnotation.GetStartColumn()), + EndColumn: github.Int(reportAnnotation.GetEndColumn()), + AnnotationLevel: github.String(reportAnnotation.GetAnnotationLevel()), + Message: github.String(reportAnnotation.GetMessage()), + Title: github.String(reportAnnotation.GetTitle()), + RawDetails: github.String(reportAnnotation.GetRawDetails()), + } + + annotations = append(annotations, annotation) + } + + output := &github.CheckRunOutput{ + Title: github.String(s.GetReport().GetTitle()), + Summary: github.String(s.GetReport().GetSummary()), + Text: github.String(s.GetReport().GetText()), + AnnotationsCount: github.Int(s.GetReport().GetAnnotationsCount()), + AnnotationsURL: github.String(s.GetReport().GetAnnotationsURL()), + Annotations: annotations, + } + opts := github.UpdateCheckRunOptions{ - Name: fmt.Sprintf("vela-%s-%s", commit, s.GetName()), + Name: fmt.Sprintf("vela-%s-%s", event, s.GetName()), Conclusion: github.String(conclusion), Status: github.String(status), + Output: output, } - _, _, err := client.Checks.UpdateCheckRun(ctx, r.GetOrg(), r.GetName(), s.GetCheckID(), opts) + _, _, err = client.Checks.UpdateCheckRun(ctx, r.GetOrg(), r.GetName(), s.GetCheckID(), opts) if err != nil { return err } diff --git a/scm/service.go b/scm/service.go index 08382eb98..e36595de0 100644 --- a/scm/service.go +++ b/scm/service.go @@ -143,8 +143,8 @@ type Service interface { GetHTMLURL(context.Context, *api.User, string, string, string, string) (string, error) // TODO: add comments - CreateChecks(context.Context, *api.Repo, string, string) (int64, error) - UpdateChecks(context.Context, *api.Repo, *library.Step, string) error + CreateChecks(context.Context, *api.Repo, string, string, string) (int64, error) + UpdateChecks(context.Context, *api.Repo, *library.Step, string, string) error // Webhook SCM Interface Functions