Skip to content

Commit

Permalink
Add support for GitHub actions
Browse files Browse the repository at this point in the history
  • Loading branch information
mszostok committed Mar 29, 2020
1 parent dc43bb0 commit 01777ee
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Ignore everything
**

# Allow files and directories
!/codeowners-validator
12 changes: 12 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
builds:
- env:
- CGO_ENABLED=0
hooks:
# Install upx first, https://github.com/upx/upx/releases
post: ./hack/ci/compress.sh
goos:
- linux
- darwin
Expand Down Expand Up @@ -30,3 +33,12 @@ changelog:
exclude:
- '^docs:'
- '^test:'

dockers:
- dockerfile: Dockerfile
binaries:
- codeowners-validator
image_templates:
- "mszostok/codeowners-validator:latest"
- "mszostok/codeowners-validator:{{ .Tag }}"
- "mszostok/codeowners-validator:v{{ .Major }}.{{ .Minor }}"
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Get latest CA certs & git
FROM alpine:latest as deps
RUN apk --update add ca-certificates
RUN apk --update add git

FROM scratch

ARG prefix=""
ENV ENVS_PREFIX=${prefix}

LABEL source=https://github.com/mszostok/codeowners-validator.git

COPY ./codeowners-validator /codeowners-validator

COPY --from=deps /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=deps /usr/bin/git /usr/bin/git
COPY --from=deps /usr/bin/xargs /usr/bin/xargs
COPY --from=deps /lib /lib
COPY --from=deps /usr/lib /usr/lib

CMD ["/codeowners-validator"]

55 changes: 55 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: "GitHub CODEOWNERS Validator"
description: "Github action to ensure the correctness of your CODEOWNERS file."
author: "[email protected]"

inputs:
github_access_token:
description: "The GitHub access token. Instruction for creating a token can be found here. If not provided then validating owners functionality could not work properly, e.g. you can reach the API calls quota or if you are setting GitHub Enterprise base URL then an unauthorized error can occur."
required: false

github_base_url:
description: "The GitHub base URL for API requests. Defaults to the public GitHub API, but can be set to a domain endpoint to use with GitHub Enterprise. Default: https://api.github.com/"
required: false

github_upload_url:
description: "The GitHub upload URL for uploading files. It is taken into account only when the GITHUB_BASE_URL is also set. If only the GITHUB_BASE_URL is provided then this parameter defaults to the GITHUB_BASE_URL value. Default: https://uploads.github.com/"
required: false

experimental_checks:
description: "The comma-separated list of experimental checks that should be executed. By default, all experimental checks are turned off. Possible values: notowned."
default: ""
required: false

checks:
description: "The list of checks that will be executed. By default, all checks are executed. Possible values: files,owners,duppatterns"
required: false
default: ""

repository_path:
description: "The repository path in which CODEOWNERS file should be validated."
required: false
default: "."

check_failure_level:
description: "Defines the level on which the application should treat check issues as failures. Defaults to warning, which treats both errors and warnings as failures, and exits with error code 3. Possible values are error and warning. Default: warning"
required: false

not_owned_checker_skip_patterns:
description: "The comma-separated list of patterns that should be ignored by not-owned-checker. For example, you can specify * and as a result, the * pattern from the CODEOWNERS file will be ignored and files owned by this pattern will be reported as unowned unless a later specific pattern will match that path. It's useful because often we have default owners entry at the begging of the CODOEWNERS file, e.g. * @global-owner1 @global-owner2"
required: false

owner_checker_repository:
description: "The owner and repository name. For example, gh-codeowners/codeowners-samples. Used to check if GitHub team is in the given organization and has permission to the given repository."
required: false
default: "${{ github.repository }}"

runs:
using: 'docker'
image: 'docker://mszostok/codeowners-validator@canary'
env:
ENVS_PREFIX: "INPUT"
INPUT_USED_AS_GITHUB_ACTION: "true"

branding:
icon: "shield"
color: "gray-dark"
27 changes: 27 additions & 0 deletions hack/ci/compress.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash

