Skip to content

Commit

Permalink
feat: add username whitelist (#66)
Browse files Browse the repository at this point in the history
* chore(dummy): test yaml sequence compatibility

* fix: use single tick for regex default values

* feat: add string array reader utility

* feat: add username whitelist

* docs: write documentation about username whitelist
  • Loading branch information
Namchee authored Feb 4, 2022
1 parent 1637e52 commit eec0dba
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 28 deletions.
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,19 @@ You can customize this actions with these following options (fill it on `with` s
| --------------------- | ------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `access_token` | `true` | | [GitHub access token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token) to interact with the GitHub API. It is recommended to store this token with [GitHub Secrets](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets). |
| `close` | `false` | `true` | Determine whether `conventional pr` should attempt to automatically close invalid pull requests. |
| `template` | `false` | `` | Comment template to use when commenting on invalid pull requests. Fill with an empty string to disable further comments. |
| `template` | `false` | `''` | Comment template to use when commenting on invalid pull requests. Fill with an empty string to disable further comments. |
| `label` | `false` | `conventional pr:invalid` | Label to use when marking invalid pull requests. If it doesn't exist, this action will automatically create it. Fill with an empty string to disable labeling. |
| `draft` | `false` | `true` | Determine whether `conventional pr` should skip validating draft pull requests. |
| `strict` | `false` | `true` | Determine whether the restrictions should apply to repository administrators. |
| `bot` | `false` | `true` | Determines whether checks should be skipped on PRs that is created by bots. Useful when relying on bots like [dependabot](https://github.com/dependabot) |
| `title_pattern` | `false` | `([\w\-]+)(\([\w\-]+\))?!?: [\w\s:\-]+` | Valid pull request title regex pattern in Perl syntax. Defaults to the [conventional commit style](https://www.conventionalcommits.org/en/v1.0.0/) commit messages. Fill with an empty string to disabled pull request title validation. |
| `commit_pattern` | `false` | `` | Valid pull request commit messages regex pattern in Perl syntax. Fill with an empty string to disabled commit message validation. |
| `branch_pattern` | `false` | `` | Valid pull request branch name regex pattern in Perl syntax. Fill with an empty string to disabled branch name validation. |
| `commit_pattern` | `false` | `''` | Valid pull request commit messages regex pattern in Perl syntax. Fill with an empty string to disabled commit message validation. |
| `branch_pattern` | `false` | `''` | Valid pull request branch name regex pattern in Perl syntax. Fill with an empty string to disabled branch name validation. |
| `body` | `false` | `true` | Determine whether a valid pull request should always have a non-empty body. |
| `issue` | `false` | `true` | Determine whether a valid pull request should always have an issue or pull requests references on it. |
| `maximum_file_change` | `false` | `0` | Limits how many file can be changed per one pull request. Fill with zero to disable this feature. |
| `verified_commits` | `false` | `false` | Require all commits on the pull request to be [signed commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) |
| `ignored_users` | `false` | `''` | GitHub usernames to be whitelisted from pull request validation. Must be a comma-separated string. Example: `Namchee, foo, bar` will bypass pull request validation for users `Namchee`, `foo`, `bar`. Case-sensitive.

## Supported Events

Expand All @@ -116,20 +117,20 @@ Ideally, Conventional PR workflow should only triggered when an event related to
## Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/Namchee"><img src="https://avatars.githubusercontent.com/u/32661241?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cristopher</b></sub></a><br /><a href="https://github.com/Namchee/conventional-pr/commits?author=Namchee" title="Code">💻</a> <a href="https://github.com/Namchee/conventional-pr/issues?q=author%3ANamchee" title="Bug reports">🐛</a> <a href="https://github.com/Namchee/conventional-pr/commits?author=Namchee" title="Documentation">📖</a> <a href="#example-Namchee" title="Examples">💡</a> <a href="#ideas-Namchee" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/smutel"><img src="https://avatars.githubusercontent.com/u/12967891?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Samuel Mutel</b></sub></a><br /><a href="https://github.com/Namchee/conventional-pr/issues?q=author%3Asmutel" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/BinToss"><img src="https://avatars.githubusercontent.com/u/7243190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Noah Sherwin</b></sub></a><br /><a href="https://github.com/Namchee/conventional-pr/issues?q=author%3ABinToss" title="Bug reports">🐛</a> <a href="#ideas-BinToss" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
</table>

<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/Namchee"><img src="https://avatars.githubusercontent.com/u/32661241?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cristopher</b></sub></a><br /><a href="https://github.com/Namchee/conventional-pr/commits?author=Namchee" title="Code">💻</a> <a href="https://github.com/Namchee/conventional-pr/issues?q=author%3ANamchee" title="Bug reports">🐛</a> <a href="https://github.com/Namchee/conventional-pr/commits?author=Namchee" title="Documentation">📖</a> <a href="#example-Namchee" title="Examples">💡</a> <a href="#ideas-Namchee" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/smutel"><img src="https://avatars.githubusercontent.com/u/12967891?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Samuel Mutel</b></sub></a><br /><a href="https://github.com/Namchee/conventional-pr/issues?q=author%3Asmutel" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/BinToss"><img src="https://avatars.githubusercontent.com/u/7243190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Noah Sherwin</b></sub></a><br /><a href="https://github.com/Namchee/conventional-pr/issues?q=author%3ABinToss" title="Bug reports">🐛</a> <a href="#ideas-BinToss" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
</table>

<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://allcontributors.org) specification.
Expand Down
10 changes: 7 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ inputs:
description: "GitHub account access token"
required: true
close:
description: "Determine whether checks should close invalid PRs"
description: "Determine whether checks should close invalid pull requests"
required: false
default: true
label:
description: "Invalid PRs label"
description: "Invalid pull requests label"
required: false
default: "cpr:invalid"
template:
description: "Comment template on invalid PRs"
description: "Comment template on invalid pull requests"
required: false
default: ""
draft:
Expand Down Expand Up @@ -57,6 +57,10 @@ inputs:
description: "Require all commits to be verified or signed with GPG keys"
required: false
default: false
ignored_users:
description: "GitHub usernames to be whitelisted from pull request validation"
required: false
default: ""
runs:
using: docker
image: "Dockerfile"
Expand Down
1 change: 1 addition & 0 deletions internal/constants/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ const (
BotWhitelistName = "Pull request is submitted by a bot and should be ignored"
DraftWhitelistName = "Pull request is a draft and should be ignored"
PermissionWhitelistName = "Pull request is submitted by administrators and should be ignored"
UsernameWhitelistName = "Pull request is made by a whitelisted user and should be ignored"
)
4 changes: 4 additions & 0 deletions internal/entity/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Config struct {
Body bool
Bot bool
Verified bool
IgnoredUsers []string
}

// ReadConfig reads environment variables for input values which are supplied
Expand Down Expand Up @@ -71,6 +72,8 @@ func ReadConfig() (*Config, error) {
return nil, constants.ErrNegativeFileChange
}

ignoredUsers := utils.ReadEnvStringArray("INPUT_IGNORED_USERS")

return &Config{
Token: token,
Draft: draft,
Expand All @@ -87,5 +90,6 @@ func ReadConfig() (*Config, error) {
Template: template,
FileChanges: fileChanges,
Verified: verified,
IgnoredUsers: ignoredUsers,
}, nil
}
14 changes: 8 additions & 6 deletions internal/entity/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ func TestReadConfig(t *testing.T) {
"INPUT_BOT": "false",
"INPUT_MAXIMUM_FILE_CHANGES": "11",
"INPUT_VERIFIED_COMMITS": "true",
"INPUT_IGNORED_USERS": "Namchee, snyk-bot",
},
want: expected{
config: &Config{
Token: "foo_bar",
Draft: false,
Issue: true,
Bot: false,
FileChanges: 11,
Verified: true,
Token: "foo_bar",
Draft: false,
Issue: true,
Bot: false,
FileChanges: 11,
Verified: true,
IgnoredUsers: []string{"Namchee", "snyk-bot"},
},
err: nil,
},
Expand Down
24 changes: 23 additions & 1 deletion internal/utils/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package utils
import (
"os"
"strconv"
"strings"
)

// ReadEnvBool read and parse boolean environment variables.
Expand All @@ -24,7 +25,7 @@ func ReadEnvString(key string) string {
return os.Getenv(key)
}

// ReadEnvInt read and integer environment variables.
// ReadEnvInt read an integer environment variables.
// Will return `0` if the variable is not a `bool`
func ReadEnvInt(key string) int {
value := os.Getenv(key)
Expand All @@ -36,3 +37,24 @@ func ReadEnvInt(key string) int {

return parsed
}

// ReadEnvStringArray read an array of string environment variables.
// Must be comma-separated
// Automatically trims all values
func ReadEnvStringArray(key string) []string {
raw := os.Getenv(key)

if len(raw) == 0 {
return []string{}
}

tokens := strings.Split(raw, ",")

var value []string

for i := range tokens {
value = append(value, strings.TrimSpace(tokens[i]))
}

return value
}
35 changes: 35 additions & 0 deletions internal/utils/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,38 @@ func TestReadEnvInt(t *testing.T) {
})
}
}

func TestReadEnvStringArray(t *testing.T) {
tests := []struct {
name string
mockValue string
want []string
}{
{
name: "should return an empty array",
mockValue: "",
want: []string{},
},
{
name: "should return an array with one member",
mockValue: "Namchee",
want: []string{"Namchee"},
},
{
name: "should return an array",
mockValue: "Namchee, Foo, Bar",
want: []string{"Namchee", "Foo", "Bar"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
os.Setenv("TEST", tc.mockValue)
defer os.Unsetenv("TEST")

got := ReadEnvStringArray("TEST")

assert.Equal(t, tc.want, got)
})
}
}
1 change: 1 addition & 0 deletions internal/whitelist/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var (
NewBotWhitelist,
NewDraftWhitelist,
NewPermissionWhitelist,
NewUsernameWhitelist,
}
)

Expand Down
2 changes: 1 addition & 1 deletion internal/whitelist/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestWhitelistGroup(t *testing.T) {

got := whitelistGroup.Process(pullRequest)

assert.Equal(t, 3, len(got))
assert.Equal(t, 4, len(got))
}

func TestIsWhitelisted(t *testing.T) {
Expand Down
42 changes: 42 additions & 0 deletions internal/whitelist/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package whitelist

import (
"github.com/Namchee/conventional-pr/internal"
"github.com/Namchee/conventional-pr/internal/constants"
"github.com/Namchee/conventional-pr/internal/entity"
"github.com/Namchee/conventional-pr/internal/utils"
"github.com/google/go-github/v32/github"
)

type usernameWhitelist struct {
client internal.GithubClient
config *entity.Config
Name string
}

// NewUsernameWhitelist creates a whitelist that bypasses checks for certain usernames
func NewUsernameWhitelist(client internal.GithubClient, config *entity.Config, _ *entity.Meta) internal.Whitelist {
return &usernameWhitelist{
client: client,
config: config,
Name: constants.UsernameWhitelistName,
}
}

func (w *usernameWhitelist) IsWhitelisted(pullRequest *github.PullRequest) *entity.WhitelistResult {
if len(w.config.IgnoredUsers) == 0 {
return &entity.WhitelistResult{
Name: w.Name,
Active: false,
Result: false,
}
}

user := pullRequest.GetUser().GetLogin()

return &entity.WhitelistResult{
Name: w.Name,
Active: true,
Result: utils.ContainsString(w.config.IgnoredUsers, user),
}
}
81 changes: 81 additions & 0 deletions internal/whitelist/user_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package whitelist

import (
"testing"

"github.com/Namchee/conventional-pr/internal/constants"
"github.com/Namchee/conventional-pr/internal/entity"
"github.com/Namchee/conventional-pr/internal/mocks"
"github.com/google/go-github/v32/github"
"github.com/stretchr/testify/assert"
)

func TestUsernameWhitelist_IsWhitelisted(t *testing.T) {
type args struct {
name string
config []string
}
tests := []struct {
name string
args args
want *entity.WhitelistResult
}{
{
name: "should be skipped if config is empty",
args: args{
name: "foo",
config: []string{},
},
want: &entity.WhitelistResult{
Name: constants.UsernameWhitelistName,
Active: false,
Result: false,
},
},
{
name: "should be checked if user is not on whitelist",
args: args{
name: "foo",
config: []string{"bar"},
},
want: &entity.WhitelistResult{
Name: constants.UsernameWhitelistName,
Active: true,
Result: false,
},
},
{
name: "should be skipped if user is on whitelist",
args: args{
name: "bar",
config: []string{"bar"},
},
want: &entity.WhitelistResult{
Name: constants.UsernameWhitelistName,
Active: true,
Result: true,
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
user := &github.User{
Login: &tc.args.name,
}
pull := &github.PullRequest{
User: user,
}
config := &entity.Config{
IgnoredUsers: tc.args.config,
}
client := mocks.NewGithubClientMock()

whitelister := NewUsernameWhitelist(client, config, nil)

got := whitelister.IsWhitelisted(pull)

assert.Equal(t, got, tc.want)
})
}
}

0 comments on commit eec0dba

Please sign in to comment.