Browse Source

Improve primary CI pipeline and refactor Makefile

Kellin McAvoy 5 years ago
parent
commit
b4b1f892c5
7 changed files with 420 additions and 366 deletions
  1. 0 85
      .github/workflows/all.yml
  2. 239 0
      .github/workflows/ci.yml
  3. 5 4
      .github/workflows/helm.yml
  4. 0 161
      .github/workflows/main.yml
  5. 9 24
      .gitignore
  6. 5 24
      Dockerfile
  7. 162 68
      Makefile

+ 0 - 85
.github/workflows/all.yml

@@ -1,85 +0,0 @@
-name: all-ci
-
-on:
-  push:
-    branches:
-      - '*'         # matches every branch that doesn't contain a '/'
-      - '*/*'       # matches every branch containing a single '/'
-      - '**'        # matches every branch
-      - '!main'     # excludes main
-    paths-ignore:
-      - 'deploy/**'
-  pull_request:
-    branches: [ '!main' ]
-    paths-ignore:
-      - 'deploy/**'
-
-env:
-  KUBEBUILDER_VERSION: 2.3.1
-
-jobs:
-
-  build:
-    name: Build
-    container:
-      image: golang:1.15
-    runs-on: ubuntu-latest
-
-    steps:
-    - name: Check out code into the Go module directory
-      uses: actions/checkout@v2
-
-    - name: Set up Go
-      uses: actions/setup-go@v2
-      with:
-        go-version: '~1.15'
-
-    - name: Add kubebuilder
-      run:  |
-        curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${{env.KUBEBUILDER_VERSION}}/kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz > kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz
-        tar -xvf kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz
-        mv kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64 /usr/local/kubebuilder
-
-    - name: Lint
-      run: |
-        make lint-install
-        make lint
-
-    - name: Build
-      run: make build
-
-  test:
-    name: Test
-    container:
-      image: golang:1.15
-    runs-on: ubuntu-latest
-
-    steps:
-    - name: Check out code into the Go module directory
-      uses: actions/checkout@v2
-
-    - name: Set up Go
-      uses: actions/setup-go@v2
-      with:
-        go-version: '~1.15'
-
-    - name: Add kubebuilder
-      run:  |
-        curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${{env.KUBEBUILDER_VERSION}}/kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz > kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz
-        tar -xvf kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz
-        mv kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64 /usr/local/kubebuilder
-
-    - name: Check out code into the Go module directory
-      uses: actions/checkout@v2
-
-    - name: Test
-      run: make test
-
-    - name: Coverage
-      uses: codecov/codecov-action@v1
-      with:
-        # token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
-        file: ./cover.out
-        # flags: unittests # optional
-        name: external-secrets
-        fail_ci_if_error: false

+ 239 - 0
.github/workflows/ci.yml

