action/github/github.go
Rama main · réplica de lectura del repositorio original.
package github
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"github.com/caarlos0/env/v11"
"github.com/pkg/errors"
"github.com/bytebase/bytebase/action/common"
v1pb "github.com/bytebase/bytebase/backend/generated-go/v1"
)
const maxCommentLength = 65536
const commentHeader = `<!--BYTEBASE_MARKER-DO_NOT_EDIT-->`
const githubActionUserID = 41898282
type githubEnv struct {
APIUrl string `env:"GITHUB_API_URL,required,notEmpty"`
Repo string `env:"GITHUB_REPOSITORY,required,notEmpty"`
Token string `env:"GITHUB_TOKEN,required,notEmpty"`
EventName string `env:"GITHUB_EVENT_NAME,required,notEmpty"`
EventPath string `env:"GITHUB_EVENT_PATH,required,notEmpty"`
}
func CreateCommentAndAnnotation(resp *v1pb.CheckReleaseResponse) error {
// Write annotations to the pull request.
if err := writeAnnotations(resp); err != nil {
return err
}
ghe, err := env.ParseAs[githubEnv]()
if err != nil {
return errors.Wrap(err, "failed to parse GitHub environment variables")
}
// Upsert a comment on the pull request with the check results.
if err := upsertComment(resp, &ghe); err != nil {
fmt.Printf("failed to upsert comment on the pull request: %v\n", err)
return nil
}
return nil
}
func getPRNumberFromEventFile(eventPath string) (string, error) {
eventFile, err := os.ReadFile(eventPath)
if err != nil {
return "", errors.Wrapf(err, "failed to read event file %s", eventPath)
}
var event struct {
Number int64 `json:"number"`
}
if err := json.Unmarshal(eventFile, &event); err != nil {
return "", errors.Wrapf(err, "failed to unmarshal event file %s", eventPath)
}
if event.Number == 0 {
return "", errors.New("no pull request number found in the event file")
}
return fmt.Sprintf("%d", event.Number), nil
}
func upsertComment(resp *v1pb.CheckReleaseResponse, ghe *githubEnv) error {
if ghe.EventName != "pull_request" {
fmt.Println("::warning not a pull request event, will not create a comment.")
return nil
}
pr, err := getPRNumberFromEventFile(ghe.EventPath)
if err != nil {
fmt.Printf("::warning failed to get pull request number from event file, will not create a comment. error: %v", err.Error())
return nil
}
if pr == "" {
fmt.Println("::warning no pull request number found in the environment variables, will not create a comment.")
return nil
}
// upsert the comment
c := newClient(ghe.APIUrl, ghe.Token)
comments, err := c.listComments(ghe.Repo, pr)
if err != nil {
return errors.Wrapf(err, "failed to list comments")
}
for _, comment := range comments {
if comment.User.ID == githubActionUserID && strings.HasPrefix(comment.Body, commentHeader) {
// update the comment
if err := c.updateComment(ghe.Repo, comment.ID, buildCommentMessage(resp)); err != nil {
return errors.Wrapf(err, "failed to update comment")
}
return nil
}
}
// create a new comment
if err := c.createComment(ghe.Repo, pr, buildCommentMessage(resp)); err != nil {
return errors.Wrapf(err, "failed to create comment")
}
return nil
}
func buildCommentMessage(resp *v1pb.CheckReleaseResponse) string {
var errorCount, warningCount int
for _, result := range resp.Results {
for _, advice := range result.Advices {
switch advice.Status {
case v1pb.Advice_WARNING:
warningCount++
case v1pb.Advice_ERROR:
errorCount++
case v1pb.Advice_ADVICE_LEVEL_UNSPECIFIED, v1pb.Advice_SUCCESS:
// No action needed
default:
// Ignore unknown advice statuses
}
}
}
var sb strings.Builder
_, _ = sb.WriteString(commentHeader + "\n")
_, _ = sb.WriteString("## SQL Review Summary\n\n")
_, _ = fmt.Fprintf(&sb, "* Total Affected Rows: **%d**\n", resp.AffectedRows)
_, _ = fmt.Fprintf(&sb, "* Overall Risk Level: **%s**\n", formatRiskLevel(resp.RiskLevel))
_, _ = fmt.Fprintf(&sb, "* Advices Statistics: **%d Error(s), %d Warning(s)**\n", errorCount, warningCount)
_, _ = sb.WriteString("### Detailed Results\n")
_, _ = sb.WriteString(`
<table>
<thead>
<tr>
<th>File</th>
<th>Target</th>
<th>Affected Rows</th>
<th>Risk Level</th>
<th>Advices</th>
</tr>
</thead>
<tbody>`)
for _, result := range resp.Results {
if sb.Len() > maxCommentLength-1000 {
break
}
var errorCount, warningCount int
for _, advice := range result.Advices {
switch advice.Status {
case v1pb.Advice_WARNING:
warningCount++
case v1pb.Advice_ERROR:
errorCount++
case v1pb.Advice_ADVICE_LEVEL_UNSPECIFIED, v1pb.Advice_SUCCESS:
// No action needed
default:
// Ignore unknown advice statuses
}
}
counts := []string{}
if errorCount > 0 {
counts = append(counts, fmt.Sprintf("%d Error(s)", errorCount))
}
if warningCount > 0 {
counts = append(counts, fmt.Sprintf("%d Warning(s)", warningCount))
}
adviceCell := "-"
if len(counts) > 0 {
adviceCell = strings.Join(counts, ", ")
}
_, _ = fmt.Fprintf(&sb, `<tr>
<td>%s</td>
<td>%s</td>
<td>%d</td>
<td>%s</td>
<td>%s</td>
</tr>`, result.File, result.Target, result.AffectedRows, formatRiskLevel(result.RiskLevel), adviceCell)
}
_, _ = sb.WriteString("</tbody></table>")
return sb.String()
}
func writeAnnotations(resp *v1pb.CheckReleaseResponse) error {
// annotation template
// `::${advice.status} file=${file},line=${advice.line},col=${advice.column},title=${advice.title} (${advice.code})::${advice.content}. Targets: ${targets.join(', ')} https://docs.bytebase.com/sql-review/error-codes#${advice.code}`
for _, result := range resp.Results {
for _, advice := range result.Advices {
var sb strings.Builder
_, _ = sb.WriteString("::")
switch advice.Status {
case v1pb.Advice_WARNING:
_, _ = sb.WriteString("warning ")
case v1pb.Advice_ERROR:
_, _ = sb.WriteString("error ")
default:
continue
}
_, _ = sb.WriteString(" file=")
_, _ = sb.WriteString(result.File)
_, _ = sb.WriteString(",line=")
_, _ = sb.WriteString(strconv.Itoa(common.ConvertLineToActionLine(int(advice.GetStartPosition().GetLine()))))
_, _ = sb.WriteString(",title=")
_, _ = sb.WriteString(advice.Title)
_, _ = sb.WriteString(" (")
_, _ = sb.WriteString(strconv.Itoa(int(advice.Code)))
_, _ = sb.WriteString(")::")
_, _ = sb.WriteString(advice.Content)
_, _ = sb.WriteString(". Targets: ")
_, _ = sb.WriteString(result.Target)
_, _ = sb.WriteString(" ")
_, _ = sb.WriteString(" https://docs.bytebase.com/sql-review/error-codes#")
_, _ = sb.WriteString(strconv.Itoa(int(advice.Code)))
fmt.Println(sb.String())
}
}
return nil
}
func formatRiskLevel(r v1pb.RiskLevel) string {
switch r {
case v1pb.RiskLevel_LOW:
return "🟢 Low"
case v1pb.RiskLevel_MODERATE:
return "🟡 Moderate"
case v1pb.RiskLevel_HIGH:
return "🔴 High"
case v1pb.RiskLevel_RISK_LEVEL_UNSPECIFIED:
return "⚪ None"
default:
return "⚪ None"
}
}