From 936b64e1b6169a28ab254455df3457366d04a5fa Mon Sep 17 00:00:00 2001 From: Andrew Rynhard Date: Fri, 21 Jul 2017 19:54:57 -0700 Subject: [PATCH] refactor(*): complete rewrite (#20) --- .travis.yml | 17 +- Gopkg.lock | 2 +- README.md | 60 ++--- cmd/enforce.go | 32 +-- cmd/version.go | 6 +- conform.yaml | 195 +++++++-------- conform/config/config.go | 59 ----- conform/conform.go | 99 ++++++++ conform/enforce.go | 224 ------------------ conform/enforce_test.go | 80 ------- conform/git/git.go | 150 ++---------- conform/git/policy.go | 96 -------- conform/metadata/metadata.go | 153 ++++++++++++ conform/pipeline/pipeline.go | 95 ++++++++ .../conventionalcommit/conventionalcommit.go | 99 ++++++++ .../conventionalcommit_test.go | 130 ++++++++++ conform/policy/policy.go | 27 ++- conform/policy/version/version.go | 5 + conform/renderer/renderer.go | 27 +++ conform/script/script.go | 49 ++++ conform/stage/stage.go | 7 + conform/task/task.go | 24 ++ conform/utilities/utilities.go | 33 ++- conform/{ => version}/version.go | 2 +- scripts/deploy.sh | 8 - scripts/lint.sh | 17 ++ scripts/test.sh | 10 +- 27 files changed, 908 insertions(+), 798 deletions(-) delete mode 100644 conform/config/config.go create mode 100644 conform/conform.go delete mode 100644 conform/enforce.go delete mode 100644 conform/enforce_test.go delete mode 100644 conform/git/policy.go create mode 100644 conform/metadata/metadata.go create mode 100644 conform/pipeline/pipeline.go create mode 100644 conform/policy/conventionalcommit/conventionalcommit.go create mode 100644 conform/policy/conventionalcommit/conventionalcommit_test.go create mode 100644 conform/policy/version/version.go create mode 100644 conform/renderer/renderer.go create mode 100644 conform/script/script.go create mode 100644 conform/stage/stage.go create mode 100644 conform/task/task.go rename conform/{ => version}/version.go (98%) delete mode 100644 scripts/deploy.sh create mode 100644 scripts/lint.sh diff --git a/.travis.yml b/.travis.yml index 54060681..3818bee5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,22 +21,11 @@ install: - sudo apt-get update - sudo apt-get -y install docker-ce - go get github.com/autonomy/conform + - go get github.com/golang/dep/cmd/dep + - dep ensure script: - - conform enforce test - - conform enforce image + - conform enforce after_success: - bash <(curl -s https://codecov.io/bash) - -deploy: - - provider: script - script: chmod +x scripts/deploy.sh && scripts/deploy.sh - skip_cleanup: true - on: - branch: master - - provider: script - script: chmod +x scripts/deploy.sh && scripts/deploy.sh - skip_cleanup: true - on: - tags: true diff --git a/Gopkg.lock b/Gopkg.lock index 135190b4..ec2a6b11 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -238,6 +238,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "cd1c71fc1c1241669bffe6e2c89adbecdb5a51432c41da8eac90c80c8cdde661" + inputs-digest = "270f7799e97cb455cf2ec2f61a73122a936bf5453af158a4febd93bcda95af0b" solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index d6a9b592..f40d5358 100644 --- a/README.md +++ b/README.md @@ -31,50 +31,34 @@ Create a file named `conform.yaml` with the following contents: ```yaml metadata: repository: example -scripts: - init : | - #!/bin/bash - - set -e - - echo "Initialize any dependencies here." - deploy : | - #!/bin/bash - - set -e - - echo "Deploy you image here." -templates: - build: | - FROM alpine:latest as build - RUN echo "Run your build here." - RUN touch artifact - test: | - FROM alpine:latest as test - COPY --from=build artifact . - RUN echo "Run your tests here." - image: | - FROM scratch as image - RUN echo "Prepare your final image here." - COPY --from=build artifact . -rules: - image: - before: - - init - templates: - - build - - test - - image - after: - - deploy +policies: + - type: conventionalCommit + spec: + types: + - "type" + scopes: + - "scope" + - type: branch + spec: + name: master + pipelines: + - name: example +pipelines: + example: + stages: + - example +stages: + example: + template: | + FROM scratch ``` In the same directory, run: ``` -$ conform enforce image +$ conform enforce ``` -Devloping Conform +Developing Conform ---------------- ### License diff --git a/cmd/enforce.go b/cmd/enforce.go index 1374bb15..a5f8c08b 100644 --- a/cmd/enforce.go +++ b/cmd/enforce.go @@ -28,23 +28,24 @@ var enforceCmd = &cobra.Command{ Use: "enforce", Short: "", Long: ``, - RunE: func(cmd *cobra.Command, args []string) (err error) { - if len(args) != 1 { - err = fmt.Errorf("Invalid arguments %v", args) + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + err := fmt.Errorf("The enforce command does not take arguments") return err } - err = checkDockerVersion() - if err != nil { - return + if err := checkDockerVersion(); err != nil { + return err } - e, err := conform.NewEnforcer(args[0]) + c, err := conform.New() if err != nil { - return + return err + } + if err = c.Enforce(); err != nil { + return err } - err = e.ExecuteRule() - return + return nil }, } @@ -53,19 +54,18 @@ func init() { RootCmd.Flags().BoolVar(&debug, "debug", false, "Debug rendering") } -func checkDockerVersion() (err error) { +func checkDockerVersion() error { cli, err := client.NewEnvClient() if err != nil { - return + return err } - serverVersion, err := cli.ServerVersion(context.Background()) if err != nil { - return + return err } minVersion, err := semver.NewVersion(minDockerVersion) if err != nil { - return + return err } serverSemVer := semver.MustParse(serverVersion.Version) i := serverSemVer.Compare(minVersion) @@ -75,5 +75,5 @@ func checkDockerVersion() (err error) { return err } - return + return nil } diff --git a/cmd/version.go b/cmd/version.go index a9c69abc..dde078c8 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -14,7 +14,7 @@ package cmd import ( - "github.com/autonomy/conform/conform" + "github.com/autonomy/conform/conform/version" "github.com/spf13/cobra" ) @@ -29,9 +29,9 @@ var versionCmd = &cobra.Command{ Long: ``, Run: func(cmd *cobra.Command, args []string) { if shortVersion { - conform.PrintShortVersion() + version.PrintShortVersion() } else { - conform.PrintLongVersion() + version.PrintLongVersion() } }, } diff --git a/conform.yaml b/conform.yaml index aef24491..6a96ed09 100644 --- a/conform.yaml +++ b/conform.yaml @@ -1,124 +1,101 @@ metadata: repository: autonomy/conform -default: image - policies: - git: - types: - - "docs" - - "style" - - "refactor" - - "perf" - - "test" - - "chore" - scopes: - - "ci" - - "docker" - - "git" - - "policy" - - "*" - -scripts: - init : | - #!/bin/bash - - set -e - - BUILDDEPS=( github.com/golang/dep/cmd/dep ) - for b in ${BUILDDEPS[@]}; do - echo "Installing $b" - go get $b - done - - cp_coverage_report: | - #!/bin/bash - - set -e - - docker run --rm -i --volume $(pwd):/out ${CONFORM_IMAGE} cp coverage.txt /out - - if [ ! -f coverage.txt ]; then - echo "No coverage report found." - exit 1 - fi - - deploy: | + - type: conventionalCommit + spec: + types: + - "docs" + - "style" + - "refactor" + - "perf" + - "test" + - "chore" + scopes: + - "ci" + - "docker" + - "git" + - "policy" + - "renderer" + - "*" + +script: + template: | #!/bin/bash set -e - if [ ${CONFORM_IS_DIRTY} == "true" ]; then - echo "The working tree is dirty." - exit 1 - fi - + {{ if not .Git.IsClean }} + echo "The working tree is dirty." + exit 1 + {{ end }} + {{ if eq .Git.Branch "master" }} docker login -u "${DOCKER_USERNAME}" -p "${DOCKER_PASSWORD}" - docker push ${CONFORM_IMAGE} - - if [ ${CONFORM_IS_TAG} == "true" ]; then - docker tag ${CONFORM_IMAGE} autonomy/conform:${CONFORM_TAG:1} - docker push autonomy/conform:${CONFORM_TAG:1} - if [ ${CONFORM_PRERELEASE} == "" ]; then - docker tag ${CONFORM_IMAGE} autonomy/conform:latest - docker push autonomy/conform:latest - fi - fi - - clean: | - #!/bin/bash - - set -e - - cat .gitignore | while read line; do rm -rf "$line"; done - dep ensure - dep prune - -templates: - build: | - FROM golang:1.8.3 as build - WORKDIR $GOPATH/src/github.com/autonomy/conform - COPY ./ ./ - RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /conform -ldflags "-X \"github.com/autonomy/conform/cmd.Tag={{ trimAll "v" .GitInfo.Tag }}\" -X \"github.com/autonomy/conform/conform.Tag={{ trimAll "v" .GitInfo.Tag }}\" -X \"github.com/autonomy/conform/conform.SHA={{ .GitInfo.SHA }}\" -X \"github.com/autonomy/conform/conform.Built={{ .Built }}\"" - test: | - FROM golang:1.8.3 as test - WORKDIR $GOPATH/src/github.com/autonomy/conform - RUN go get -u gopkg.in/alecthomas/gometalinter.v1 - RUN gometalinter.v1 --install - COPY --from=build $GOPATH/src/github.com/autonomy/conform . - RUN chmod +x ./scripts/test.sh; sync; ./scripts/test.sh - image: | - FROM alpine:3.6 as image - MAINTAINER Andrew Rynhard - RUN apk --update add bash \ - && rm -rf /var/cache/apk/* - COPY --from=build /conform /bin - ENTRYPOINT ["conform"] - -rules: - build: - templates: - - build - + docker push {{ .Docker.Image }} + {{ if .Git.IsTag }} + docker tag {{ .Docker.Image }} {{ .Repository }}:{{ .Git.Tag }} + docker push {{ .Repository }}:{{ .Git.Tag }} + {{ end }} + {{ if not .Git.IsPrerelease }} + docker tag {{ .Docker.Image }} {{ .Repository }}:latest + docker push {{ .Repository }}:latest + {{ end }} + {{ end }} + +pipeline: + stages: + - test + - lint + - build + +stages: test: - before: - - init - - clean - templates: + artifacts: + - coverage.txt + tasks: - build - test - after: - - cp_coverage_report - - image: - templates: + lint: + tasks: - build - - image - - deploy: - templates: + - lint + build: + tasks: - build - - test - image - after: - - deploy + +tasks: + build: + template: | + FROM golang:1.8.3 as build + WORKDIR $GOPATH/src/github.com/{{ .Repository }} + COPY ./ ./ + {{ if and .Git.IsClean .Git.IsTag }} + RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /conform -ldflags "-X \"github.com/{{ .Repository }}/conform/version.Tag={{ trimAll "v" .Git.Tag }}\" -X \"github.com/{{ .Repository }}/conform/version.SHA={{ .Git.SHA }}\" -X \"github.com/{{ .Repository }}/conform/version.Built={{ .Built }}\"" + {{ else if .Git.IsClean }} + RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /conform -ldflags "-X \"github.com/{{ .Repository }}/conform/version.SHA={{ .Git.SHA }}\" -X \"github.com/{{ .Repository }}/conform/version.Built={{ .Built }}\"" + {{ else }} + RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /conform + {{ end }} + test: + template: | + FROM golang:1.8.3 as test + WORKDIR $GOPATH/src/github.com/{{ .Repository }} + COPY --from=build $GOPATH/src/github.com/{{ .Repository }} . + RUN chmod +x ./scripts/test.sh; sync; ./scripts/test.sh + lint: + template: | + FROM golang:1.8.3 as lint + WORKDIR $GOPATH/src/github.com/{{ .Repository }} + RUN go get -u github.com/alecthomas/gometalinter + RUN gometalinter --install + COPY --from=build $GOPATH/src/github.com/{{ .Repository }} . + RUN chmod +x ./scripts/lint.sh; sync; ./scripts/lint.sh + image: + template: | + FROM alpine:3.6 as image + MAINTAINER Andrew Rynhard + RUN apk --update add bash \ + && rm -rf /var/cache/apk/* + COPY --from=build /conform /bin + ENTRYPOINT ["conform"] diff --git a/conform/config/config.go b/conform/config/config.go deleted file mode 100644 index 6a1d7956..00000000 --- a/conform/config/config.go +++ /dev/null @@ -1,59 +0,0 @@ -package config - -import ( - "fmt" - "io/ioutil" - - yaml "gopkg.in/yaml.v2" -) - -// Config represents the YAML. -type Config struct { - Debug bool - Default *string `yaml:"default"` - Metadata *Metadata `yaml:"metadata"` - Policies *Policies `yaml:"policies"` - Scripts map[string]string `yaml:"scripts"` - Templates map[string]string `yaml:"templates"` - Rules map[string]*Rule `yaml:"rules"` -} - -// Metadata contains metadata. -type Metadata struct { - Repository *string `yaml:"repository"` - Registry *string `yaml:"registry"` -} - -// Policies contains policies that are enforced. -type Policies struct { - Git *Git `yaml:"git"` -} - -// Git contains git specific policies. -type Git struct { - Types []string `yaml:"types"` - Scopes []string `yaml:"scopes"` -} - -// Rule contains rules. -type Rule struct { - Templates []string `yaml:"templates"` - Artifacts []string `yaml:"artifacts"` - Before []string `yaml:"before"` - After []string `yaml:"after"` -} - -// NewConfig instantiates and returns a config. -func NewConfig() (*Config, error) { - rBytes, err := ioutil.ReadFile("conform.yaml") - if err != nil { - return nil, fmt.Errorf("Unable to load conform.yaml: %v", err) - } - c := Config{} - err = yaml.Unmarshal(rBytes, &c) - if err != nil { - return nil, fmt.Errorf("Unable to load conform.yaml: %v", err) - } - - return &c, nil -} diff --git a/conform/conform.go b/conform/conform.go new file mode 100644 index 00000000..4bfd3686 --- /dev/null +++ b/conform/conform.go @@ -0,0 +1,99 @@ +package conform + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/autonomy/conform/conform/metadata" + "github.com/autonomy/conform/conform/pipeline" + "github.com/autonomy/conform/conform/policy" + "github.com/autonomy/conform/conform/policy/conventionalcommit" + "github.com/autonomy/conform/conform/script" + "github.com/autonomy/conform/conform/stage" + "github.com/autonomy/conform/conform/task" + "github.com/mitchellh/mapstructure" + + yaml "gopkg.in/yaml.v2" +) + +// Conform is a struct that conform.yaml gets decoded into. +type Conform struct { + Metadata *metadata.Metadata `yaml:"metadata"` + Policies []*PolicyDeclaration `yaml:"policies"` + Pipeline *pipeline.Pipeline `yaml:"pipeline"` + Stages map[string]*stage.Stage `yaml:"stages"` + Tasks map[string]*task.Task `yaml:"tasks"` + Script *script.Script `yaml:"script"` +} + +// PolicyDeclaration allows a user to declare an arbitrary type along with a +// spec that will be decoded into the appropriate concrete type. +type PolicyDeclaration struct { + Type string `yaml:"type"` + Spec interface{} `yaml:"spec"` +} + +// policyMap defines the set of policies allowed within Conform. +var policyMap = map[string]policy.Policy{ + "conventionalCommit": &conventionalcommit.Conventional{}, + // "version": &version.Version{}, +} + +// New loads the conform.yaml file and unmarshals it into a Conform struct. +func New() (*Conform, error) { + configBytes, err := ioutil.ReadFile("conform.yaml") + if err != nil { + return nil, err + } + c := &Conform{} + err = yaml.Unmarshal(configBytes, c) + if err != nil { + return nil, err + } + + return c, nil +} + +// Enforce enforces all policies defined in the conform.yaml file. +func (c *Conform) Enforce() error { + for _, p := range c.Policies { + err := c.enforce(p) + if err != nil { + return err + } + } + err := c.Pipeline.Build(c.Metadata, c.Stages, c.Tasks) + if err != nil { + return err + } + + return c.Script.Execute(c.Metadata) +} + +func (c *Conform) enforce(p *PolicyDeclaration) error { + if _, ok := policyMap[p.Type]; !ok { + return fmt.Errorf("Policy %q is not defined", p.Type) + } + policy := policyMap[p.Type] + err := mapstructure.Decode(p.Spec, policy) + if err != nil { + return err + } + + report := policy.Compliance( + c.Metadata, + policy.Pipeline(c.Pipeline), + policy.Tasks(c.Tasks), + ) + + if !report.Valid() { + fmt.Printf("Violation of policy %q:\n", p.Type) + for i, err := range report.Errors { + fmt.Printf("\tViolation %d: %v\n", i, err) + } + os.Exit(1) + } + + return nil +} diff --git a/conform/enforce.go b/conform/enforce.go deleted file mode 100644 index 9efc1894..00000000 --- a/conform/enforce.go +++ /dev/null @@ -1,224 +0,0 @@ -package conform - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "strings" - "text/template" - - "github.com/Masterminds/sprig" - "github.com/autonomy/conform/conform/config" - "github.com/autonomy/conform/conform/git" - "github.com/autonomy/conform/conform/policy" -) - -// Enforcer performs all the build actions for a rule. -type Enforcer struct { - config *config.Config - rule string - GitInfo *git.Info - Built string -} - -// NewEnforcer instantiates and returns an executer. -func NewEnforcer(rule string) (enforcer *Enforcer, err error) { - enforcer = &Enforcer{} - gitInfo, err := git.NewInfo() - if err != nil { - return - } - date := []byte{} - if gitInfo.IsTag { - date, err = exec.Command("/bin/date").Output() - if err != nil { - return - } - } - - c, err := config.NewConfig() - if err != nil { - return - } - enforcer.config = c - enforcer.GitInfo = gitInfo - enforcer.Built = strings.TrimSuffix(string(date), "\n") - enforcer.rule = rule - - return -} - -// ExecuteBuild executes a docker build. -func (e *Enforcer) ExecuteBuild(dockerfile string) error { - image := e.FormatImageNameSHA() - if e.GitInfo.IsDirty { - image = e.FormatImageNameDirty() - } - - err := os.Setenv("CONFORM_IMAGE", image) - if err != nil { - return err - } - - args := append([]string{"build", "--tag", image, "-f", "-", "."}) - command := exec.Command("docker", args...) - command.Stdin = strings.NewReader(dockerfile) - command.Stdout = os.Stdout - command.Stderr = os.Stderr - err = command.Start() - if err != nil { - return err - } - err = command.Wait() - - return err -} - -// RenderDockerfile writes the final Dockerfile to disk. -func (e *Enforcer) RenderDockerfile(target *config.Rule) (dockerfile string, err error) { - for _, p := range target.Templates { - r, _err := e.RenderTemplate(p) - if _err != nil { - err = _err - return - } - dockerfile = dockerfile + "\n" + *r - } - - if e.config.Debug { - fmt.Println(dockerfile) - } - - return -} - -// RenderTemplate executes the template and returns it. -func (e *Enforcer) RenderTemplate(s string) (*string, error) { - if _s, ok := e.config.Templates[s]; ok { - var wr bytes.Buffer - tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(_s) - if err != nil { - return nil, err - } - - err = tmpl.Execute(&wr, &e) - if err != nil { - return nil, err - } - - str := wr.String() - return &str, nil - } - - return nil, fmt.Errorf("Template %q is not defined in conform.yaml", s) -} - -// ExtractArtifact copies an artifact from a build. -func (e *Enforcer) ExtractArtifact(artifact string) error { - return fmt.Errorf("Artifact %q is not defined in conform.yaml", artifact) -} - -// ExecuteScript executes a script for a rule. -func (e *Enforcer) ExecuteScript(script string) error { - if s, ok := e.config.Scripts[script]; ok { - fmt.Printf("Running %q script\n", script) - - command := exec.Command("bash", "-c", s) - command.Stdout = os.Stdout - command.Stderr = os.Stderr - err := command.Start() - if err != nil { - return err - } - err = command.Wait() - if err != nil { - return fmt.Errorf("Failed executing %q: %v", script, err) - } - - return nil - } - - return fmt.Errorf("Script %q is not defined in conform.yaml", script) -} - -// EnforcePolicies enforces all defined polcies. In the case that the working -// tree is dirty, all git policies are skipped. -func (e *Enforcer) EnforcePolicies() { - if !e.GitInfo.IsDirty { - enforceGitPolicy( - e.GitInfo, - &git.ConventionalCommitsOptions{ - Message: e.GitInfo.Message, - Types: e.config.Policies.Git.Types, - Scopes: e.config.Policies.Git.Scopes, - }, - ) - } -} - -func enforceGitPolicy(p policy.Policy, opts *git.ConventionalCommitsOptions) { - report, err := p.Compliance(opts) - if err != nil { - fmt.Print(err) - os.Exit(1) - } - if !report.Valid { - for _, err := range report.Errors { - fmt.Printf("%s", err) - os.Exit(1) - } - } -} - -// ExecuteRule performs all the relevant actions specified in its' declaration. -func (e *Enforcer) ExecuteRule() error { - e.EnforcePolicies() - if t, ok := e.config.Rules[e.rule]; ok { - fmt.Printf("Enforcing %q\n", e.rule) - for _, s := range t.Before { - err := e.ExecuteScript(s) - if err != nil { - return err - } - } - dockerfile, err := e.RenderDockerfile(t) - if err != nil { - return err - } - err = e.ExecuteBuild(dockerfile) - if err != nil { - return err - } - for _, s := range t.After { - err := e.ExecuteScript(s) - if err != nil { - return err - } - } - - return nil - } - - return fmt.Errorf("Rule %q is not defined in conform.yaml", e.rule) -} - -// FormatImageNameDirty formats the image name. -func (e *Enforcer) FormatImageNameDirty() string { - return fmt.Sprintf("%s:%s", *e.config.Metadata.Repository, "dirty") -} - -// FormatImageNameSHA formats the image name. -func (e *Enforcer) FormatImageNameSHA() string { - return fmt.Sprintf("%s:%s", *e.config.Metadata.Repository, e.GitInfo.SHA) -} - -// FormatImageNameTag formats the image name. -func (e *Enforcer) FormatImageNameTag() string { - return fmt.Sprintf("%s:%s", *e.config.Metadata.Repository, e.GitInfo.Tag) -} - -// FormatImageNameLatest formats the image name. -func (e *Enforcer) FormatImageNameLatest() string { - return fmt.Sprintf("%s:%s", *e.config.Metadata.Repository, "latest") -} diff --git a/conform/enforce_test.go b/conform/enforce_test.go deleted file mode 100644 index df2b1254..00000000 --- a/conform/enforce_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package conform - -import ( - "io/ioutil" - "log" - "os" - "os/exec" - "path/filepath" - "testing" -) - -var conformYAML = ` -metadata: - repository: test -scripts: - before : | - #!/bin/bash - exit 0 - after : | - #!/bin/bash - exit 0 -templates: - test: | - FROM scratch -rules: - test: - before: - - before - templates: - - test - after: - - after -` - -func RemoveAll(dir string) { - err := os.RemoveAll(dir) - if err != nil { - log.Fatal(err) - } -} - -func TestNewEnforcer(t *testing.T) { - dir, err := ioutil.TempDir("", "test") - if err != nil { - log.Fatal(err) - } - defer RemoveAll(dir) - tmpfn := filepath.Join(dir, "conform.yaml") - if err = ioutil.WriteFile(tmpfn, []byte(conformYAML), 0666); err != nil { - log.Fatal(err) - } - err = os.Chdir(dir) - if err != nil { - t.Error(err) - } - output, err := exec.Command("git", "init").Output() - if err != nil { - t.Fatal(output) - } - output, err = exec.Command("git", "config", "--global", "user.email", "'test@autonomy.io'").Output() - if err != nil { - t.Fatal(output) - } - output, err = exec.Command("git", "config", "--global", "user.name", "'test'").Output() - if err != nil { - t.Fatal(output) - } - output, err = exec.Command("git", "add", "conform.yaml").Output() - if err != nil { - t.Fatal(output) - } - output, err = exec.Command("git", "commit", "-m", "'test'").Output() - if err != nil { - t.Fatal(output) - } - _, err = NewEnforcer("test") - if err != nil { - t.Fail() - } -} diff --git a/conform/git/git.go b/conform/git/git.go index 6e32ae25..2d404820 100644 --- a/conform/git/git.go +++ b/conform/git/git.go @@ -1,124 +1,59 @@ package git import ( - "fmt" - "strconv" - "strings" - "github.com/Masterminds/semver" - "github.com/autonomy/conform/conform/utilities" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" ) -// Info contains the status of the working tree. -type Info struct { - Branch string - SHA string - Tag string - Prerelease string - Status string - Message string - IsTag bool - IsPrerelease bool - IsDirty bool +// Git is a helper for git. +type Git struct { + repo *git.Repository } -// NewInfo instantiates and returns info. -func NewInfo() (info *Info, err error) { +// NewGit instantiates and returns a Git struct. +func NewGit() (g *Git, err error) { repo, err := git.PlainOpen("./") if err != nil { return } - - branch, err := Branch(repo) - if err != nil { - return - } - - sha, err := SHA(repo) - if err != nil { - return - } - - tag, isTag, err := Tag(repo) - if err != nil { - return - } - - prerelease, isPrerelease, err := Prerelease(tag, isTag) - if err != nil { - return - } - - status, isDirty, err := Status(repo) - if err != nil { - return - } - - message, err := Message(repo, isDirty) - if err != nil { - return - } - - info = &Info{ - Branch: branch, - SHA: sha, - Tag: tag, - Prerelease: prerelease, - Status: status, - Message: message, - IsTag: isTag, - IsPrerelease: isPrerelease, - IsDirty: isDirty, - } + g = &Git{repo: repo} return } // Branch returns the current git branch name. -func Branch(repo *git.Repository) (branch string, err error) { - ref, err := repo.Head() +func (g *Git) Branch() (branch string, isBranch bool, err error) { + ref, err := g.repo.Head() if err != nil { return } if ref.IsBranch() { + isBranch = true branch = ref.Name().Short() } - fmt.Printf("Branch: %s\n", branch) - err = utilities.ExportConformVar("branch", branch) - if err != nil { - return - } - return } // SHA returns the sha of the current commit. -func SHA(repo *git.Repository) (sha string, err error) { - ref, err := repo.Head() +func (g *Git) SHA() (sha string, err error) { + ref, err := g.repo.Head() if err != nil { return } sha = ref.Hash().String()[0:7] - fmt.Printf("SHA: %s\n", sha) - err = utilities.ExportConformVar("sha", sha) - if err != nil { - return - } - return } // Tag returns the tag name if HEAD is a tag. -func Tag(repo *git.Repository) (tag string, isTag bool, err error) { - ref, err := repo.Head() +func (g *Git) Tag() (tag string, isTag bool, err error) { + ref, err := g.repo.Head() if err != nil { return } - tags, err := repo.Tags() + tags, err := g.repo.Tags() if err != nil { return } @@ -134,22 +69,11 @@ func Tag(repo *git.Repository) (tag string, isTag bool, err error) { return } - fmt.Printf("Tag: %s\n", tag) - err = utilities.ExportConformVar("tag", tag) - if err != nil { - return - } - fmt.Printf("IsTag: %s\n", strconv.FormatBool(isTag)) - err = utilities.ExportConformVar("is_tag", strconv.FormatBool(isTag)) - if err != nil { - return - } - return } // Prerelease returns the prerelease name if the tag is a prerelease. -func Prerelease(tag string, isTag bool) (prerelease string, isPrerelease bool, err error) { +func (g *Git) Prerelease(tag string, isTag bool) (prerelease string, isPrerelease bool, err error) { if isTag { var ver *semver.Version ver, err = semver.NewVersion(tag[1:]) @@ -162,23 +86,12 @@ func Prerelease(tag string, isTag bool) (prerelease string, isPrerelease bool, e } } - fmt.Printf("Prerelease: %s\n", prerelease) - err = utilities.ExportConformVar("prerelease", prerelease) - if err != nil { - return - } - fmt.Printf("IsPrerelease: %s\n", strconv.FormatBool(isPrerelease)) - err = utilities.ExportConformVar("is_prerelease", strconv.FormatBool(isPrerelease)) - if err != nil { - return - } - return } // Status returns the status of the working tree. -func Status(repo *git.Repository) (status string, isDirty bool, err error) { - worktree, err := repo.Worktree() +func (g *Git) Status() (status string, isClean bool, err error) { + worktree, err := g.repo.Worktree() if err != nil { return } @@ -187,38 +100,27 @@ func Status(repo *git.Repository) (status string, isDirty bool, err error) { return } if worktreeStatus.IsClean() { + isClean = true status = " nothing to commit, working tree clean" } else { - isDirty = true status = worktreeStatus.String() } - fmt.Printf("Status: \n%s\n", strings.TrimRight(status, "\n")) - err = utilities.ExportConformVar("status", strconv.FormatBool(isDirty)) - if err != nil { - return - } - fmt.Printf("IsDirty: %s\n", strconv.FormatBool(isDirty)) - err = utilities.ExportConformVar("is_dirty", strconv.FormatBool(isDirty)) - if err != nil { - return - } - return } // Message returns the commit message. In the case that a commit has multiple // parents, the message of the last parent is returned. -func Message(repo *git.Repository, isDirty bool) (message string, err error) { - ref, err := repo.Head() +func (g *Git) Message() (message string, err error) { + ref, err := g.repo.Head() if err != nil { return } - commit, err := repo.CommitObject(ref.Hash()) + commit, err := g.repo.CommitObject(ref.Hash()) if err != nil { return } - if commit.NumParents() != 1 { + if commit.NumParents() > 1 { parents := commit.Parents() for i := 1; i <= commit.NumParents(); i++ { next, err := parents.Next() @@ -233,13 +135,5 @@ func Message(repo *git.Repository, isDirty bool) (message string, err error) { message = commit.Message } - if !isDirty { - fmt.Printf("Message: %s\n", strings.TrimRight(message, "\n")) - err = utilities.ExportConformVar("message", message) - if err != nil { - return - } - } - return } diff --git a/conform/git/policy.go b/conform/git/policy.go deleted file mode 100644 index e2297b60..00000000 --- a/conform/git/policy.go +++ /dev/null @@ -1,96 +0,0 @@ -package git - -import ( - "fmt" - "regexp" - "strings" - - "github.com/autonomy/conform/conform/policy" -) - -// ConventionalCommitsOneDotZeroDotZeroBeta1 is the regular expression used for Conventional Commits 1.0.0-beta.1. -const ConventionalCommitsOneDotZeroDotZeroBeta1 = `^(\w*)\(([^)]+)\):\s{1}(.*)($|\n{2})` - -// TypeFeat is a commit of the type fix patches a bug in your codebase (this correlates with PATCH in semantic versioning). -const TypeFeat = "feat" - -// TypeFix is a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in semantic versioning). -const TypeFix = "fix" - -// ConventionalCommitsOptions are the configurable options used to check the copliance of conventional commits policy. -type ConventionalCommitsOptions struct { - Message string - Types []string - Scopes []string -} - -// Compliance implements the policy.Policy interface. -func (i *Info) Compliance(obj interface{}) (report *policy.Report, err error) { - opts := obj.(*ConventionalCommitsOptions) - report = &policy.Report{Valid: true} - re, err := regexp.Compile(ConventionalCommitsOneDotZeroDotZeroBeta1) - if err != nil { - return - } - lines := strings.Split(opts.Message, "\n") - groups := re.FindStringSubmatch(lines[0]) - if len(groups) != 5 { - err = fmt.Errorf("Invalid commit format") - return - } - validType, err := ValidateType(groups, opts.Types) - if !validType { - report.Valid = false - report.Errors = append(report.Errors, err) - } - validScope, err := ValidateScope(groups, opts.Scopes) - if !validScope { - report.Valid = false - report.Errors = append(report.Errors, err) - } - validDescription, err := ValidateDescription(groups) - if !validDescription { - report.Valid = false - report.Errors = append(report.Errors, err) - } - - return -} - -// ValidateType returns the commit type. -func ValidateType(groups []string, types []string) (valid bool, err error) { - types = append(types, TypeFeat, TypeFix) - for _, t := range types { - if t == groups[1] { - valid = true - } - } - if !valid { - err = fmt.Errorf("Invalid type: %s", groups[1]) - } - - return -} - -// ValidateScope returns the commit scope. -func ValidateScope(groups []string, scopes []string) (valid bool, err error) { - for _, scope := range scopes { - if scope == groups[2] { - valid = true - } - } - if !valid { - err = fmt.Errorf("Invalid scope: %s", groups[2]) - } - - return -} - -// ValidateDescription returns the commit description. -func ValidateDescription(groups []string) (valid bool, err error) { - if groups[3] != "" { - valid = true - } - - return -} diff --git a/conform/metadata/metadata.go b/conform/metadata/metadata.go new file mode 100644 index 00000000..4d7a566f --- /dev/null +++ b/conform/metadata/metadata.go @@ -0,0 +1,153 @@ +package metadata + +import ( + "time" + + "github.com/autonomy/conform/conform/git" + "github.com/autonomy/conform/conform/utilities" +) + +// Metadata contains metadata. +type Metadata struct { + Repository string `yaml:"repository"` + Docker *Docker + Git *Git + Version *Version + Built string +} + +// Docker contains docker specific metadata. +type Docker struct { + Image string +} + +// Git contains git specific metadata. +type Git struct { + Branch string + Message string + SHA string + Tag string + IsBranch bool + IsClean bool + IsTag bool +} + +// Version contains version specific metadata. +type Version struct { + Major string + Minor string + Prerelease string + IsPrerelease bool +} + +// UnmarshalYAML implements the yaml.UnmarshalYAML interface. +func (m *Metadata) UnmarshalYAML(unmarshal func(interface{}) error) error { + var aux struct { + Repository string `yaml:"repository"` + } + if err := unmarshal(&aux); err != nil { + return err + } + + m.Repository = aux.Repository + m.Built = time.Now().UTC().Format(time.RFC1123) + + if err := addMetadataForGit(m); err != nil { + return err + } + if err := addMetadataForDocker(m); err != nil { + return err + } + + return nil +} + +func addMetadataForDocker(m *Metadata) error { + image, err := utilities.ImageName(m.Repository, m.Git.SHA, m.Git.IsClean) + if err != nil { + return err + } + dockerMetadata := &Docker{ + Image: image, + } + m.Docker = dockerMetadata + + return nil +} + +func addMetadataForGit(m *Metadata) error { + g, err := git.NewGit() + if err != nil { + return err + } + m.Git = &Git{} + if err = addBranchMetadataForGit(g, m); err != nil { + return err + } + if err = addMessageMetadataForGit(g, m); err != nil { + return err + } + if err = addSHAMetadataForGit(g, m); err != nil { + return err + } + if err = addStatusMetadataForGit(g, m); err != nil { + return err + } + if err = addTagMetadataForGit(g, m); err != nil { + return err + } + + return nil +} + +func addBranchMetadataForGit(g *git.Git, m *Metadata) error { + branch, isBranch, err := g.Branch() + if err != nil { + return err + } + m.Git.Branch = branch + m.Git.IsBranch = isBranch + + return nil +} + +func addMessageMetadataForGit(g *git.Git, m *Metadata) error { + message, err := g.Message() + if err != nil { + return err + } + m.Git.Message = message + + return nil +} + +func addSHAMetadataForGit(g *git.Git, m *Metadata) error { + sha, err := g.SHA() + if err != nil { + return err + } + m.Git.SHA = sha + + return nil +} + +func addStatusMetadataForGit(g *git.Git, m *Metadata) error { + _, isClean, err := g.Status() + if err != nil { + return err + } + m.Git.IsClean = isClean + + return nil +} + +func addTagMetadataForGit(g *git.Git, m *Metadata) error { + tag, isTag, err := g.Tag() + if err != nil { + return err + } + m.Git.Tag = tag + m.Git.IsTag = isTag + + return nil +} diff --git a/conform/pipeline/pipeline.go b/conform/pipeline/pipeline.go new file mode 100644 index 00000000..73a8df49 --- /dev/null +++ b/conform/pipeline/pipeline.go @@ -0,0 +1,95 @@ +package pipeline + +import ( + "fmt" + "os" + "os/exec" + "path" + "strings" + + "github.com/autonomy/conform/conform/metadata" + "github.com/autonomy/conform/conform/stage" + "github.com/autonomy/conform/conform/task" +) + +// Pipeline defines the stages and artifacts. +type Pipeline struct { + Stages []string `yaml:"stages"` +} + +// Build executes a docker build. +func (p *Pipeline) Build(metadata *metadata.Metadata, stages map[string]*stage.Stage, tasks map[string]*task.Task) (err error) { + for _, stage := range p.Stages { + if _, ok := stages[stage]; !ok { + return fmt.Errorf("Stage %q is not defined in conform.yaml", stage) + } + s, err := p.render(metadata, stages[stage].Tasks, tasks) + if err != nil { + return err + } + err = build(metadata.Docker.Image, s) + if err != nil { + return err + } + for _, artifact := range stages[stage].Artifacts { + err = p.extract(metadata, artifact) + if err != nil { + return err + } + } + } + + return +} + +// extract extracts an artifact from a docker image. +func (p *Pipeline) extract(metadata *metadata.Metadata, artifact string) (err error) { + argsSlice := [][]string{ + {"create", "--name=" + metadata.Git.SHA, metadata.Docker.Image}, + {"cp", metadata.Git.SHA + ":" + artifact, path.Base(artifact)}, + {"rm", metadata.Git.SHA}, + } + for _, args := range argsSlice { + command := exec.Command("docker", args...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err = command.Start() + if err != nil { + return err + } + err = command.Wait() + } + + return +} + +// render renders the stage tasks. +func (p *Pipeline) render(metadata *metadata.Metadata, requestedTasks []string, tasks map[string]*task.Task) (string, error) { + var s string + for _, task := range requestedTasks { + if _, ok := tasks[task]; !ok { + return "", fmt.Errorf("Task %q is not defined in conform.yaml", task) + } + err := tasks[task].Render(metadata) + if err != nil { + return "", err + } + s += tasks[task].Rendered + } + + return s, nil +} + +func build(image, s string) error { + args := append([]string{"build", "--tag", image, "-f", "-", "."}) + command := exec.Command("docker", args...) + command.Stdin = strings.NewReader(s) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err := command.Start() + if err != nil { + return err + } + + return command.Wait() +} diff --git a/conform/policy/conventionalcommit/conventionalcommit.go b/conform/policy/conventionalcommit/conventionalcommit.go new file mode 100644 index 00000000..467cc57b --- /dev/null +++ b/conform/policy/conventionalcommit/conventionalcommit.go @@ -0,0 +1,99 @@ +package conventionalcommit + +import ( + "fmt" + "regexp" + "strings" + + "github.com/autonomy/conform/conform/metadata" + "github.com/autonomy/conform/conform/pipeline" + "github.com/autonomy/conform/conform/policy" + "github.com/autonomy/conform/conform/task" +) + +// 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. +const HeaderRegex = `^(\w*)\(([^)]+)\):\s{1}(.*)($|\n{2})` + +// TypeFeat is a commit of the type fix patches a bug in your codebase +// (this correlates with PATCH in semantic versioning). +const TypeFeat = "feat" + +// TypeFix is a commit of the type feat introduces a new feature to the +// codebase (this correlates with MINOR in semantic versioning). +const TypeFix = "fix" + +// Compliance implements the policy.Policy.Compliance function. +func (c *Conventional) Compliance(metadata *metadata.Metadata, options ...policy.Option) (report policy.Report) { + report = policy.Report{} + if !metadata.Git.IsClean { + return + } + groups := parseHeader(metadata.Git.Message) + if len(groups) != 5 { + report.Errors = append(report.Errors, fmt.Errorf("Invalid commit format")) + return + } + ValidateType(&report, groups, c.Types) + ValidateScope(&report, groups, c.Scopes) + ValidateDescription(&report, groups) + + return +} + +// Pipeline implements the policy.Policy.Pipeline function. +func (c *Conventional) Pipeline(*pipeline.Pipeline) policy.Option { + return func(args *policy.Options) {} +} + +// Tasks implements the policy.Policy.Tasks function. +func (c *Conventional) Tasks(map[string]*task.Task) policy.Option { + return func(args *policy.Options) {} +} + +// ValidateType returns the commit type. +func ValidateType(report *policy.Report, groups []string, types []string) { + types = append(types, TypeFeat, TypeFix) + for _, t := range types { + if t == groups[1] { + return + } + } + report.Errors = append(report.Errors, fmt.Errorf("Invalid type: %s", groups[1])) +} + +// ValidateScope returns the commit scope. +func ValidateScope(report *policy.Report, groups []string, scopes []string) { + for _, scope := range scopes { + if scope == groups[2] { + return + } + } + report.Errors = append(report.Errors, fmt.Errorf("Invalid scope: %s", groups[2])) +} + +// ValidateDescription returns the commit description. +func ValidateDescription(report *policy.Report, groups []string) { + if len(groups[3]) <= 72 && len(groups[3]) != 0 { + return + } + report.Errors = append(report.Errors, fmt.Errorf("Invalid description: %s", groups[3])) +} + +func parseHeader(message string) []string { + re, err := regexp.Compile(HeaderRegex) + if err != nil { + return nil + } + header := strings.Split(message, "\n")[0] + groups := re.FindStringSubmatch(header) + + return groups +} diff --git a/conform/policy/conventionalcommit/conventionalcommit_test.go b/conform/policy/conventionalcommit/conventionalcommit_test.go new file mode 100644 index 00000000..f43702aa --- /dev/null +++ b/conform/policy/conventionalcommit/conventionalcommit_test.go @@ -0,0 +1,130 @@ +package conventionalcommit + +import ( + "io/ioutil" + "log" + "os" + "os/exec" + "testing" + + "github.com/autonomy/conform/conform/git" + "github.com/autonomy/conform/conform/metadata" + "github.com/autonomy/conform/conform/policy" +) + +func RemoveAll(dir string) { + err := os.RemoveAll(dir) + if err != nil { + log.Fatal(err) + } +} + +func TestValidConventionalCommitPolicy(t *testing.T) { + dir, err := ioutil.TempDir("", "test") + if err != nil { + log.Fatal(err) + } + defer RemoveAll(dir) + err = os.Chdir(dir) + if err != nil { + t.Error(err) + } + err = initRepo() + if err != nil { + t.Error(err) + } + err = createValidCommit() + if err != nil { + t.Error(err) + } + report, err := runCompliance() + if err != nil { + t.Error(err) + } + if !report.Valid() { + t.Error("Report is invalid with valid conventional commit") + } +} + +func TestInvalidConventionalCommitPolicy(t *testing.T) { + dir, err := ioutil.TempDir("", "test") + if err != nil { + log.Fatal(err) + } + defer RemoveAll(dir) + err = os.Chdir(dir) + if err != nil { + t.Error(err) + } + err = initRepo() + if err != nil { + t.Error(err) + } + err = createInvalidCommit() + if err != nil { + t.Error(err) + } + report, err := runCompliance() + if err != nil { + t.Error(err) + } + if report.Valid() { + t.Error("Report is valid with invalid conventional commit") + } +} + +func runCompliance() (*policy.Report, error) { + g, err := git.NewGit() + if err != nil { + return nil, err + } + message, err := g.Message() + if err != nil { + return nil, err + } + c := &Conventional{} + c.Types = []string{"type"} + c.Scopes = []string{"scope"} + m := &metadata.Metadata{ + Git: &metadata.Git{ + Message: message, + IsClean: true, + }, + } + report := c.Compliance(m) + + return &report, nil +} +func initRepo() error { + _, err := exec.Command("git", "init").Output() + if err != nil { + return err + } + _, err = exec.Command("git", "config", "--global", "user.email", "'test@autonomy.io'").Output() + if err != nil { + return err + } + _, err = exec.Command("git", "config", "--global", "user.name", "test").Output() + if err != nil { + return err + } + _, err = exec.Command("touch", "test").Output() + if err != nil { + return err + } + _, err = exec.Command("git", "add", "test").Output() + + return err +} + +func createValidCommit() error { + _, err := exec.Command("git", "commit", "-m", "type(scope): description").Output() + + return err +} + +func createInvalidCommit() error { + _, err := exec.Command("git", "commit", "-m", "invalid commit").Output() + + return err +} diff --git a/conform/policy/policy.go b/conform/policy/policy.go index a0964721..d3089b28 100644 --- a/conform/policy/policy.go +++ b/conform/policy/policy.go @@ -1,12 +1,33 @@ package policy +import ( + "github.com/autonomy/conform/conform/metadata" + "github.com/autonomy/conform/conform/pipeline" + "github.com/autonomy/conform/conform/task" +) + +// Option is a functional option used to pass in arguments to a Policy. +type Option func(*Options) + +// Options defines the set of options available to a Policy. +type Options struct { + Pipeline *pipeline.Pipeline + Tasks map[string]*task.Task +} + // Report summarizes the compliance of a policy. type Report struct { - Valid bool Errors []error } -// Policy is an interface used for enforcing policies. +// Policy is an interface that policies must implement. type Policy interface { - Compliance(interface{}) (report *Report, err error) + Compliance(*metadata.Metadata, ...Option) Report + Pipeline(*pipeline.Pipeline) Option + Tasks(map[string]*task.Task) Option +} + +// Valid checks if a report is valid. +func (r Report) Valid() bool { + return len(r.Errors) == 0 } diff --git a/conform/policy/version/version.go b/conform/policy/version/version.go new file mode 100644 index 00000000..a634a82c --- /dev/null +++ b/conform/policy/version/version.go @@ -0,0 +1,5 @@ +package version + +// Version defines the version policy to use and the options specific to the +// policy. +type Version struct{} diff --git a/conform/renderer/renderer.go b/conform/renderer/renderer.go new file mode 100644 index 00000000..4302221d --- /dev/null +++ b/conform/renderer/renderer.go @@ -0,0 +1,27 @@ +package renderer + +import ( + "bytes" + "text/template" + + "github.com/Masterminds/sprig" + "github.com/autonomy/conform/conform/metadata" +) + +// Renderer renders all pipeline templates. +type Renderer struct{} + +// Render renders a template. +func (r *Renderer) Render(m *metadata.Metadata, s string) (string, error) { + var wr bytes.Buffer + tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(s) + if err != nil { + return "", err + } + err = tmpl.Execute(&wr, m) + if err != nil { + return "", err + } + + return wr.String(), nil +} diff --git a/conform/script/script.go b/conform/script/script.go new file mode 100644 index 00000000..68201e24 --- /dev/null +++ b/conform/script/script.go @@ -0,0 +1,49 @@ +package script + +import ( + "fmt" + "os" + "os/exec" + + "github.com/autonomy/conform/conform/metadata" + "github.com/autonomy/conform/conform/renderer" +) + +// Script defines a template that can be executed. +type Script struct { + Template string `yaml:"template"` + Rendered string +} + +// Execute executes the pipeline script. +func (s *Script) Execute(metadata *metadata.Metadata) error { + err := s.Render(metadata) + if err != nil { + return err + } + command := exec.Command("bash", "-c", s.Rendered) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err = command.Start() + if err != nil { + return err + } + err = command.Wait() + if err != nil { + return fmt.Errorf("Failed executing script: %v", err) + } + + return nil +} + +// Render renders the script. +func (s *Script) Render(metadata *metadata.Metadata) error { + renderer := renderer.Renderer{} + rendered, err := renderer.Render(metadata, s.Template) + if err != nil { + return err + } + s.Rendered = rendered + + return nil +} diff --git a/conform/stage/stage.go b/conform/stage/stage.go new file mode 100644 index 00000000..241a9a47 --- /dev/null +++ b/conform/stage/stage.go @@ -0,0 +1,7 @@ +package stage + +// Stage defines a stage within a pipeline. +type Stage struct { + Artifacts []string `yaml:"artifacts"` + Tasks []string `yaml:"tasks"` +} diff --git a/conform/task/task.go b/conform/task/task.go new file mode 100644 index 00000000..87800ae0 --- /dev/null +++ b/conform/task/task.go @@ -0,0 +1,24 @@ +package task + +import ( + "github.com/autonomy/conform/conform/metadata" + "github.com/autonomy/conform/conform/renderer" +) + +// Task defines a stage that can be used within a pipeline. +type Task struct { + Template string `yaml:"template"` + Rendered string +} + +// Render renders the stage. +func (s *Task) Render(metadata *metadata.Metadata) error { + renderer := renderer.Renderer{} + rendered, err := renderer.Render(metadata, s.Template) + if err != nil { + return err + } + s.Rendered = rendered + + return nil +} diff --git a/conform/utilities/utilities.go b/conform/utilities/utilities.go index 5bdfa137..bc91554c 100644 --- a/conform/utilities/utilities.go +++ b/conform/utilities/utilities.go @@ -1,15 +1,30 @@ package utilities -import ( - "fmt" - "os" - "strings" -) +import "fmt" -// ExportConformVar exports variable prefixed with CONFORM_ -func ExportConformVar(name, value string) (err error) { - variable := fmt.Sprintf("CONFORM_%s", strings.ToUpper(name)) - err = os.Setenv(variable, value) +// ImageName formats the image name based on the status of a git repository. +func ImageName(repository, sha string, isClean bool) (image string, err error) { + if !isClean { + image = formatImageNameDirty(repository) + } else { + image = formatImageNameSHA(repository, sha) + } return } + +func formatImageNameDirty(repository string) string { + return fmt.Sprintf("%s:dirty", repository) +} + +func formatImageNameSHA(repository, sha string) string { + return fmt.Sprintf("%s:%s", repository, sha) +} + +// func formatImageNameTag(repository, tag string) string { +// return fmt.Sprintf("%s:%s", repository, tag) +// } +// +// func formatImageNameLatest(repository string) string { +// return fmt.Sprintf("%s:latest", repository) +// } diff --git a/conform/version.go b/conform/version/version.go similarity index 98% rename from conform/version.go rename to conform/version/version.go index a9844762..7f69acec 100644 --- a/conform/version.go +++ b/conform/version/version.go @@ -1,4 +1,4 @@ -package conform +package version import ( "bytes" diff --git a/scripts/deploy.sh b/scripts/deploy.sh deleted file mode 100644 index f4ecdc41..00000000 --- a/scripts/deploy.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -set -e - -# The deploy stage within Travis-CI dirtys the working tree. This will cause -# the docker tagging to fail. Reset to HEAD as a workaround. -git reset --hard HEAD -conform enforce deploy diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100644 index 00000000..78e848f4 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +GOFILES=$(find . -type f -name '*.go' -not -path "./vendor/*") + +echo "Linting packages" +gometalinter --vendor --disable=gas --disable=gotype --sort=path --deadline=600s ./... + +echo "Formatting go files" +GOFMTFILES="$(gofmt -l -d -s ${GOFILES})" +if [ ! -z "${GOFMTFILES}" ]; then + echo -e "Failed gofmt files:\n${GOFMTFILES}" + exit 1 +fi + +exit 0 diff --git a/scripts/test.sh b/scripts/test.sh index e230faa5..4cb566e3 100644 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,8 +3,6 @@ set -e GOPACKAGES=$(go list ./... | grep -v /vendor/) -GOFILES=$(find . -type f -name '*.go' -not -path "./vendor/*") - COVERAGE_REPORT=coverage.txt PROFILE=profile.out @@ -21,12 +19,6 @@ for package in ${GOPACKAGES[@]}; do fi done -echo "Linting packages" -gometalinter.v1 --vendor --disable=gas --disable=gotype --sort=linter --deadline=240s ./... - -echo "Formatting go files" -if [ ! -z "$(gofmt -l -s ${GOFILES})" ]; then - exit 1 -fi +mv ${COVERAGE_REPORT} /${COVERAGE_REPORT} exit 0