@@ -0,0 +1,239 @@
+name: CI
+
+on:
+  push:
+    branches:
+      - main
+      - release-*
+  pull_request: {}
+  workflow_dispatch: {}
+
+env:
+  # Common versions
+  GO_VERSION: '1.15'
+  GOLANGCI_VERSION: 'v1.33'
+  KUBEBUILDER_VERSION: '2.3.1'
+  DOCKER_BUILDX_VERSION: 'v0.4.2'
+
+  # Common users. We can't run a step 'if secrets.GHCR_USER != ""' but we can run
+  # a step 'if env.GHCR_USER' != ""', so we copy these to succinctly test whether
+  # credentials have been provided before trying to run steps that need them.
+  GHCR_USER: ${{ secrets.GHCR_USERNAME }}
+
+jobs:
+  detect-noop:
+    runs-on: ubuntu-18.04
+    outputs:
+      noop: ${{ steps.noop.outputs.should_skip }}
+    steps:
+      - name: Detect No-op Changes
+        id: noop
+        uses: fkirc/skip-duplicate-actions@v2.1.0
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          paths_ignore: '["**.md", "**.png", "**.jpg"]'
+          do_not_skip: '["workflow_dispatch", "schedule", "push"]'
+          concurrent_skipping: false
+
+  lint:
+    runs-on: ubuntu-18.04
+    needs: detect-noop
+    if: needs.detect-noop.outputs.noop != 'true'
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Find the Go Cache
+        id: go
+        run: |
+          echo "::set-output name=build-cache::$(go env GOCACHE)"
+          echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
+
+      - name: Cache the Go Build Cache
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.go.outputs.build-cache }}
+          key: ${{ runner.os }}-build-lint-${{ hashFiles('**/go.sum') }}
+          restore-keys: ${{ runner.os }}-build-lint-
+
+      - name: Cache Go Dependencies
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.go.outputs.mod-cache }}
+          key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
+          restore-keys: ${{ runner.os }}-pkg-
+
+      # This action uses its own setup-go, which always seems to use the latest
+      # stable version of Go. We could run 'make lint' to ensure our desired Go
+      # version, but we prefer this action because it leaves 'annotations' (i.e.
+      # it comments on PRs to point out linter violations).
+      - name: Lint
+        uses: golangci/golangci-lint-action@v2
+        with:
+          version: ${{ env.GOLANGCI_VERSION }}
+
+  check-diff:
+    runs-on: ubuntu-18.04
+    needs: detect-noop
+    if: needs.detect-noop.outputs.noop != 'true'
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Setup Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ env.GO_VERSION }}
+
+      - name: Find the Go Cache
+        id: go
+        run: |
+          echo "::set-output name=build-cache::$(go env GOCACHE)"
+          echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
+
+      - name: Cache the Go Build Cache
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.go.outputs.build-cache }}
+          key: ${{ runner.os }}-build-check-diff-${{ hashFiles('**/go.sum') }}
+          restore-keys: ${{ runner.os }}-build-check-diff-
+
+      - name: Cache Go Dependencies
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.go.outputs.mod-cache }}
+          key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
+          restore-keys: ${{ runner.os }}-pkg-
+
+      - name: Check Diff
+        run: make check-diff
+
+  unit-tests:
+    runs-on: ubuntu-18.04
+    needs: detect-noop
+    if: needs.detect-noop.outputs.noop != 'true'
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Fetch History
+        run: git fetch --prune --unshallow
+
+      - name: Setup Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ env.GO_VERSION }}
+
+      - name: Find the Go Cache
+        id: go
+        run: |
+          echo "::set-output name=build-cache::$(go env GOCACHE)"
+          echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
+
+      - name: Cache the Go Build Cache
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.go.outputs.build-cache }}
+          key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }}
+          restore-keys: ${{ runner.os }}-build-unit-tests-
+
+      - name: Cache Go Dependencies
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.go.outputs.mod-cache }}
+          key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
+          restore-keys: ${{ runner.os }}-pkg-
+
+      - name: Add kubebuilder
+        run:  |
+          curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${{env.KUBEBUILDER_VERSION}}/kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz > kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz
+          tar -xvf kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz
+          sudo mv kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64 /usr/local/kubebuilder
+
+      - name: Cache kubebuilder
+        uses: actions/cache@v2
+        with:
+          path: /usr/local/kubebuilder
+          key: ${{ runner.os }}-kubebuilder-${{env.KUBEBUILDER_VERSION}}
+          restore-keys: ${{ runner.os }}-kubebuilder-
+
+      - name: Run Unit Tests
+        run: make test
+
+      - name: Publish Unit Test Coverage
+        uses: codecov/codecov-action@v1
+        with:
+          flags: unittests
+          file: ./cover.out
+
+  publish-artifacts:
+    runs-on: ubuntu-18.04
+    needs: detect-noop
+    if: needs.detect-noop.outputs.noop != 'true'
+
+    steps:
+      - name: Setup QEMU
+        uses: docker/setup-qemu-action@v1
+        with:
+          platforms: all
+
+      - name: Setup Docker Buildx
+        uses: docker/setup-buildx-action@v1
+        with:
+          version: ${{ env.DOCKER_BUILDX_VERSION }}
+          install: true
+
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Fetch History
+        run: git fetch --prune --unshallow
+
+      - name: Setup Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ env.GO_VERSION }}
+
+      - name: Find the Go Cache
+        id: go
+        run: |
+          echo "::set-output name=build-cache::$(go env GOCACHE)"
+          echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
+
+      - name: Cache the Go Build Cache
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.go.outputs.build-cache }}
+          key: ${{ runner.os }}-build-publish-artifacts-${{ hashFiles('**/go.sum') }}
+          restore-keys: ${{ runner.os }}-build-publish-artifacts-
+
+      - name: Cache Go Dependencies
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.go.outputs.mod-cache }}
+          key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
+          restore-keys: ${{ runner.os }}-pkg-
+
+      - name: Build Artifacts
+        run: make docker.build
+
+      - name: Login to Docker
+        uses: docker/login-action@v1
+        if: env.GHCR_USERNAME != ''
+        with:
+          registry: ghcr.io
+          username: ${{ secrets.GHCR_USERNAME }}
+          password: ${{ secrets.GHCR_TOKEN }}
+
+      - name: Publish Artifacts
+        run: make docker.push
+        if: env.GHCR_USERNAME != ''
+
+      - name: Promote Artifacts to main release channel
+        if: github.ref == 'refs/heads/main' && env.GHCR_USERNAME != ''
+        run: make docker.promote
+        env:
+          RELEASE_TAG: main

