Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃尡 Get repo info from REST API if event file is unavailable #576

Merged
merged 1 commit into from Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
130 changes: 64 additions & 66 deletions github/github.go
Expand Up @@ -15,20 +15,18 @@
package github

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"

"github.com/ossf/scorecard/v4/clients/githubrepo/roundtripper"
"github.com/ossf/scorecard/v4/log"
)

const (
baseRepoURL = "https://api.github.com/repos/"
sclog "github.com/ossf/scorecard/v4/log"
)

// RepoInfo is a struct for repository information.
Expand All @@ -43,9 +41,9 @@ type repo struct {

GITHUB_REPOSITORY_IS_FORK is true if the repository is a fork.
*/
DefaultBranch string `json:"default_branch"`
Fork bool `json:"fork"`
Private bool `json:"private"`
DefaultBranch *string `json:"default_branch"`
Fork *bool `json:"fork"`
Private *bool `json:"private"`
}

// Client holds a context and roundtripper for querying repo info from GitHub.
Expand All @@ -54,20 +52,6 @@ type Client struct {
rt http.RoundTripper
}

// NewClient returns a new Client for querying repo info from GitHub.
func NewClient(ctx context.Context) *Client {
c := &Client{}

defaultCtx := context.Background()
if ctx == nil {
ctx = defaultCtx
}

c.SetContext(ctx)
c.SetDefaultTransport()
return c
}