# standard bash error handling
set -o nounset # treat unset variables as an error and exit immediately.
set -o errexit # exit immediately when a command fails.
set -E # needs to be set if we want the ERR trap

readonly CURRENT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
readonly ROOT_PATH=$( cd "${CURRENT_DIR}/../.." && pwd )
readonly GOLANGCI_LINT_VERSION="v1.23.8"

source "${CURRENT_DIR}/utilities.sh" || { echo 'Cannot load CI utilities.'; exit 1; }

# This will find all files (not symlinks) with the executable bit set:
# https://apple.stackexchange.com/a/116371
binariesToCompress=$(find "${ROOT_PATH}/dist" -perm +111 -type f)

shout "Staring compression for: \n$binariesToCompress"

# Kudos for https://liam.sh/post/makefiles-for-go-projects
command -v upx > /dev/null || { echo 'UPX binary not found, skipping compression.'; exit 1; }

# I just do not like playing with xargs ¯\_(ツ)_/¯
for i in $binariesToCompress
do
upx --brute "$i"
done
17 changes: 9 additions & 8 deletions internal/check/not_owned_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import (
"path"
"strings"

"github.com/hashicorp/go-multierror"
ctxutil "github.com/mszostok/codeowners-validator/internal/context"
"github.com/mszostok/codeowners-validator/pkg/codeowners"

"github.com/pkg/errors"
"gopkg.in/pipe.v2"

ctxutil "github.com/mszostok/codeowners-validator/internal/context"
)

type NotOwnedFileConfig struct {
Expand Down Expand Up @@ -46,11 +45,11 @@ func (c *NotOwnedFile) Check(ctx context.Context, in Input) (output Output, err
}

defer func() {
errReset := c.GitResetCurrentBranch(in.RepoDir)
if err != nil {
output = Output{}
err = multierror.Append(err, errReset).ErrorOrNil()
}
//errReset := c.GitResetCurrentBranch(in.RepoDir)
//if err != nil {
// output = Output{}
// err = multierror.Append(err, errReset).ErrorOrNil()
//}
}()

err = c.AppendToGitignoreFile(in.RepoDir, patterns)
Expand Down Expand Up @@ -104,6 +103,8 @@ func (c *NotOwnedFile) AppendToGitignoreFile(repoDir string, patterns []string)
defer f.Close()

content := strings.Builder{}
// ensure we are starting from new line
content.WriteString("\n")
for _, p := range patterns {
content.WriteString(fmt.Sprintf("%s\n", p))
}
Expand Down
60 changes: 32 additions & 28 deletions internal/check/valid_owner.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,27 @@ import (
)

type ValidOwnerConfig struct {
OrganizationName string `envconfig:"optional"`
Repository string
}

// ValidOwner validates each owner
type ValidOwner struct {
ghClient *github.Client
orgMembers *map[string]struct{}
orgName string
orgTeams map[string][]*github.Team
ghClient *github.Client
orgMembers *map[string]struct{}
orgName string
orgTeams []*github.Team
orgRepoName string
}

// NewValidOwner returns new instance of the ValidOwner
func NewValidOwner(cfg ValidOwnerConfig, ghClient *github.Client) *ValidOwner {
// todo check len
split := strings.Split(cfg.Repository, "/")

return &ValidOwner{
ghClient: ghClient,
orgName: cfg.OrganizationName,
ghClient: ghClient,
orgName: split[0],
orgRepoName: split[1],
}
}

Expand Down Expand Up @@ -87,24 +92,24 @@ func (v *ValidOwner) selectValidateFn(name string) func(context.Context, string)
}
}

