Skip to content

Commit

Permalink
feat: add support for GH actions on forked repo PRs (#130)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Rynhard <[email protected]>
  • Loading branch information
andrewrynhard authored Jul 2, 2019
1 parent 4447684 commit cc97536
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 46 deletions.
15 changes: 15 additions & 0 deletions .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ services:
- name: docker
image: docker:dind
privileged: true
network_mode: host
volumes:
- name: dockersock
path: /var/run
Expand Down Expand Up @@ -66,6 +67,20 @@ steps:
event:
- push

- name: release
image: plugins/github-release
settings:
api_key:
from_secret: github_token
draft: true
files:
- build/conform-*
checksum:
- sha256
- sha512
when:
event: tag

volumes:
- name: dockersock
temp: {}
60 changes: 16 additions & 44 deletions internal/enforcer/enforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@
package enforcer

import (
"context"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"strings"
"text/tabwriter"

"github.com/autonomy/conform/internal/policy"
"github.com/autonomy/conform/internal/policy/commit"
"github.com/autonomy/conform/internal/policy/license"
"github.com/google/go-github/github"
"github.com/autonomy/conform/internal/summarizer"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"

Expand All @@ -27,9 +23,8 @@ import (

// Conform is a struct that conform.yaml gets decoded into.
type Conform struct {
Policies []*PolicyDeclaration `yaml:"policies"`

token string
Policies []*PolicyDeclaration `yaml:"policies"`
summarizer summarizer.Summarizer
}

// PolicyDeclaration allows a user to declare an arbitrary type along with a
Expand Down Expand Up @@ -60,7 +55,13 @@ func New() (*Conform, error) {

token, ok := os.LookupEnv("GITHUB_TOKEN")
if ok {
c.token = token
s, err := summarizer.NewGitHubSummarizer(token)
if err != nil {
return nil, err
}
c.summarizer = s
} else {
c.summarizer = &summarizer.Noop{}
}

return c, nil
Expand All @@ -85,10 +86,14 @@ func (c *Conform) Enforce(setters ...policy.Option) {
for _, err := range check.Errors() {
fmt.Fprintf(w, "%s\t%s\t%s\t%v\t\n", p.Type, check.Name(), "FAILED", err)
}
c.SetStatus("failure", p.Type, check.Name(), check.Message())
if err := c.summarizer.SetStatus("failure", p.Type, check.Name(), check.Message()); err != nil {
log.Printf("WARNING: summary failed: %+v", err)
}
} else {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n", p.Type, check.Name(), "PASS", "<none>")
c.SetStatus("success", p.Type, check.Name(), check.Message())
if err := c.summarizer.SetStatus("success", p.Type, check.Name(), check.Message()); err != nil {
log.Printf("WARNING: summary failed: %+v", err)
}
}
}
}
Expand All @@ -101,39 +106,6 @@ func (c *Conform) Enforce(setters ...policy.Option) {
}
}

// SetStatus sets the status of a GitHub check.
// Valid statuses are "error", "failure", "pending", "success"
func (c *Conform) SetStatus(state, policy, check, message string) {
if c.token == "" {
return
}
statusCheckContext := strings.ReplaceAll(strings.ToLower(path.Join("conform", policy, check)), " ", "-")
description := message
repoStatus := &github.RepoStatus{}
repoStatus.Context = &statusCheckContext
repoStatus.Description = &description
repoStatus.State = &state

http.DefaultClient.Transport = roundTripper{c.token}
githubClient := github.NewClient(http.DefaultClient)

parts := strings.Split(os.Getenv("GITHUB_REPOSITORY"), "/")

_, _, err := githubClient.Repositories.CreateStatus(context.Background(), parts[0], parts[1], os.Getenv("GITHUB_SHA"), repoStatus)
if err != nil {
log.Fatal(err)
}
}

type roundTripper struct {
accessToken string
}

func (rt roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", rt.accessToken))
return http.DefaultTransport.RoundTrip(r)
}

func (c *Conform) enforce(declaration *PolicyDeclaration, opts *policy.Options) (*policy.Report, error) {
if _, ok := policyMap[declaration.Type]; !ok {
return nil, errors.Errorf("Policy %q is not defined", declaration.Type)
Expand Down
51 changes: 49 additions & 2 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
package git

import (
"fmt"
"os"
"path"
"path/filepath"

git "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/config"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object"
)

Expand Down Expand Up @@ -76,14 +79,58 @@ func (g *Git) Message() (message string, err error) {
func (g *Git) HasGPGSignature() (ok bool, err error) {
ref, err := g.repo.Head()
if err != nil {
return
return false, err
}
commit, err := g.repo.CommitObject(ref.Hash())
if err != nil {
return
return false, err
}

ok = commit.PGPSignature != ""

return ok, err
}

// FetchPullRequest fetches a remote PR.
func (g *Git) FetchPullRequest(remote string, number int) (err error) {
opts := &git.FetchOptions{
RemoteName: remote,
RefSpecs: []config.RefSpec{
config.RefSpec(fmt.Sprintf("refs/pull/%d/head:pr/%d", number, number)),
},
}
if err = g.repo.Fetch(opts); err != nil {
return err
}

return nil
}

// CheckoutPullRequest checks out pull request.
func (g *Git) CheckoutPullRequest(number int) (err error) {
w, err := g.repo.Worktree()
if err != nil {
return err
}

opts := &git.CheckoutOptions{
Branch: plumbing.ReferenceName(fmt.Sprintf("pr/%d", number)),
}

if err := w.Checkout(opts); err != nil {
return err
}

return nil
}

// SHA returns the sha of the current commit.
func (g *Git) SHA() (sha string, err error) {
ref, err := g.repo.Head()
if err != nil {
return sha, err
}
sha = ref.Hash().String()

return sha, nil
}
119 changes: 119 additions & 0 deletions internal/summarizer/summarizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package summarizer

import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"strings"

"github.com/autonomy/conform/internal/git"
"github.com/google/go-github/github"
)

// Summarizer describes a hook for send summarized results to a remote API.
type Summarizer interface {
SetStatus(string, string, string, string) error
}

// GitHub is a summarizer that can be used with GitHub.
type GitHub struct {
token string
owner string
repo string
sha string
}

// Noop is a summarizer that does nothing.
type Noop struct {
}

// SetStatus is a noop func.
func (n *Noop) SetStatus(state, policy, check, message string) error {
return nil
}

// NewGitHubSummarizer returns a summarizer that posts policy checks as status
// checks on a pull request.
func NewGitHubSummarizer(token string) (*GitHub, error) {
eventPath, ok := os.LookupEnv("GITHUB_EVENT_PATH")
if !ok {
return nil, errors.New("GITHUB_EVENT_PATH is not set")
}

data, err := ioutil.ReadFile(eventPath)
if err != nil {
return nil, err
}

pullRequestEvent := &github.PullRequestEvent{}
if err = json.Unmarshal(data, pullRequestEvent); err != nil {
return nil, err
}

g, err := git.NewGit()
if err != nil {
return nil, err
}

if err = g.FetchPullRequest("origin", pullRequestEvent.GetNumber()); err != nil {
return nil, err
}

if err = g.CheckoutPullRequest(pullRequestEvent.GetNumber()); err != nil {
return nil, err
}

sha, err := g.SHA()
if err != nil {
log.Fatal(err)
}

gh := &GitHub{
token: token,
owner: pullRequestEvent.GetRepo().GetOwner().GetLogin(),
repo: pullRequestEvent.GetRepo().GetName(),
sha: sha,
}

return gh, nil
}

// SetStatus sets the status of a GitHub check.
// Valid statuses are "error", "failure", "pending", "success"
func (gh *GitHub) SetStatus(state, policy, check, message string) error {
if gh.token == "" {
return errors.New("no token")
}
statusCheckContext := strings.ReplaceAll(strings.ToLower(path.Join("conform", policy, check)), " ", "-")
description := message
repoStatus := &github.RepoStatus{}
repoStatus.Context = &statusCheckContext
repoStatus.Description = &description
repoStatus.State = &state

http.DefaultClient.Transport = roundTripper{gh.token}
githubClient := github.NewClient(http.DefaultClient)

_, _, err := githubClient.Repositories.CreateStatus(context.Background(), gh.owner, gh.repo, gh.sha, repoStatus)
if err != nil {
return err
}

return nil
}

type roundTripper struct {
accessToken string
}

// RoundTrip implements the net/http.RoundTripper interface.
func (rt roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", rt.accessToken))
return http.DefaultTransport.RoundTrip(r)
}

0 comments on commit cc97536

Please sign in to comment.