blob: a6f290ab492940334b796a0b008ca5d96c25c568 [file] [log] [blame]
//
// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
package review
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"github.com/google/go-github/v33/github"
"golang.org/x/oauth2"
"github.com/apache/skywalking-eyes/internal/logger"
comments2 "github.com/apache/skywalking-eyes/pkg/comments"
config2 "github.com/apache/skywalking-eyes/pkg/config"
header2 "github.com/apache/skywalking-eyes/pkg/header"
)
var (
Identification = "license-eye hidden identification"
gh *github.Client
ctx context.Context
owner string
repo string
sha string
pr int
requiredEnvVars = []string{
"GITHUB_TOKEN",
"GITHUB_HEAD_REF",
"GITHUB_REPOSITORY",
"GITHUB_EVENT_NAME",
"GITHUB_EVENT_PATH",
}
)
func init() {
if os.Getenv("GITHUB_TOKEN") == "" {
logger.Log.Infoln("GITHUB_TOKEN is not set, license-eye won't comment on the pull request")
return
}
if !IsPR() {
return
}
if !IsGHA() {
panic(fmt.Errorf(fmt.Sprintf(
`this must be run on GitHub Actions or you have to set the environment variables %v manually.`, requiredEnvVars,
)))
}
s, err := GetSha()
if err != nil {
logger.Log.Warnln("failed to get sha", err)
return
}
sha = s
token := os.Getenv("GITHUB_TOKEN")
ref := os.Getenv("GITHUB_REF")
fullName := os.Getenv("GITHUB_REPOSITORY")
logger.Log.Debugln("ref:", ref, "; repo:", fullName, "; sha:", sha)
ownerRepo := strings.Split(fullName, "/")
if len(ownerRepo) != 2 {
logger.Log.Warnln("Length of ownerRepo is not 2", ownerRepo)
return
}
owner, repo = ownerRepo[0], ownerRepo[1]
matches := regexp.MustCompile(`refs/pull/(\d+)/merge`).FindStringSubmatch(ref)
if len(matches) < 1 {
logger.Log.Warnln("Length of ref < 1", matches)
return
}
prString := matches[1]
if p, err := strconv.Atoi(prString); err == nil {
pr = p
} else {
logger.Log.Warnln("Failed to parse pull request number.", err)
return
}
ctx = context.Background()
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(ctx, ts)
gh = github.NewClient(tc)
}
// Header reviews the license header, including suggestions on the pull request and an overview of the checks.
func Header(result *header2.Result, config *config2.Config) error {
if !result.HasFailure() || !IsPR() || gh == nil || config.Header.Comment == header2.Never {
return nil
}
commentedFiles := make(map[string]bool)
for _, comment := range GetAllReviewsComments() {
decodeString := comment.GetBody()
if strings.Contains(decodeString, Identification) {
logger.Log.Debugln("Path:", comment.GetPath())
commentedFiles[comment.GetPath()] = true
}
}
logger.Log.Debugln("CommentedFiles:", commentedFiles)
s := "RIGHT"
l := 1
var comments []*github.DraftReviewComment
for _, changedFile := range GetChangedFiles() {
logger.Log.Debugln("ChangedFile:", changedFile.GetFilename())
if commentedFiles[changedFile.GetFilename()] {
logger.Log.Debugln("ChangedFile was reviewed, skipping:", changedFile.GetFilename())
continue
}
for _, invalidFile := range result.Failure {
if !strings.HasSuffix(invalidFile, changedFile.GetFilename()) {
continue
}
blob, _, err := gh.Git.GetBlob(ctx, owner, repo, changedFile.GetSHA())
if err != nil {
logger.Log.Warnln("Failed to get blob:", changedFile.GetFilename(), changedFile.GetSHA())
continue
}
style := comments2.FileCommentStyle(changedFile.GetFilename())
if style == nil {
logger.Log.Warnln("Failed to determine the comment style of file:", changedFile.GetFilename())
continue
}
header, err := header2.GenerateLicenseHeader(style, &config.Header)
if err != nil {
logger.Log.Warnln("Failed to generate comment header:", changedFile.GetFilename())
continue
}
decodeString, err := base64.StdEncoding.DecodeString(blob.GetContent())
if err != nil {
logger.Log.Debugln("Failed to decode blob content:", err)
continue
}
body := "```suggestion\n" + header + strings.Split(string(decodeString), "\n")[0] + "\n```\n" + fmt.Sprintf(`<!-- %v -->`, Identification)
comments = append(comments, &github.DraftReviewComment{
Path: changedFile.Filename,
Body: &body,
Side: &s,
Line: &l,
})
}
}
return tryReview(result, config, comments)
}
func tryReview(result *header2.Result, config *config2.Config, comments []*github.DraftReviewComment) error {
tryBestEffortToComment := func() error {
if err := doReview(result, comments); err != nil {
logger.Log.Warnln("Failed to create review comment, fallback to a plain comment:", err)
_ = doReview(result, nil)
return err
}
return nil
}
if config.Header.Comment == header2.Always {
if err := tryBestEffortToComment(); err != nil {
return err
}
} else if config.Header.Comment == header2.OnFailure && len(comments) > 0 {
if err := tryBestEffortToComment(); err != nil {
return err
}
}
return nil
}
func doReview(result *header2.Result, comments []*github.DraftReviewComment) error {
logger.Log.Debugln("Comments:", comments)
c := Markdown(result)
e := "COMMENT"
if _, _, err := gh.PullRequests.CreateReview(ctx, owner, repo, pr, &github.PullRequestReviewRequest{
CommitID: &sha,
Body: &c,
Event: &e,
Comments: comments,
}); err != nil {
return err
}
return nil
}
// GetChangedFiles returns the changed files in this pull request.
func GetChangedFiles() []*github.CommitFile {
prsvc := gh.PullRequests
options := &github.ListOptions{Page: 1, PerPage: 100}
var allFiles []*github.CommitFile
for files, response, err := prsvc.ListFiles(ctx, owner, repo, pr, options); err == nil; {
allFiles = append(allFiles, files...)
if response.NextPage <= options.Page {
break
}
options = &github.ListOptions{Page: response.NextPage, PerPage: options.PerPage}
}
return allFiles
}
// GetAllReviewsComments returns all review comments of the pull request.
func GetAllReviewsComments() []*github.PullRequestComment {
prsvc := gh.PullRequests
options := &github.PullRequestListCommentsOptions{ListOptions: github.ListOptions{Page: 1, PerPage: 100}}
var allComments []*github.PullRequestComment
for comments, response, err := prsvc.ListComments(ctx, owner, repo, pr, options); err == nil; {
allComments = append(allComments, comments...)
if response.NextPage <= options.Page {
break
}
options = &github.PullRequestListCommentsOptions{
ListOptions: github.ListOptions{Page: response.NextPage, PerPage: options.PerPage},
}
}
return allComments
}
func IsGHA() bool {
for _, key := range requiredEnvVars {
if val := os.Getenv(key); val == "" {
return false
}
}
return true
}
func IsPR() bool {
return os.Getenv("GITHUB_EVENT_NAME") == "pull_request"
}
// TODO add fixing guide
func Markdown(result *header2.Result) string {
return fmt.Sprintf(`
<!-- %s -->
[license-eye](https://github.com/apache/skywalking-eyes/tree/main/cmd/license-eye) has totally checked %d files.
| Valid | Invalid | Ignored | Fixed |
| --- | --- | --- | --- |
| %d | %d | %d | %d |
<details>
<summary>Click to see the invalid file list</summary>
%v
</details>
`,
Identification,
len(result.Success)+len(result.Failure)+len(result.Ignored),
len(result.Success),
len(result.Failure),
len(result.Ignored),
len(result.Fixed),
"- "+strings.Join(result.Failure, "\n- "),
)
}
type Event struct {
PR github.PullRequest `json:"pull_request"`
}
func GetSha() (string, error) {
filepath := os.Getenv("GITHUB_EVENT_PATH")
logger.Log.Debugln("GITHUB_EVENT_PATH: ", filepath)
if filepath == "" {
return "", fmt.Errorf("failed to get event path")
}
content, err := os.ReadFile(filepath)
if err != nil {
return "", err
}
logger.Log.Debugln(filepath, "content:", string(content))
var event Event
if err := json.Unmarshal(content, &event); err != nil {
return "", err
}
return *event.PR.Head.SHA, nil
}