func (v *ValidOwner) initOrgListTeams(ctx context.Context, org string) ([]*github.Team, *validateError) {
func (v *ValidOwner) initOrgListTeams(ctx context.Context) *validateError {
var teams []*github.Team
req := &github.ListOptions{
PerPage: 100,
}
for {
resultPage, resp, err := v.ghClient.Teams.ListTeams(ctx, org, req)
resultPage, resp, err := v.ghClient.Repositories.ListTeams(ctx, v.orgName, v.orgRepoName, req)
if err != nil { // TODO(mszostok): implement retry?
switch err := err.(type) {
case *github.ErrorResponse:
if err.Response.StatusCode == http.StatusUnauthorized {
return nil, newValidateError("Teams for organization %q could not be queried. Requires GitHub authorization.", org)
return newValidateError("Teams for organization %q could not be queried. Requires GitHub authorization.", v.orgName)
}
return nil, newValidateError("HTTP error occurred while calling GitHub: %v", err)
return newValidateError("HTTP error occurred while calling GitHub: %v", err)
case *github.RateLimitError:
return nil, newValidateError("GitHub rate limit reached: %v", err.Message).RateLimitReached()
return newValidateError("GitHub rate limit reached: %v", err.Message).RateLimitReached()
default:
return nil, newValidateError("Unknown error occurred while calling GitHub: %v", err)
return newValidateError("Unknown error occurred while calling GitHub: %v", err)
}
}
teams = append(teams, resultPage...)
Expand All @@ -114,31 +119,30 @@ func (v *ValidOwner) initOrgListTeams(ctx context.Context, org string) ([]*githu
req.Page = resp.NextPage
}

if v.orgTeams == nil {
v.orgTeams = map[string][]*github.Team{}
}
v.orgTeams[org] = teams
v.orgTeams = teams

return teams, nil
return nil
}

func (v *ValidOwner) validateTeam(ctx context.Context, name string) *validateError {
if v.orgTeams == nil {
if err := v.initOrgListTeams(ctx); err != nil {
return err
}
}

// called after validation it's safe to work on `parts` slice
parts := strings.SplitN(name, "/", 2)
org := parts[0]
org = strings.TrimPrefix(org, "@")
team := parts[1]

allTeams, ok := v.orgTeams[org]
if !ok {
var err *validateError
allTeams, err = v.initOrgListTeams(ctx, org)
if err != nil {
return err
}
if org != v.orgName {
return newValidateError("Team %q does not belongs to %q organization.", team, v.orgName)
}

teamExists := func() bool {
for _, v := range allTeams {
for _, v := range v.orgTeams {
if v.GetSlug() == team {
return true
}
Expand Down Expand Up @@ -223,9 +227,9 @@ func isEmailAddress(s string) bool {
func isGithubTeam(s string) bool {
hasPrefix := strings.HasPrefix(s, "@")
containsSlash := strings.Contains(s, "/")
splited := strings.SplitN(s, "/", 3) // 3 is enough to confirm that is invalid + will not overflow the buffer
split := strings.SplitN(s, "/", 3) // 3 is enough to confirm that is invalid + will not overflow the buffer

if hasPrefix && containsSlash && len(splited) == 2 {
if hasPrefix && containsSlash && len(split) == 2 {
return true
}

Expand Down
13 changes: 13 additions & 0 deletions internal/envconfig/envconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package envconfig

import (
"os"

"github.com/vrischmann/envconfig"
)

// Init the given config. Supports also envs prefix if set.
func Init(conf interface{}) error {
envPrefix := os.Getenv("ENVS_PREFIX")
return envconfig.InitWithPrefix(conf, envPrefix)
}
2 changes: 1 addition & 1 deletion internal/load/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"context"

"github.com/mszostok/codeowners-validator/internal/check"
"github.com/mszostok/codeowners-validator/internal/envconfig"
"github.com/mszostok/codeowners-validator/internal/github"

"github.com/pkg/errors"
"github.com/vrischmann/envconfig"
)

// For now, it is a good enough solution to init checks. Important thing is to do not require env variables
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import (
"syscall"

"github.com/mszostok/codeowners-validator/internal/check"
"github.com/mszostok/codeowners-validator/internal/envconfig"
"github.com/mszostok/codeowners-validator/internal/load"
"github.com/mszostok/codeowners-validator/internal/runner"
"github.com/mszostok/codeowners-validator/pkg/codeowners"
"github.com/mszostok/codeowners-validator/pkg/version"

"github.com/sirupsen/logrus"
"github.com/vrischmann/envconfig"
)

// Config holds the application configuration
Expand Down

0 comments on commit 01777ee

Please sign in to comment.