diff --git a/.gitignore b/.gitignore index a30baa4..4b40cb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ scripts/ .idea codeowners-validator +dist/ \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index 8d4f5f6..39fcf3b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -8,6 +8,8 @@ builds: goarch: - amd64 - 386 + ldflags: + - -s -w -X github.com/mszostok/codeowners-validator/pkg/version.version={{.Version}} -X github.com/mszostok/codeowners-validator/pkg/version.commit={{.ShortCommit}} -X github.com/mszostok/codeowners-validator/pkg/version.buildDate={{.Date}} archive: replacements: darwin: Darwin diff --git a/Gopkg.lock b/Gopkg.lock index f03da4c..b5d7eb2 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -116,6 +116,14 @@ revision = "a5d6946387efe7d64d09dcba68cdd523dc1273a3" version = "v1.2.0" +[[projects]] + digest = "1:524b71991fc7d9246cc7dc2d9e0886ccb97648091c63e30eef619e6862c955dd" + name = "github.com/spf13/pflag" + packages = ["."] + pruneopts = "UT" + revision = "2e9d26c8c37aae03e3f9d4e90b7116f5accb7cab" + version = "v1.0.5" + [[projects]] digest = "1:5da8ce674952566deae4dbc23d07c85caafc6cfa815b0b3e03e41979cedb8750" name = "github.com/stretchr/testify" @@ -247,6 +255,7 @@ "github.com/pkg/errors", "github.com/sirupsen/logrus", "github.com/spf13/afero", + "github.com/spf13/pflag", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", "github.com/vrischmann/envconfig", diff --git a/README.md b/README.md index 976c76b..376ad17 100644 --- a/README.md +++ b/README.md @@ -7,57 +7,90 @@ The Codeowners Validator project validates the GitHub [CODEOWNERS](https://help.github.com/articles/about-code-owners/) file. It supports private GitHub repositories and GitHub Enterprise installations. -Executed checks: -* [x] [EXPERIMENTAL] Find unowned files (owners not specified for given files) -* [x] Find duplicated patterns -* [x] Find files/directories that do not exist in a given repository -* [x] Validate owners: - * [x] check if the owner definition is valid (is either a GitHub user name, an organization team name, or an email address) - * [x] check if a GitHub owner has a GitHub account - * [x] check if a GitHub owner is in a given organization - * [x] check if an organization team exists - -## Local Installation - -`env GO111MODULE=off go get -u github.com/mszostok/codeowners-validator` +![usage](./docs/assets/usage.png) -## Usage +## Installation -![usage](./docs/assets/usage.png) +It's highly recommended to install a fixed version of ` codeowners-validator`. Releases are available on the [releases page](https://github.com/mszostok/codeowners-validator/releases). + +#### From Release + +Here is the recommended way to install `codeowners-validator`: + +```bash +# binary installed into ./bin/ +curl -sfL https://raw.githubusercontent.com/mszostok/codeowners-validator/master/install.sh| sh -s v0.1.1 + +# binary installed into $(go env GOPATH)/bin/codeowners-validator +curl -sfL https://raw.githubusercontent.com/mszostok/codeowners-validator/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v0.1.1 + +# In alpine linux (as it does not come with curl by default) +wget -O - -q https://raw.githubusercontent.com/mszostok/codeowners-validator/master/install.sh| sh -s v0.2.0 + +# Print version. Add `--short` to print just the version number. +codeowners-validator -v +``` + +You can also download [latest version](https://github.com/mszostok/codeowners-validator/releases/latest) from release page manually. + +#### From Sources + +You can install `codeowners-validator` with `env GO111MODULE=off go get -u github.com/mszostok/codeowners-validator`. + +> NOTE: please use the latest go to do this, ideally go 1.12 or greater. + +This will put `codeowners-validator` in `$(go env GOPATH)/bin` + +## Checks + +The following checks are enabled by default: + +| Name | Description | +|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| duppatterns | **[Duplicated Pattern Checker]**

Reports if CODEOWNERS file contain duplicated lines with the same file pattern. | +| files | **[File Exist Checker]**

Reports if CODEOWNERS file contain lines with the file pattern that do not exist in a given repository. | +| owners | **[Valid Owner Checker]**

Reports if CODEOWNERS file contain invalid owners definition. Allowed owner syntax: `@username`, `@org/team-name` or `user@example.com`
_source: https://help.github.com/articles/about-code-owners/#codeowners-syntax_.

**Checks:**
1. Check if the owner's definition is valid (is either a GitHub user name, an organization team name or an email address).

2. Check if a GitHub owner has a GitHub account

3. Check if a GitHub owner is in a given organization

4. Check if an organization team exists | + +The experimental checks are disabled by default: + +| Name | Description | +|----------|---------------------------------------------------------------------------------------------------------------------------------------------| +| notowned | **[Not Owned File Checker]**

