Skip to content

Commit d18c45d

Browse files
authored
Merge pull request #31 from mszostok/add-action-support
2 parents dc43bb0 + dcf72d1 commit d18c45d

11 files changed

Lines changed: 181 additions & 60 deletions

File tree

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Ignore everything
2+
**
3+
4+
# Allow files and directories
5+
!/codeowners-validator

.goreleaser.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
builds:
22
- env:
33
- CGO_ENABLED=0
4+
hooks:
5+
# Install upx first, https://github.com/upx/upx/releases
6+
post: ./hack/ci/compress.sh
47
goos:
58
- linux
69
- darwin
@@ -30,3 +33,12 @@ changelog:
3033
exclude:
3134
- '^docs:'
3235
- '^test:'
36+
37+
dockers:
38+
- dockerfile: Dockerfile
39+
binaries:
40+
- codeowners-validator
41+
image_templates:
42+
- "mszostok/codeowners-validator:latest"
43+
- "mszostok/codeowners-validator:{{ .Tag }}"
44+
- "mszostok/codeowners-validator:v{{ .Major }}.{{ .Minor }}"

Dockerfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Get latest CA certs & git
2+
FROM alpine:latest as deps
3+
RUN apk --update add ca-certificates
4+
RUN apk --update add git
5+
6+
FROM scratch
7+
8+
LABEL source=https://github.com/mszostok/codeowners-validator.git
9+
10+
COPY ./codeowners-validator /codeowners-validator
11+
12+
COPY --from=deps /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
13+
COPY --from=deps /usr/bin/git /usr/bin/git
14+
COPY --from=deps /usr/bin/xargs /usr/bin/xargs
15+
COPY --from=deps /lib /lib
16+
COPY --from=deps /usr/lib /usr/lib
17+
18+
CMD ["/codeowners-validator"]
19+