+ 5 - 4
.github/workflows/helm.yml

@@ -2,14 +2,15 @@ name: Helm
 
 on:
   push:
-    tags:
-      - '*'
+    branches:
+      - main
+      - release-*
     paths:
       - 'deploy/charts/**'
   pull_request:
-    branches: main
     paths:
       - 'deploy/charts/**'
+  workflow_dispatch: {}
 
 jobs:
   lint-and-test:
@@ -22,7 +23,7 @@ jobs:
 
       - name: Generate chart
         run: |
-          make crds-to-chart
+          make helm.generate
 
       - name: Set up Helm
         uses: azure/setup-helm@v1

+ 0 - 161
.github/workflows/main.yml

@@ -1,161 +0,0 @@
-name: main-ci
-
-on:
-  push:
-    branches: [ main ]
-    tags:
-      - '*'
-    paths-ignore:
-      - 'deploy/**'
-  pull_request:
-    branches: [ main ]
-    paths-ignore:
-      - 'deploy/**'
-
-env:
-  KUBEBUILDER_VERSION: 2.3.1
-
-jobs:
-
-  build:
-    name: Build
-    container:
-      image: golang:1.15
-    runs-on: ubuntu-latest
-
-    steps:
-    - name: Check out code into the Go module directory
-      uses: actions/checkout@v2
-
-    - name: Set up Go
-      uses: actions/setup-go@v2
-      with:
-        go-version: '~1.15'
-
-    - name: Add kubebuilder
-      run:  |
-        curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${{env.KUBEBUILDER_VERSION}}/kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz > kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz
-        tar -xvf kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz
-        mv kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64 /usr/local/kubebuilder
-
-    - name: Lint
-      run: |
-        make lint-install
-        make lint
-
-    - name: Build
-      run: make build
-
-  test:
-    name: Test
-    container:
-      image: golang:1.15
-    runs-on: ubuntu-latest
-
-    steps:
-    - name: Check out code into the Go module directory
-      uses: actions/checkout@v2
-
-    - name: Set up Go
-      uses: actions/setup-go@v2
-      with:
-        go-version: '~1.15'
-
-    - name: Add kubebuilder
-      run:  |
-        curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${{env.KUBEBUILDER_VERSION}}/kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz > kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz
-        tar -xvf kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz
-        mv kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64 /usr/local/kubebuilder
-
-    - name: Test
-      run: make test
-
-    - name: Coverage
-      uses: codecov/codecov-action@v1
-      with:
-        # token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
-        file: ./cover.out
-        # flags: unittests # optional
-        name: external-secrets
-        fail_ci_if_error: true
-
-  docker:
-    name: Docker
-    runs-on: ubuntu-latest
-    needs: [build, test]
-    steps:
-    - name: Prepare
-      id: prep
-      run: |
-        DOCKER_IMAGE=ghcr.io/external-secrets/external-secrets
-
-        VERSION=edge
-        if [[ $GITHUB_REF == refs/tags/* ]]; then
-          VERSION=${GITHUB_REF#refs/tags/}
-        elif [[ $GITHUB_REF == refs/heads/* ]]; then
-          VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g')
-        elif [[ $GITHUB_REF == refs/pull/* ]]; then
-          VERSION=pr-${{ github.event.number }}
-        fi
-
-        TAGS="${DOCKER_IMAGE}:${VERSION}"
-        if [ "${{ github.event_name }}" = "push" ]; then
-          TAGS="$TAGS,${DOCKER_IMAGE}:sha-${GITHUB_SHA::8}"
-        fi
-
-        PUSH_IMAGE=true
-        REPO_FULL_NAME="${{ github.event.pull_request.head.repo.full_name }}"
-        # If this is both a pull request and a fork, then don't push the image
-        if [[ ${{ github.event_name }} == pull_request ]]; then
-          if [[ $REPO_FULL_NAME != external-secrets/external-secrets ]]; then
-            PUSH_IMAGE=false
-          fi
-        fi
-
-        REPO_URL=https://github.com/${{github.repository}}
-
-        echo ::set-output name=version::${VERSION}
-        echo ::set-output name=tags::${TAGS}
-        echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
-        echo ::set-output name=push_image::$PUSH_IMAGE
-        echo ::set-output name=repo_url::$REPO_URL
-
-    - name: Check out the repo
-      uses: actions/checkout@v2
-
-    - name: Set up QEMU
-      id: qemu
-      uses: docker/setup-qemu-action@v1
-      with:
-        platforms: linux/amd64
-
-    - name: Set up Docker Buildx
-      id: buildx
-      uses: docker/setup-buildx-action@v1
-
-    - name: Login to Github Packages
-      id: docker-login
-      uses: docker/login-action@v1
-      with:
-        registry: ghcr.io
-        username: ${{ secrets.GHCR_USERNAME }}
-        password: ${{ secrets.GHCR_TOKEN }}
-      if: ${{ steps.prep.outputs.push_image == 'true' }}
-
-    - name: Build and push
-      id: docker_build
-      uses: docker/build-push-action@v2
-      with:
-        context: .
-        file: ./Dockerfile
-        builder: ${{ steps.buildx.outputs.name }}
-        platforms: linux/amd64
-        tags: ${{ steps.prep.outputs.tags }}
-        push: ${{ steps.prep.outputs.push_image }}
-        labels: |
-          org.opencontainers.image.source=${{ steps.prep.outputs.repo_url }}
-          org.opencontainers.image.created=${{ steps.prep.outputs.created }}
-          org.opencontainers.image.revision=${{ github.sha }}
-
-    - name: Image digest
-      run: echo ${{ steps.docker_build.outputs.digest }}

+ 9 - 24
.gitignore

@@ -1,28 +1,13 @@
-# Binaries for programs and plugins
-*.exe
-*.exe~
-*.dll
-*.so
-*.dylib
-bin
-
-# Test binary, build with `go test -c`
-*.test
-
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
-# Kubernetes Generated files - skip generated files, except for vendored files
-
-!vendor/**/zz_generated.*
+/bin
+/vendor
+cover.out
 
-# editor and IDE paraphernalia
-.idea
-*.swp
-*.swo
-*~
+# ignore ide files (debug config etc...)
+/.vscode
+/.idea
 
-# Code test output
-cover.out
+# helm chart dependencies
+**/charts/*.tgz
+**/charts/**/requirements.lock
 
 deploy/charts/external-secrets/templates/crds/*.yaml

+ 5 - 24
Dockerfile

@@ -1,27 +1,8 @@
-# Build the manager binary
-FROM golang:1.15 as builder
+FROM alpine:3.13
 
-WORKDIR /workspace
-# Copy the Go Modules manifests
-COPY go.mod go.mod
-COPY go.sum go.sum
-# cache deps before building and copying source so that we don't need to re-download as much
-# and so that source changes don't invalidate our downloaded layer
-RUN go mod download
+COPY bin/external-secrets /bin/external-secrets
 
-# Copy the go source
-COPY main.go main.go
-COPY apis/ apis/
-COPY pkg/ pkg/
+# Run as UID for nobody
+USER 65534
 
-# Build
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go
-
-# Use distroless as minimal base image to package the manager binary
-# Refer to https://github.com/GoogleContainerTools/distroless for more details
-FROM gcr.io/distroless/static:nonroot
-WORKDIR /
-COPY --from=builder /workspace/manager .
-USER nonroot:nonroot
-
-ENTRYPOINT ["/manager"]
+ENTRYPOINT ["/bin/external-secrets"]

+ 162 - 68
Makefile

@@ -1,15 +1,26 @@
-MAKEFLAGS     += --warn-undefined-variables
+# set the shell to bash always
 SHELL         := /bin/bash
+
+# set make and shell flags to exit on errors
+MAKEFLAGS     += --warn-undefined-variables
 .SHELLFLAGS   := -euo pipefail -c
+
+# default target is build
 .DEFAULT_GOAL := all
+.PHONY: all
+all: build
+
+# Image registry for build/push image targets
+IMAGE_REGISTRY ?= ghcr.io/external-secrets/external-secrets
 
-# Image URL to use all building/pushing image targets
-IMG ?= controller:latest
 # Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
 CRD_OPTIONS ?= "crd:trivialVersions=true"
-HELM_DIR    ?= deploy/charts/external-secrets
 CRD_DIR     ?= config/crd/bases
 
+HELM_DIR    ?= deploy/charts/external-secrets
+
+OUTPUT_DIR  ?= bin
+
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
 ifeq (,$(shell go env GOBIN))
 GOBIN=$(shell go env GOPATH)/bin
@@ -17,92 +28,103 @@ else
 GOBIN=$(shell go env GOBIN)
 endif
 
-all: build
+# check if there are any existing `git tag` values
+ifeq ($(shell git tag),)
+# no tags found - default to initial tag `v0.0.0`
+VERSION := $(shell echo "v0.0.0-$$(git rev-list HEAD --count)-g$$(git describe --dirty --always)" | sed 's/-/./2' | sed 's/-/./2')
+else
+# use tags
+VERSION := $(shell git describe --dirty --always --tags | sed 's/-/./2' | sed 's/-/./2' )
+endif
 
-.PHONY: test
-test: generate manifests ## Run tests
-	go test ./... -coverprofile cover.out
+# ====================================================================================
+# Colors
 
-.PHONY: build
-build: generate fmt ## Build binary
-	go build -o bin/manager main.go
+BLUE         := $(shell printf "\033[34m")
+YELLOW       := $(shell printf "\033[33m")
+RED          := $(shell printf "\033[31m")
+GREEN        := $(shell printf "\033[32m")
+CNone        := $(shell printf "\033[0m")
 
-# Run against the configured Kubernetes cluster in ~/.kube/config
-run: generate fmt manifests
-	go run ./main.go
+# ====================================================================================
+# Logger
 
-# Install CRDs into a cluster
-install: manifests
-	kustomize build config/crd | kubectl apply -f -
+TIME_LONG	= `date +%Y-%m-%d' '%H:%M:%S`
+TIME_SHORT	= `date +%H:%M:%S`
+TIME		= $(TIME_SHORT)
 
-# Uninstall CRDs from a cluster
-uninstall: manifests
-	kustomize build config/crd | kubectl delete -f -
+INFO	= echo ${TIME} ${BLUE}[ .. ]${CNone}
+WARN	= echo ${TIME} ${YELLOW}[WARN]${CNone}
+ERR		= echo ${TIME} ${RED}[FAIL]${CNone}
+OK		= echo ${TIME} ${GREEN}[ OK ]${CNone}
+FAIL	= (echo ${TIME} ${RED}[FAIL]${CNone} && false)
 
-.PHONY: deploy
-deploy: manifests ## Deploy controller in the Kubernetes cluster of current context
-	cd config/manager && kustomize edit set image controller=${IMG}
-	kustomize build config/default | kubectl apply -f -
+# ====================================================================================
+# Conformance
 
-manifests: controller-gen ## Generate manifests e.g. CRD, RBAC etc.
-	$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=$(CRD_DIR)
-# Remove extra header lines in generated CRDs
-	@for i in $(CRD_DIR)/*.yaml; do \
-  		tail -n +3 <"$$i" >"$$i.bkp" && \
-  		cp "$$i.bkp" "$$i" && \
-  		rm "$$i.bkp"; \
-  	done
+# Ensure a PR is ready for review.
+reviewable: generate helm.generate
+	@go mod tidy
+
+# Ensure branch is clean.
+check-diff: reviewable
+	@$(INFO) checking that branch is clean
+	@test -z "$$(git status --porcelain)" || $(FAIL)
+	@$(OK) branch is clean
+
+# ====================================================================================
+# Golang
+
+.PHONY: test
+test: generate ## Run tests
+	@$(INFO) go test unit-tests
+	go test ./... -coverprofile cover.out
+	@$(OK) go test unit-tests
+
+.PHONY: build
+build: generate ## Build binary
+	@$(INFO) go build
+	@CGO_ENABLED=0 go build -o $(OUTPUT_DIR)/external-secrets main.go
+	@$(OK) go build
 
-lint/check: # Check install of golanci-lint
+# Check install of golanci-lint
+lint.check:
 	@if ! golangci-lint --version > /dev/null 2>&1; then \
 		echo -e "\033[0;33mgolangci-lint is not installed: run \`\033[0;32mmake lint-install\033[0m\033[0;33m\` or install it from https://golangci-lint.run\033[0m"; \
 		exit 1; \
 	fi
 
-lint-install: # installs golangci-lint to the go bin dir
+# installs golangci-lint to the go bin dir
+lint.install:
 	@if ! golangci-lint --version > /dev/null 2>&1; then \
 		echo "Installing golangci-lint"; \
 		curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOBIN) v1.33.0; \
 	fi
 
-lint: lint/check ## run golangci-lint
+lint: lint.check ## run golangci-lint
 	@if ! golangci-lint run; then \
 		echo -e "\033[0;33mgolangci-lint failed: some checks can be fixed with \`\033[0;32mmake fmt\033[0m\033[0;33m\`\033[0m"; \
 		exit 1; \
 	fi
 
-fmt: lint/check ## ensure consistent code style
-	go mod tidy
-	go fmt ./...
-	golangci-lint run --fix > /dev/null 2>&1 || true
-
-generate: controller-gen ## Generate code
-	$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
-
-docker-build: test ## Build the docker image
-	docker build . -t ${IMG}
-
-docker-push: ## Push the docker image
-	docker push ${IMG}
-
-helm-docs: ## Generate helm docs
-	cd $(HELM_DIR); \
-	docker run --rm -v $(shell pwd)/$(HELM_DIR):/helm-docs -u $(shell id -u) jnorwood/helm-docs:latest
-
-crds-to-chart: # Copy crds to helm chart directory
-	cp $(CRD_DIR)/*.yaml $(HELM_DIR)/templates/crds/
-# Add helm chart if statement for installing CRDs
-	@for i in $(HELM_DIR)/templates/crds/*.yaml; do \
-		cp "$$i" "$$i.bkp" && \
-		echo "{{- if .Values.installCRDs }}" > "$$i" && \
-		cat "$$i.bkp" >> "$$i" && \
-		echo "{{- end }}" >> "$$i" && \
-		rm "$$i.bkp"; \
-	done
+fmt: lint.check ## ensure consistent code style
+	@go mod tidy
+	@go fmt ./...
+	@golangci-lint run --fix > /dev/null 2>&1 || true
+	@$(OK) Ensured consistent code style
 
+generate: controller-gen ## Generate code, crds, manifests, etc
+	@$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
+	@$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=$(CRD_DIR)
+# Remove extra header lines in generated CRDs
+	@for i in $(CRD_DIR)/*.yaml; do \
+  		tail -n +3 <"$$i" >"$$i.bkp" && \
+  		cp "$$i.bkp" "$$i" && \
+  		rm "$$i.bkp"; \
+  	done
+	@$(OK) Finished generating deepcopy and manifests
 
-# find or download controller-gen
-# download controller-gen if necessary
+# Find or download controller-gen
 controller-gen:
 ifeq (, $(shell which controller-gen))
 	@{ \
@@ -118,7 +140,79 @@ else
 CONTROLLER_GEN=$(shell which controller-gen)
 endif
 
+# ====================================================================================
+# Local Utility
+
+# This is for running out-of-cluster locally, and is for convenience.
+# For more control, try running the binary directly with different arguments.
+run: generate
+	go run ./main.go
+
+# Install CRDs into a cluster. This is for convenience.
+crds.install: generate
+	kubectl apply -f $(CRD_DIR)
+
+# Uninstall CRDs from a cluster. This is for convenience.
+crds.uninstall:
+	kubectl delete -f $(CRD_DIR)
+
+# ====================================================================================
+# Helm Chart
+
+helm.docs: ## Generate helm docs
+	cd $(HELM_DIR); \
+	docker run --rm -v $(shell pwd)/$(HELM_DIR):/helm-docs -u $(shell id -u) jnorwood/helm-docs:latest
+
+helm.build: helm.generate ## Build helm chart
+	@$(INFO) helm package
+	@helm package $(HELM_DIR) --dependency-update --destination $(OUTPUT_DIR)/chart
+	@$(OK) helm package
+
+# Copy crds to helm chart directory
+helm.generate:
+	@cp $(CRD_DIR)/*.yaml $(HELM_DIR)/templates/crds/
+# Add helm if statement for controlling the install of CRDs
+	@for i in $(HELM_DIR)/templates/crds/*.yaml; do \
+		cp "$$i" "$$i.bkp" && \
+		echo "{{- if .Values.installCRDs }}" > "$$i" && \
+		cat "$$i.bkp" >> "$$i" && \
+		echo "{{- end }}" >> "$$i" && \
+		rm "$$i.bkp"; \
+	done
+	@$(OK) Finished generating helm chart files
+
+# ====================================================================================
+# Build Artifacts
+
+build.all: docker.build helm.build
+
+docker.build: build ## Build the docker image
+	@$(INFO) docker build
+	@docker build . -t $(IMAGE_REGISTRY):$(VERSION)
+	@$(OK) docker build
+
+docker.push:
+	@$(INFO) docker push
+	@docker push $(IMAGE_REGISTRY):$(VERSION)
+	@$(OK) docker push
+
+# RELEASE_TAG is tag to promote. Default is promooting to main branch, but can be overriden
+# to promote a tag to a specific version.
+RELEASE_TAG ?= main
+SOURCE_TAG ?= $(VERSION)
+
+
+docker.promote:
+	@$(INFO) docker pull $(SOURCE_TAG)
+	@docker pull $(IMAGE_REGISTRY):$(SOURCE_TAG)
+	@docker tag $(IMAGE_REGISTRY):$(SOURCE_TAG) $(IMAGE_REGISTRY):$(RELEASE_TAG)
+	@docker push $(IMAGE_REGISTRY):$(RELEASE_TAG)
+	@$(OK) docker push $(RELEASE_TAG)
+
+
+# ====================================================================================
+# Help
+
+# only comments after make target name are shown as help text
 help: ## displays this help message
-	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_\/-]+:.*?## / {printf "\033[34m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | \
-		sort | \
-		grep -v '#'
+	@echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s : | sort)"