// SetContext sets a context for a GitHub client.
func (c *Client) SetContext(ctx context.Context) {
c.ctx = ctx
Expand All @@ -80,82 +64,96 @@ func (c *Client) SetTransport(rt http.RoundTripper) {

// SetDefaultTransport sets the scorecard roundtripper for a GitHub client.
func (c *Client) SetDefaultTransport() {
logger := log.NewLogger(log.DefaultLevel)
logger := sclog.NewLogger(sclog.DefaultLevel)
rt := roundtripper.NewTransport(c.ctx, logger)
c.rt = rt
}

// WriteRepoInfo queries GitHub for repo info and writes it to a file.
func WriteRepoInfo(ctx context.Context, repoName, path string) error {
c := NewClient(ctx)
repoInfo, err := c.RepoInfo(repoName)
if err != nil {
return fmt.Errorf("getting repo info: %w", err)
}

repoFile, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating repo info file: %w", err)
}
defer repoFile.Close()

resp := repoInfo.respBytes
_, writeErr := repoFile.Write(resp)
if writeErr != nil {
return fmt.Errorf("writing repo info: %w", writeErr)
}

return nil
}

// RepoInfo is a function to get the repository information.
// ParseFromURL is a function to get the repository information.
// It is decided to not use the golang GitHub library because of the
// dependency on the github.com/google/go-github/github library
// which will in turn require other dependencies.
func (c *Client) RepoInfo(repoName string) (RepoInfo, error) {
var r RepoInfo

func (c *Client) ParseFromURL(baseRepoURL, repoName string) (RepoInfo, error) {
var ret RepoInfo
baseURL, err := url.Parse(baseRepoURL)
if err != nil {
return r, fmt.Errorf("parsing base repo URL: %w", err)
return ret, fmt.Errorf("parsing base repo URL: %w", err)
}

repoURL, err := baseURL.Parse(repoName)
repoURL, err := baseURL.Parse(fmt.Sprintf("repos/%s", repoName))
if err != nil {
return r, fmt.Errorf("parsing repo endpoint: %w", err)
return ret, fmt.Errorf("parsing repo endpoint: %w", err)
}

method := "GET"
log.Printf("getting repo info from URL: %s", repoURL.String())
req, err := http.NewRequestWithContext(
c.ctx,
method,
http.MethodGet,
repoURL.String(),
nil,
)
nil /*body*/)
if err != nil {
return r, fmt.Errorf("error creating request: %w", err)
return ret, fmt.Errorf("error creating request: %w", err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return r, fmt.Errorf("error creating request: %w", err)
return ret, fmt.Errorf("error creating request: %w", err)
}
defer resp.Body.Close()
if err != nil {
return r, fmt.Errorf("error reading response body: %w", err)
return ret, fmt.Errorf("error reading response body: %w", err)
}

respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return r, fmt.Errorf("error reading response body: %w", err)
return ret, fmt.Errorf("error reading response body: %w", err)
}

r.respBytes = respBytes
prettyPrintJSON(respBytes)
ret.respBytes = respBytes
if err := json.Unmarshal(respBytes, &ret.Repo); err != nil {
return ret, fmt.Errorf("error decoding response body: %w", err)
}
return ret, nil
}

// ParseFromFile is a function to get the repository information
// from GitHub event file.
func (c *Client) ParseFromFile(filepath string) (RepoInfo, error) {
var ret RepoInfo

err = json.Unmarshal(respBytes, &r)
log.Printf("getting repo info from file: %s", filepath)
repoInfo, err := ioutil.ReadFile(filepath)
if err != nil {
return r, fmt.Errorf("error decoding response body: %w", err)
return ret, fmt.Errorf("reading GitHub event path: %w", err)
}

return r, nil
prettyPrintJSON(repoInfo)
if err := json.Unmarshal(repoInfo, &ret); err != nil {
return ret, fmt.Errorf("unmarshalling repo info: %w", err)
}

return ret, nil
}

// NewClient returns a new Client for querying repo info from GitHub.
func NewClient(ctx context.Context) *Client {
c := &Client{
ctx: ctx,
}

if c.ctx == nil {
c.SetContext(context.Background())
}
c.SetDefaultTransport()
return c
}

func prettyPrintJSON(jsonBytes []byte) {
var buf bytes.Buffer
if err := json.Indent(&buf, jsonBytes, "", ""); err != nil {
log.Printf("%v", err)
return
}
log.Println(buf.String())
}
1 change: 1 addition & 0 deletions options/env.go
Expand Up @@ -21,6 +21,7 @@ import (

// Environment variables.
// TODO(env): Remove once environment variables are not used for config.
//
//nolint:revive,nolintlint
const (
EnvEnableSarif = "ENABLE_SARIF"
Expand Down
58 changes: 39 additions & 19 deletions options/options.go
Expand Up @@ -15,15 +15,14 @@
package options

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"

"github.com/caarlos0/env/v6"
"golang.org/x/net/context"

"github.com/ossf/scorecard-action/github"
"github.com/ossf/scorecard/v4/checks"
Expand All @@ -44,6 +43,7 @@ var (
// Errors.
errGithubEventPathEmpty = errors.New("GitHub event path is empty")
errResultsPathEmpty = errors.New("results path is empty")
errGitHubRepoInfoUnavailable = errors.New("GitHub repo info inaccessible")
errOnlyDefaultBranchSupported = errors.New("only default branch is supported")
)

Expand All @@ -67,6 +67,7 @@ type Options struct {
GithubRef string `env:"GITHUB_REF"`
GithubRepository string `env:"GITHUB_REPOSITORY"`
GithubWorkspace string `env:"GITHUB_WORKSPACE"`
GithubAPIURL string `env:"GITHUB_API_URL"`

DefaultBranch string `env:"SCORECARD_DEFAULT_BRANCH"`
// TODO(options): This may be better as a bool
Expand All @@ -87,6 +88,13 @@ func New() (*Options, error) {
if err := env.Parse(opts); err != nil {
return opts, fmt.Errorf("parsing entrypoint env vars: %w", err)
}
// GITHUB_AUTH_TOKEN
// Needs to be set *before* setRepoInfo() is invoked.
// setRepoInfo() uses the GITHUB_AUTH_TOKEN env for querying the REST API.
if _, tokenSet := os.LookupEnv(EnvGithubAuthToken); !tokenSet {
inputToken := os.Getenv(EnvInputRepoToken)
os.Setenv(EnvGithubAuthToken, inputToken)
}
if err := opts.setRepoInfo(); err != nil {
return opts, fmt.Errorf("parsing repo info: %w", err)
}
Expand Down Expand Up @@ -143,12 +151,6 @@ func (o *Options) Print() {

func (o *Options) setScorecardOpts() {
o.ScorecardOpts = scopts.New()
// GITHUB_AUTH_TOKEN
_, tokenSet := os.LookupEnv(EnvGithubAuthToken)
if !tokenSet {
inputToken := os.Getenv(EnvInputRepoToken)
os.Setenv(EnvGithubAuthToken, inputToken)
}

// --repo= | --local
// This section restores functionality that was removed in
Expand Down Expand Up @@ -194,6 +196,8 @@ func (o *Options) setScorecardOpts() {
// setPublishResults sets whether results should be published based on a
// repository's visibility.
func (o *Options) setPublishResults() {
inputVal := o.PublishResults
o.PublishResults = false
privateRepo, err := strconv.ParseBool(o.PrivateRepoStr)
if err != nil {
// TODO(options): Consider making this an error.
Expand All @@ -202,9 +206,10 @@ func (o *Options) setPublishResults() {
o.PrivateRepoStr,
err,
)
return
}

o.PublishResults = o.PublishResults && !privateRepo
o.PublishResults = inputVal && !privateRepo
}

// setRepoInfo gets the path to the GitHub event and sets the
Expand All @@ -217,21 +222,36 @@ func (o *Options) setRepoInfo() error {
return errGithubEventPathEmpty
}

repoInfo, err := ioutil.ReadFile(eventPath)
if err != nil {
return fmt.Errorf("reading GitHub event path: %w", err)
ghClient := github.NewClient(context.Background())
if repoInfo, err := ghClient.ParseFromFile(eventPath); err == nil &&
o.parseFromRepoInfo(repoInfo) {
return nil
}

var r github.RepoInfo
if err := json.Unmarshal(repoInfo, &r); err != nil {
return fmt.Errorf("unmarshalling repo info: %w", err)
if repoInfo, err := ghClient.ParseFromURL(o.GithubAPIURL, o.GithubRepository); err == nil &&
o.parseFromRepoInfo(repoInfo) {
return nil
}

o.PrivateRepoStr = strconv.FormatBool(r.Repo.Private)
o.IsForkStr = strconv.FormatBool(r.Repo.Fork)
o.DefaultBranch = r.Repo.DefaultBranch
return errGitHubRepoInfoUnavailable
}

return nil
func (o *Options) parseFromRepoInfo(repoInfo github.RepoInfo) bool {
if repoInfo.Repo.DefaultBranch == nil &&
repoInfo.Repo.Fork == nil &&
repoInfo.Repo.Private == nil {
return false
}
if repoInfo.Repo.Private != nil {
o.PrivateRepoStr = strconv.FormatBool(*repoInfo.Repo.Private)
}
if repoInfo.Repo.Fork != nil {
o.IsForkStr = strconv.FormatBool(*repoInfo.Repo.Fork)
}
if repoInfo.Repo.DefaultBranch != nil {
o.DefaultBranch = *repoInfo.Repo.DefaultBranch
}
return true
}

func (o *Options) isPullRequestEvent() bool {
Expand Down