README.md

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,39 @@ You can install `codeowners-validator` with `env GO111MODULE=on go get -u github
4747
4848
This will put `codeowners-validator` in `$(go env GOPATH)/bin`
4949

50-
## Checks
50+
## Usage
5151

52-
The following checks are enabled by default:
52+
#### Docker
5353

54-
| Name | Description |
55-
|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
56-
| duppatterns | **[Duplicated Pattern Checker]** <br /><br /> Reports if CODEOWNERS file contain duplicated lines with the same file pattern. |
57-
| files | **[File Exist Checker]** <br /><br /> Reports if CODEOWNERS file contain lines with the file pattern that do not exist in a given repository. |
58-
| owners | **[Valid Owner Checker]** <br /><br /> Reports if CODEOWNERS file contain invalid owners definition. Allowed owner syntax: `@username`, `@org/team-name` or `user@example.com` <br /> _source: https://help.github.com/articles/about-code-owners/#codeowners-syntax_. <br /> <br /> **Checks:** <br /> &#x09; 1. Check if the owner's definition is valid (is either a GitHub user name, an organization team name or an email address). <br /><br /> 2. Check if a GitHub owner has a GitHub account <br /><br /> 3. Check if a GitHub owner is in a given organization <br /> <br />4. Check if an organization team exists |
54+
```bash
55+
export GH_TOKEN=<your_token>
56+
docker run --rm -v $(pwd):/repo -w /repo \
57+
-e REPOSITORY_PATH="." \
58+
-e GITHUB_ACCESS_TOKEN="$GH_TOKEN" \
59+
-e EXPERIMENTAL_CHECKS="notowned" \
60+
-e OWNER_CHECKER_REPOSITORY="org-name/rep-name" \
61+
mszostok/codeowners-validator:v0.4.0
62+
```
5963

60-
The experimental checks are disabled by default:
64+
#### Command line
6165

62-
| Name | Description |
63-
|----------|---------------------------------------------------------------------------------------------------------------------------------------------|
64-
| notowned | **[Not Owned File Checker]** <br /><br /> Reports if a given repository contain files that do not have specified owners in CODEOWNERS file. |
66+
```bash
67+
export GH_TOKEN=<your_token>
68+
env REPOSITORY_PATH="." \
69+
GITHUB_ACCESS_TOKEN="$GH_TOKEN" \
70+
EXPERIMENTAL_CHECKS="notowned" \
71+
OWNER_CHECKER_REPOSITORY="org-name/rep-name" \
72+
codeowners-validator
73+
```
6574

66-
To enable experimental check set `EXPERIMENTAL_CHECKS=notowned` environment variable.
75+
#### GitHub Action
6776

68-
Check the [Usage](#usage) section for more info on how to enable and configure given checks.
77+
Coming soon 😎 Stay tuned!
6978

70-
## Usage
79+
80+
Check the [Configuration](#configuration) section for more info on how to enable and configure given checks.
81+
82+
## Configuration
7183

7284
Use the following environment variables to configure the application:
7385

@@ -80,11 +92,31 @@ Use the following environment variables to configure the application:
8092
| <tt>CHECKS</tt>| - | The list of checks that will be executed. By default, all checks are executed. Possible values: `files`,`owners`,`duppatterns` |
8193
| <tt>EXPERIMENTAL_CHECKS</tt> | - | The comma-separated list of experimental checks that should be executed. By default, all experimental checks are turned off. Possible values: `notowned`.|
8294
| <tt>CHECK_FAILURE_LEVEL</tt> | `warning` | 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`. |
83-
| <tt>OWNER_CHECKER_ORGANIZATION_NAME</tt> <b>*</b>| | The organization name where the repository is created. Used to check if GitHub owner is in the given organization. |
95+
| <tt>OWNER_CHECKER_REPOSITORY</tt> <b>*</b>| | The owner and repository name separated by slash. For example, gh-codeowners/codeowners-samples. Used to check if GitHub owner is in the given organization. |
8496
| <tt>NOT_OWNED_CHECKER_SKIP_PATTERNS</tt>| - | 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` |
8597

8698
<b>*</b> - Required
8799

100+
## Checks
101+
102+
The following checks are enabled by default:
103+
104+
| Name | Description |
105+
|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
106+
| duppatterns | **[Duplicated Pattern Checker]** <br /><br /> Reports if CODEOWNERS file contain duplicated lines with the same file pattern. |
107+
| files | **[File Exist Checker]** <br /><br /> Reports if CODEOWNERS file contain lines with the file pattern that do not exist in a given repository. |
108+
| owners | **[Valid Owner Checker]** <br /><br /> Reports if CODEOWNERS file contain invalid owners definition. Allowed owner syntax: `@username`, `@org/team-name` or `user@example.com` <br /> _source: https://help.github.com/articles/about-code-owners/#codeowners-syntax_. <br /> <br /> **Checks:** <br /> &#x09; 1. Check if the owner's definition is valid (is either a GitHub user name, an organization team name or an email address). <br /><br /> 2. Check if a GitHub owner has a GitHub account <br /><br /> 3. Check if a GitHub owner is in a given organization <br /> <br />4. Check if an organization team exists |
109+
110+
The experimental checks are disabled by default:
111+
112+
| Name | Description |
113+
|----------|---------------------------------------------------------------------------------------------------------------------------------------------|
114+
| notowned | **[Not Owned File Checker]** <br /><br /> Reports if a given repository contain files that do not have specified owners in CODEOWNERS file. |
115+
116+
To enable experimental check set `EXPERIMENTAL_CHECKS=notowned` environment variable.
117+
118+
Check the [Configuration](#configuration) section for more info on how to enable and configure given checks.
119+
88120
#### Exit status codes
89121

90122
Application exits with different status codes which allow you to easily distinguish between error categories.

hack/ci/compress.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
# Inspired by https://liam.sh/post/makefiles-for-go-projects
3+
4+
# standard bash error handling
5+
set -o nounset # treat unset variables as an error and exit immediately.
6+
set -o errexit # exit immediately when a command fails.
7+
set -E # needs to be set if we want the ERR trap
8+
9+
readonly CURRENT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
10+
readonly ROOT_PATH=$( cd "${CURRENT_DIR}/../.." && pwd )
11+
readonly GOLANGCI_LINT_VERSION="v1.23.8"
12+
13+
source "${CURRENT_DIR}/utilities.sh" || { echo 'Cannot load CI utilities.'; exit 1; }
14+
15+
# This will find all files (not symlinks) with the executable bit set:
16+
# https://apple.stackexchange.com/a/116371
17+
binariesToCompress=$(find "${ROOT_PATH}/dist" -perm +111 -type f)
18+
19+
shout "Staring compression for: \n$binariesToCompress"
20+
21+
command -v upx > /dev/null || { echo 'UPX binary not found, skipping compression.'; exit 1; }
22+
23+
# I just do not like playing with xargs ¯\_(ツ)_/¯
24+
for i in $binariesToCompress
25+
do
26+
upx --brute "$i"
27+
done

internal/check/not_owned_file.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import (
77
"path"
88
"strings"
99

10-
"github.com/hashicorp/go-multierror"
10+
ctxutil "github.com/mszostok/codeowners-validator/internal/context"
1111
"github.com/mszostok/codeowners-validator/pkg/codeowners"
12+
13+
"github.com/hashicorp/go-multierror"
1214
"github.com/pkg/errors"
1315
"gopkg.in/pipe.v2"
14-
15-
ctxutil "github.com/mszostok/codeowners-validator/internal/context"
1616
)
1717

1818
type NotOwnedFileConfig struct {
@@ -104,6 +104,8 @@ func (c *NotOwnedFile) AppendToGitignoreFile(repoDir string, patterns []string)
104104
defer f.Close()
105105

106106
content := strings.Builder{}
107+
// ensure we are starting from new line
108+
content.WriteString("\n")
107109
for _, p := range patterns {
108110
content.WriteString(fmt.Sprintf("%s\n", p))
109111
}

internal/check/valid_owner.go

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,34 @@ import (
99
ctxutil "github.com/mszostok/codeowners-validator/internal/context"
1010

1111
"github.com/google/go-github/v29/github"
12+
"github.com/pkg/errors"
1213
)
1314

1415
type ValidOwnerConfig struct {
15-
OrganizationName string `envconfig:"optional"`
16+
Repository string
1617
}
1718

1819
// ValidOwner validates each owner
1920
type ValidOwner struct {
20-
ghClient *github.Client
21-
orgMembers *map[string]struct{}
22-
orgName string
23-
orgTeams map[string][]*github.Team
21+
ghClient *github.Client
22+
orgMembers *map[string]struct{}
23+
orgName string
24+
orgTeams []*github.Team
25+
orgRepoName string
2426
}
2527

2628
// NewValidOwner returns new instance of the ValidOwner
27-
func NewValidOwner(cfg ValidOwnerConfig, ghClient *github.Client) *ValidOwner {
28-
return &ValidOwner{
29-
ghClient: ghClient,
30-
orgName: cfg.OrganizationName,
29+
func NewValidOwner(cfg ValidOwnerConfig, ghClient *github.Client) (*ValidOwner, error) {
30+
split := strings.Split(cfg.Repository, "/")
31+
if len(split) != 2 {
32+
return nil, errors.Errorf("Wrong repository name. Expected pattern 'owner/repository', got '%s'", cfg.Repository)
3133
}
34+
35+
return &ValidOwner{
36+
ghClient: ghClient,
37+
orgName: split[0],
38+
orgRepoName: split[1],
39+
}, nil
3240
}
3341

3442
// Check checks if defined owners are the valid ones.
@@ -60,7 +68,7 @@ func (v *ValidOwner) Check(ctx context.Context, in Input) (Output, error) {
6068
validFn := v.selectValidateFn(ownerName)
6169
if err := validFn(ctx, ownerName); err != nil {
6270
output.ReportIssue(err.msg, WithEntry(entry))
63-
if err.rateLimitReached { // Doesn't make sense to process further. TODO(mszostok): change for more generic solution like, `IsPermanentError`
71+
if err.permanent { // Doesn't make sense to process further
6472
return output, nil
6573
}
6674
}
@@ -87,24 +95,24 @@ func (v *ValidOwner) selectValidateFn(name string) func(context.Context, string)
8795
}
8896
}
8997

90-
func (v *ValidOwner) initOrgListTeams(ctx context.Context, org string) ([]*github.Team, *validateError) {
98+
func (v *ValidOwner) initOrgListTeams(ctx context.Context) *validateError {
9199
var teams []*github.Team
92100
req := &github.ListOptions{
93101
PerPage: 100,
94102
}
95103
for {
96-
resultPage, resp, err := v.ghClient.Teams.ListTeams(ctx, org, req)
104+
resultPage, resp, err := v.ghClient.Repositories.ListTeams(ctx, v.orgName, v.orgRepoName, req)
97105
if err != nil { // TODO(mszostok): implement retry?
98106
switch err := err.(type) {
99107
case *github.ErrorResponse:
100108
if err.Response.StatusCode == http.StatusUnauthorized {
101-
return nil, newValidateError("Teams for organization %q could not be queried. Requires GitHub authorization.", org)
109+
return newValidateError("Teams for organization %q could not be queried. Requires GitHub authorization.", v.orgName)
102110
}
103-
return nil, newValidateError("HTTP error occurred while calling GitHub: %v", err)
111+
return newValidateError("HTTP error occurred while calling GitHub: %v", err)
104112
case *github.RateLimitError:
105-
return nil, newValidateError("GitHub rate limit reached: %v", err.Message).RateLimitReached()
113+
return newValidateError("GitHub rate limit reached: %v", err.Message)
106114
default:
107-
return nil, newValidateError("Unknown error occurred while calling GitHub: %v", err)
115+
return newValidateError("Unknown error occurred while calling GitHub: %v", err)
108116
}
109117
}
110118
teams = append(teams, resultPage...)
@@ -114,31 +122,30 @@ func (v *ValidOwner) initOrgListTeams(ctx context.Context, org string) ([]*githu
114122
req.Page = resp.NextPage
115123
}
116124

117-
if v.orgTeams == nil {
118-
v.orgTeams = map[string][]*github.Team{}
119-
}
120-
v.orgTeams[org] = teams
125+
v.orgTeams = teams
121126

122-
return teams, nil
127+
return nil
123128
}
124129

125130
func (v *ValidOwner) validateTeam(ctx context.Context, name string) *validateError {
131+
if v.orgTeams == nil {
132+
if err := v.initOrgListTeams(ctx); err != nil {
133+
return err.AsPermanent()
134+
}
135+
}
136+
137+
// called after validation it's safe to work on `parts` slice
126138
parts := strings.SplitN(name, "/", 2)
127139
org := parts[0]
128140
org = strings.TrimPrefix(org, "@")
129141
team := parts[1]
130142

131-
allTeams, ok := v.orgTeams[org]
132-
if !ok {
133-
var err *validateError
134-
allTeams, err = v.initOrgListTeams(ctx, org)
135-
if err != nil {
136-
return err
137-
}
143+
if org != v.orgName {
144+
return newValidateError("Team %q does not belongs to %q organization.", team, v.orgName)
138145
}
139146

140147
teamExists := func() bool {
141-
for _, v := range allTeams {
148+
for _, v := range v.orgTeams {
142149
if v.GetSlug() == team {
143150
return true
144151
}
@@ -156,7 +163,7 @@ func (v *ValidOwner) validateTeam(ctx context.Context, name string) *validateErr
156163
func (v *ValidOwner) validateGithubUser(ctx context.Context, name string) *validateError {
157164
if v.orgMembers == nil { //TODO(mszostok): lazy init, make it more robust.
158165
if err := v.initOrgListMembers(ctx); err != nil {
159-
return newValidateError("Cannot initialize organization member list: %v", err)
166+
return newValidateError("Cannot initialize organization member list: %v", err).AsPermanent()
160167
}
161168
}
162169

@@ -168,11 +175,11 @@ func (v *ValidOwner) validateGithubUser(ctx context.Context, name string) *valid
168175
if err.Response.StatusCode == http.StatusNotFound {
169176
return newValidateError("User %q does not have github account", name)
170177
}
171-
return newValidateError("HTTP error occurred while calling GitHub: %v", err)
178+
return newValidateError("HTTP error occurred while calling GitHub: %v", err).AsPermanent()
172179
case *github.RateLimitError:
173-
return newValidateError("GitHub rate limit reached: %v", err.Message).RateLimitReached()
180+
return newValidateError("GitHub rate limit reached: %v", err.Message).AsPermanent()
174181
default:
175-
return newValidateError("Unknown error occurred while calling GitHub: %v", err)
182+
return newValidateError("Unknown error occurred while calling GitHub: %v", err).AsPermanent()
176183
}
177184
}
178185

@@ -223,9 +230,9 @@ func isEmailAddress(s string) bool {
223230
func isGithubTeam(s string) bool {
224231
hasPrefix := strings.HasPrefix(s, "@")
225232
containsSlash := strings.Contains(s, "/")
226-
splited := strings.SplitN(s, "/", 3) // 3 is enough to confirm that is invalid + will not overflow the buffer
233+
split := strings.SplitN(s, "/", 3) // 3 is enough to confirm that is invalid + will not overflow the buffer
227234

228-
if hasPrefix && containsSlash && len(splited) == 2 {
235+
if hasPrefix && containsSlash && len(split) == 2 {
229236
return true
230237
}
231238

internal/check/valid_owner_error.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package check
33
import "fmt"
44

55
type validateError struct {
6-
msg string
7-
rateLimitReached bool
6+
msg string
7+
permanent bool
88
}
99

1010
func newValidateError(format string, a ...interface{}) *validateError {
@@ -13,7 +13,7 @@ func newValidateError(format string, a ...interface{}) *validateError {
1313
}
1414
}
1515

16-
func (err *validateError) RateLimitReached() *validateError {
17-
err.rateLimitReached = true
16+
func (err *validateError) AsPermanent() *validateError {
17+
err.permanent = true
1818
return err
1919
}

0 commit comments

Comments
 (0)