// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package github import ( "context" "errors" "fmt" "slices" "strings" libgithub "github.com/google/go-github/v74/github" "github.com/jedib0t/go-pretty/v6/table" ) // CheckCommitStatusReq is a request to list workflows runs. The fields represent // various criteria we can use to filter. type CheckCommitStatusReq struct { Owner string Repo string Commit string PR int Context string Creator string State string } // CheckCommitStatusRes is a list workflows response. type CheckCommitStatusRes struct { CheckSuccessful bool `json:"check_success,omitempty"` ExpectedContext string `json:"expected_context,omitempty"` ExpectedState string `json:"expected_state,omitempty"` ExpectedCreator string `json:"expected_creator,omitempty"` Statuses []*CheckCommitStatus `json:"statuses,omitempty"` } type CheckCommitStatus struct { Status *libgithub.RepoStatus `json:"statuses,omitempty"` CheckSuccess bool `json:"success,omitempty"` } // String returns the response as a string func (r *CheckCommitStatusRes) String() string { b := strings.Builder{} b.WriteString(fmt.Sprintf("success:%t", r.CheckSuccessful)) b.WriteString(" context:" + r.ExpectedContext) b.WriteString(" state:" + r.ExpectedState) b.WriteString(" creator:" + r.ExpectedCreator) return b.String() } // Run runs the request to gather all instances of the workflow that match // our filter criteria. func (r *CheckCommitStatusReq) Run(ctx context.Context, client *libgithub.Client) (*CheckCommitStatusRes, error) { var err error res := &CheckCommitStatusRes{ CheckSuccessful: false, ExpectedCreator: r.Creator, ExpectedState: r.State, ExpectedContext: r.Context, Statuses: []*CheckCommitStatus{}, } if err = r.validate(); err != nil { return nil, fmt.Errorf("validating request: %w", err) } statusesReq := &ListCommitStatusesReq{ Owner: r.Owner, Repo: r.Repo, Commit: r.Commit, PR: r.PR, } statuses, err := statusesReq.Run(ctx, client) if err != nil { return nil, err } for _, status := range statuses.Statuses { if status.GetContext() != r.Context { continue } if r.Creator != "" && status.GetCreator().GetLogin() != r.Creator { continue } // There can be many statuses with the same context and creator. Keep track // of them all but only update our success if we get a match. res.Statuses = append(res.Statuses, &CheckCommitStatus{ Status: status, CheckSuccess: status.GetState() == r.State, }) if status.GetState() == r.State { res.CheckSuccessful = true } } return res, nil } // validate ensures that we've been given the minimum filter arguments necessary to complete a // request. It is always recommended that additional fitlers be given to reduce the response size // and not exhaust API limits. func (r *CheckCommitStatusReq) validate() error { if r == nil { return errors.New("failed to initialize request") } if r.Owner == "" { return errors.New("no github organization has been provided") } if r.Repo == "" { return errors.New("no github repository has been provided") } if r.Context == "" { return errors.New("no status context has been provided") } if r.PR == 0 && r.Commit == "" { return errors.New("no commit or Pull Request number has been provided") } allowedStates := []string{"error", "failure", "pending", "success"} if !slices.Contains(allowedStates, r.State) { return fmt.Errorf("invalid state, got: %s, expected one of: %v+", r.State, allowedStates) } return nil } // ToTable marshals the response to a text table. func (r *CheckCommitStatusRes) ToTable() table.Writer { t := table.NewWriter() t.Style().Options.DrawBorder = false t.Style().Options.SeparateColumns = false t.Style().Options.SeparateFooter = false t.Style().Options.SeparateHeader = false t.Style().Options.SeparateRows = false t.AppendHeader(table.Row{"context", "creator", "date", "state", "check success"}) for _, status := range r.Statuses { t.AppendRow(table.Row{ status.Status.GetContext(), status.Status.GetCreator().GetLogin(), status.Status.GetUpdatedAt(), status.Status.GetState(), status.CheckSuccess, }) } t.SuppressEmptyColumns() t.SuppressTrailingSpaces() return t }