Skip to content

Commit

Permalink
feat: add checks interface (siderolabs#127)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Rynhard <andrew@andrewrynhard.com>
  • Loading branch information
andrewrynhard authored Jun 29, 2019
1 parent 0af31f8 commit 6f8751c
Show file tree
Hide file tree
Showing 16 changed files with 583 additions and 220 deletions.
26 changes: 17 additions & 9 deletions .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ kind: pipeline
name: default

services:
- name: docker
image: docker:dind
privileged: true
volumes:
- name: dockersock
path: /var/run
- name: docker
image: docker:dind
privileged: true
volumes:
- name: dockersock
path: /var/run

steps:
- name: enforce
- name: build
image: autonomy/build-container:latest
pull: always
commands:
- make build
volumes:
- name: dockersock
path: /var/run

- name: conform
image: autonomy/build-container:latest
pull: always
commands:
- build/conform-linux-amd64 enforce
volumes:
- name: dockersock
Expand Down Expand Up @@ -59,5 +67,5 @@ steps:
- push

volumes:
- name: dockersock
temp: {}
- name: dockersock
temp: {}
10 changes: 8 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
ARG GOLANG_IMAGE
FROM ${GOLANG_IMAGE} AS common

ENV CGO_ENABLED 0
ENV GO111MODULES on

WORKDIR /conform
COPY go.mod ./
COPY go.sum ./
Expand Down Expand Up @@ -34,7 +32,15 @@ COPY ./hack ./hack
RUN chmod +x ./hack/test.sh
RUN ./hack/test.sh --all

FROM alpine:3.9 as ca-certificates
RUN apk add --update --no-cache ca-certificates

FROM scratch AS image
LABEL com.github.actions.name="Conform"
LABEL com.github.actions.description="Policy enforcement for your pipelines."
LABEL com.github.actions.icon="shield"
LABEL com.github.actions.color="yellow"
COPY --from=ca-certificates /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /conform-linux-amd64 /conform
ENTRYPOINT [ "/conform" ]
CMD [ "enforce" ]
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ SHA := $(shell gitmeta git sha)
TAG := $(shell gitmeta image tag)
BUILT := $(shell gitmeta built)

GOLANG_IMAGE ?= golang:1.12.3
GOLANG_IMAGE ?= golang:1.12.6

COMMON_ARGS := -f ./Dockerfile --build-arg GOLANG_IMAGE=$(GOLANG_IMAGE) --build-arg SHA=$(SHA) --build-arg TAG=$(TAG) --build-arg BUILT="$(BUILT)" .

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ require (
github.com/fsnotify/fsnotify v1.4.2 // indirect
github.com/gliderlabs/ssh v0.1.1 // indirect
github.com/google/go-cmp v0.2.0 // indirect
github.com/google/go-github v17.0.0+incompatible
github.com/google/go-querystring v1.0.0 // indirect
github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e h1:KJWs1uTCkN3E/J5ofCH9Pf8KKsibTFc3fv0CA9+WsVo=
github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
Expand Down
1 change: 0 additions & 1 deletion hack/golangci-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ linters:
disable-all: false
fast: false


issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
Expand Down
38 changes: 19 additions & 19 deletions internal/enforcer/enforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package enforcer
import (
"fmt"
"io/ioutil"
"log"
"os"
"text/tabwriter"

Expand Down Expand Up @@ -59,45 +60,44 @@ func (c *Conform) Enforce(setters ...policy.Option) {

const padding = 8
w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, ' ', 0)
fmt.Fprintln(w, "POLICY\tSTATUS\tMESSAGE\t")
fmt.Fprintln(w, "POLICY\tCHECK\tSTATUS\tMESSAGE\t")

var failed bool
pass := true
for _, p := range c.Policies {
if errs := c.enforce(p, opts); errs != nil {
failed = true
for _, err := range errs {
fmt.Fprintf(w, "%s\t%s\t%v\t\n", p.Type, "FAILED", err)
report, err := c.enforce(p, opts)
if err != nil {
log.Fatal(err)
}
for _, check := range report.Checks() {
if len(check.Errors()) != 0 {
for _, err := range check.Errors() {
fmt.Fprintf(w, "%s\t%s\t%s\t%v\t\n", p.Type, check.Name(), "FAILED", err)
}
} else {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n", p.Type, check.Name(), "PASS", "<none>")
}
} else {
fmt.Fprintf(w, "%s\t%s\t%s\t\n", p.Type, "PASS", "<none>")
}
}

// nolint: errcheck
w.Flush()

if failed {
if !pass {
os.Exit(1)
}
}

func (c *Conform) enforce(declaration *PolicyDeclaration, opts *policy.Options) []error {
func (c *Conform) enforce(declaration *PolicyDeclaration, opts *policy.Options) (*policy.Report, error) {
if _, ok := policyMap[declaration.Type]; !ok {
return []error{errors.Errorf("Policy %q is not defined", declaration.Type)}
return nil, errors.Errorf("Policy %q is not defined", declaration.Type)
}

p := policyMap[declaration.Type]

err := mapstructure.Decode(declaration.Spec, p)
if err != nil {
return []error{errors.Errorf("Internal error: %v", err)}
}

report := p.Compliance(opts)

if !report.Valid() {
return report.Errors
return nil, errors.Errorf("Internal error: %v", err)
}

return nil
return p.Compliance(opts)
}
113 changes: 113 additions & 0 deletions internal/policy/commit/check_conventional_commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package commit

import (
"regexp"
"strings"

"github.com/autonomy/conform/internal/policy"
"github.com/pkg/errors"
)

// Conventional implements the policy.Policy interface and enforces commit
// messages to conform the Conventional Commit standard.
type Conventional struct {
Types []string `mapstructure:"types"`
Scopes []string `mapstructure:"scopes"`
}

// HeaderRegex is the regular expression used for Conventional Commits
// 1.0.0-beta.1.
var HeaderRegex = regexp.MustCompile(`^(\w*)(\(([^)]+)\))?:\s{1}(.*)($|\n{2})`)

const (
// TypeFeat is a commit of the type fix patches a bug in your codebase
// (this correlates with PATCH in semantic versioning).
TypeFeat = "feat"

// TypeFix is a commit of the type feat introduces a new feature to the
// codebase (this correlates with MINOR in semantic versioning).
TypeFix = "fix"
)

// ConventionalCommitCheck ensures that the commit message is a valid
// conventional commit.
type ConventionalCommitCheck struct {
errors []error
}

// Name returns the name of the check.
func (c ConventionalCommitCheck) Name() string {
return "Conventional Commit"
}

// Message returns to check message.
func (c ConventionalCommitCheck) Message() string {
if len(c.errors) != 0 {
return c.errors[0].Error()
}
return "Commit message is a valid conventional commit"
}

// Errors returns any violations of the check.
func (c ConventionalCommitCheck) Errors() []error {
return c.errors
}

// ValidateConventionalCommit returns the commit type.
// nolint: gocyclo
func (c Commit) ValidateConventionalCommit() policy.Check {
check := &ConventionalCommitCheck{}
groups := parseHeader(c.msg)
if len(groups) != 6 {
check.errors = append(check.errors, errors.Errorf("Invalid conventional commits format: %q", c.msg))
return check
}

c.Conventional.Types = append(c.Conventional.Types, TypeFeat, TypeFix)
typeIsValid := false
for _, t := range c.Conventional.Types {
if t == groups[1] {
typeIsValid = true
}
}
if !typeIsValid {
check.errors = append(check.errors, errors.Errorf("Invalid type %q: allowed types are %v", groups[1], c.Conventional.Types))
return check
}

// Scope is optional.
if groups[3] != "" {
scopeIsValid := false
for _, scope := range c.Conventional.Scopes {
if scope == groups[3] {
scopeIsValid = true
break
}
}
if !scopeIsValid {
check.errors = append(check.errors, errors.Errorf("Invalid scope %q: allowed scopes are %v", groups[3], c.Conventional.Scopes))
return check
}
}

if len(groups[4]) <= 72 && len(groups[4]) != 0 {
return check
}
check.errors = append(check.errors, errors.Errorf("Invalid description: %s", groups[4]))

return check
}

func parseHeader(msg string) []string {
// To circumvent any policy violation due to the leading \n that GitHub
// prefixes to the commit message on a squash merge, we remove it from the
// message.
header := strings.Split(strings.TrimPrefix(msg, "\n"), "\n")[0]
groups := HeaderRegex.FindStringSubmatch(header)

return groups
}
54 changes: 54 additions & 0 deletions internal/policy/commit/check_dco.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package commit

import (
"regexp"
"strings"

"github.com/autonomy/conform/internal/policy"
"github.com/pkg/errors"
)

// DCORegex is the regular expression used for Developer Certificate of Origin.
var DCORegex = regexp.MustCompile(`^Signed-off-by: ([^<]+) <([^<>@]+@[^<>]+)>$`)

// DCOCheck ensures that the commit message contains a
// Developer Certificate of Origin.
type DCOCheck struct {
errors []error
}

// Name returns the name of the check.
func (d DCOCheck) Name() string {
return "DCO"
}

// Message returns to check message.
func (d DCOCheck) Message() string {
if len(d.errors) != 0 {
return d.errors[0].Error()
}
return "Developer Certificate of Origin was found"
}

// Errors returns any violations of the check.
func (d DCOCheck) Errors() []error {
return d.errors
}

// ValidateDCO checks the commit message for a Developer Certificate of Origin.
func (c Commit) ValidateDCO() policy.Check {
check := &DCOCheck{}
for _, line := range strings.Split(c.msg, "\n") {
if DCORegex.MatchString(strings.TrimSpace(line)) {
return check
}
}

check.errors = append(check.errors, errors.Errorf("Commit does not have a DCO"))

return check
}
Loading

0 comments on commit 6f8751c

Please sign in to comment.