Reports if a given repository contain files that do not have specified owners in CODEOWNERS file. | + +To enable experimental check set `EXPERIMENTAL_CHECKS=notowned` environment variable. + +Check the [Usage](#usage) section for more info on how to enable and configure given checks. + +## Usage Use the following environment variables to configure the application: -| Name | Required | Default | Description | -|-----|:---------:|:--------|:------------| -| **REPOSITORY_PATH** | Yes | | The repository path to your repository on your local machine. | -| **GITHUB_ACCESS_TOKEN** | No | | The GitHub access token. Instruction for creating token can be found [here](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/#creating-a-token). 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. | -| **GITHUB_BASE_URL** | No | https://api.github.com/ | 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. | -| **GITHUB_UPLOAD_URL** | No | https://uploads.github.com/ | 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. | -| **CHECKS** | No | - | The list of checks that will be executed. By default the all checks are executed. Possible values: `files`,`owner`,`duppattern` | -| **EXPERIMENTAL_CHECKS** | No | - | The comma separated list of experimental checks that should be executed. By default all experimental checks are turn off. Possible values: `owners`.| -| **CHECK_FAILURE_LEVEL** | No | `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`. | -| **OWNER_CHECKER_ORGANIZATION_NAME** | Yes | | The organization name where the repository is created. Used to check if GitHub owner is in the given organization. | -| **NOT_OWNED_CHECKER_SKIP_PATTERNS** | No | - | 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` | +| Name | Default | Description | +|-----|:--------|:------------| +| REPOSITORY_PATH * | | The repository path to your repository on your local machine. | +| GITHUB_ACCESS_TOKEN| | The GitHub access token. Instruction for creating a token can be found [here](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/#creating-a-token). 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. | +| GITHUB_BASE_URL| https://api.github.com/ | 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. | +| GITHUB_UPLOAD_URL | https://uploads.github.com/ | 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. | +| CHECKS| - | The list of checks that will be executed. By default, all checks are executed. Possible values: `files`,`owners`,`duppatterns` | +| EXPERIMENTAL_CHECKS | - | The comma-separated list of experimental checks that should be executed. By default, all experimental checks are turned off. Possible values: `notowned`.| +| CHECK_FAILURE_LEVEL | `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`. | +| OWNER_CHECKER_ORGANIZATION_NAME *| | The organization name where the repository is created. Used to check if GitHub owner is in the given organization. | +| NOT_OWNED_CHECKER_SKIP_PATTERNS| - | 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 -### Exit status codes +#### Exit status codes Application exits with different status codes which allow you to easily distinguish between error categories. | Code | Description | |:-----:|:------------| -| **1** | The application startup failed due to wrong configuration or internal error. | -| **2** | The application was closed because the OS sends termination signal (SIGINT or SIGTERM). | +| **1** | The application startup failed due to the wrong configuration or internal error. | +| **2** | The application was closed because the OS sends a termination signal (SIGINT or SIGTERM). | | **3** | The CODEOWNERS validation failed - executed checks found some issues. | ## Roadmap -_Sorted with priority. First - most important._ +The [codeowners-validator roadmap uses Github milestones](https://github.com/mszostok/codeowners-validator/milestones) to track the progress of the project. -* [ ] Possibility to execute validator online. Automatically integrates with your GitHub account and allows you to check any repository online without the need to download and execute binary locally. -* [ ] Possibility to use the GitHub URL instead of the path to the local repository. -* [ ] Offline mode - execute all checks which not require internet connection against your local repository -* [ ] Investigate the [Go Plugins](https://golang.org/pkg/plugin/). Implement if it will simplify extending this tool with other checks. -* [ ] Move to [cobra](https://github.com/spf13/cobra/) library. -* [ ] Add test coverage. -* [ ] Add support for configuration via YAML file. -* [ ] Move dep to go modules +Issues with the `priority/important-longterm` label will be implemented as the first one. diff --git a/docs/assets/usage.png b/docs/assets/usage.png index df26ad2..b6b2f2d 100644 Binary files a/docs/assets/usage.png and b/docs/assets/usage.png differ diff --git a/hack/ci/run-lint.sh b/hack/ci/run-lint.sh index 0c51c42..51e329f 100755 --- a/hack/ci/run-lint.sh +++ b/hack/ci/run-lint.sh @@ -30,7 +30,7 @@ golangci::run_checks() { ENABLE=$(sed 's/ /,/g' <<< "${LINTS[@]}") - golangci-lint --disable-all --enable="${ENABLE}" run + golangci-lint --disable-all --enable="${ENABLE}" run ./... echo -e "${GREEN}√ run golangci-lint${NC}" } diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..1182a54 --- /dev/null +++ b/install.sh @@ -0,0 +1,392 @@ +#!/bin/sh +set -e +# Code generated by godownloader on 2019-11-12T14:20:06Z. DO NOT EDIT. +# + +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +log_prefix() { + echo "$0" +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + cygwin_nt*) os="windows" ;; + mingw*) os="windows" ;; + msys_nt*) os="windows" ;; + esac + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo ${arch} +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +untar() { + tarball=$1 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" ;; + *.zip) unzip "${tarball}" ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} +http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") + else + code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") + fi + if [ "$code" != "200" ]; then + log_debug "http_download_curl received HTTP status $code" + return 1 + fi + return 0 +} +http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi +} +http_download() { + log_debug "http_download $2" + if is_command curl; then + http_download_curl "$@" + return + elif is_command wget; then + http_download_wget "$@" + return + fi + log_crit "http_download unable to find wget or curl" + return 1 +} +http_copy() { + tmp=$(mktemp) + http_download "${tmp}" "$1" "$2" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} +hash_sha256() { + TARGET=${1:-/dev/stdin} + if is_command gsha256sum; then + hash=$(gsha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command sha256sum; then + hash=$(sha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command shasum; then + hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f a + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} +hash_sha256_verify() { + TARGET=$1 + checksums=$2 + if [ -z "$checksums" ]; then + log_err "hash_sha256_verify checksum file not specified in arg2" + return 1 + fi + BASENAME=${TARGET##*/} + want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) + if [ -z "$want" ]; then + log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" + return 1 + fi + got=$(hash_sha256 "$TARGET") + if [ "$want" != "$got" ]; then + log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" + return 1 + fi +} +cat /dev/null <