diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2ce852169ca08eab97324bc3617729c9515ee950..c90da0fdc86b1b0b277e118ade011290cde45b44 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,3 +1,9 @@
+workflow:
+  rules:
+    - if: $CI_MERGE_REQUEST_IID
+    - if: $CI_COMMIT_TAG
+    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_REF_PROTECTED == 'true'
+
 variables:
   DOCKER_DRIVER: overlay2
 
@@ -17,3 +23,4 @@ include:
   - local: .gitlab/ci/shellcheck.gitlab-ci.yml
   - local: .gitlab/ci/test.gitlab-ci.yml
   - local: .gitlab/ci/release.gitlab-ci.yml
+  - local: .gitlab/ci/chart.gitlab-ci.yml
diff --git a/.gitlab/ci/chart.gitlab-ci.yml b/.gitlab/ci/chart.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bbbafcafa06374d7134a1534253cf506e40ca255
--- /dev/null
+++ b/.gitlab/ci/chart.gitlab-ci.yml
@@ -0,0 +1,59 @@
+variables:
+  SAST_DISABLE_DIND: "true"
+  SCAN_KUBERNETES_MANIFESTS: "true"
+  SAST_DEFAULT_ANALYZERS: "kubesec,secrets"
+
+include:
+  - template: SAST.gitlab-ci.yml
+
+stages:
+  - build
+  - test
+  - release
+
+.chart-job:
+  image: "registry.gitlab.com/gitlab-org/gitlab-build-images:alpine-helm"
+  before_script:
+    - cd assets/auto-deploy-app
+
+chart:compile_manifests:
+  extends: .chart-job
+  stage: build
+  script:
+    - mkdir manifests
+    - helm init --client-only
+    - helm dependency build .
+    - helm template -f values.yaml --output-dir manifests .
+  artifacts:
+    paths:
+      - manifests
+
+chart:lint:
+  extends: .chart-job
+  stage: test
+  script:
+    - helm lint .
+
+kubesec-sast:
+  needs: ["chart:compile_manifests"]
+
+chart:test:
+  extends: .chart-job
+  stage: test
+  script:
+    - apk add --no-cache build-base go
+    - helm init --client-only
+    - helm dependency build .
+    - cd test && GO111MODULE=auto go test .
+
+# auto-deploy-image doesn't need to release the chart to https://charts.gitlab.io/,
+# as it bundles a chart by default.
+# release-chart:
+#   stage: release
+#   script:
+#     - curl --fail --request POST --form "token=${CHARTS_TRIGGER_TOKEN}" --form ref=master
+#         --form "variables[CHART_NAME]=$CI_PROJECT_NAME"
+#         --form "variables[RELEASE_REF]=$CI_COMMIT_REF_NAME"
+#         https://gitlab.com/api/v4/projects/2860651/trigger/pipeline
+#   only:
+#     - /\Av[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?\Z/@gitlab-org/charts/auto-deploy-app
diff --git a/.gitlab/ci/release.gitlab-ci.yml b/.gitlab/ci/release.gitlab-ci.yml
index 089cef1e357f26979892284d887c5031b611b37c..199072f6047caa33843498989f65fef4fab82738 100644
--- a/.gitlab/ci/release.gitlab-ci.yml
+++ b/.gitlab/ci/release.gitlab-ci.yml
@@ -8,13 +8,6 @@
 .semantic-release:
   image: node:12
   stage: release
-  before_script:
-    - npm install -g semantic-release @semantic-release/gitlab
-  script:
-    - semantic-release $DRY_RUN_OPT
-  only:
-    variables:
-      - $CI_API_V4_URL == "https://gitlab.com/api/v4"
 
 release-tag:
   stage: release
@@ -31,24 +24,46 @@ release-tag:
     - docker tag "$BUILD_IMAGE_NAME" $ci_image:$ci_image_tag
     - docker push $ci_image:latest
     - docker push $ci_image:$ci_image_tag
-  only:
-    - tags
+  rules:
+    - if: $CI_COMMIT_TAG
 
 publish:
   extends: .semantic-release
-  only:
-    refs:
-      - master@gitlab-org/cluster-integration/auto-deploy-image
-      - beta@gitlab-org/cluster-integration/auto-deploy-image
-      - /^\d+\.x$/@gitlab-org/cluster-integration/auto-deploy-image
-      - /^\d+\.\d+\.x$/@gitlab-org/cluster-integration/auto-deploy-image
+  before_script:
+    - npm install -g semantic-release @semantic-release/gitlab
+  script:
+    - semantic-release
+  rules:
+    # Only protected branches on the official project
+    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_REF_PROTECTED == 'true' && $CI_PROJECT_PATH == 'gitlab-org/cluster-integration/auto-deploy-image' && $CI_API_V4_URL == "https://gitlab.com/api/v4"
 
 publish-dryrun:
   extends: .semantic-release
-  variables:
-    DRY_RUN_OPT: '-d'
-  only:
-    - branches@gitlab-org/cluster-integration/auto-deploy-image
-  except:
-    refs:
-      - master
+  before_script:
+    - npm install -g semantic-release @semantic-release/gitlab
+    - git fetch origin refs/merge-requests/$CI_MERGE_REQUEST_IID/merge:$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
+    - git checkout $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
+    # Overriding the variable that semantic-release runs on.
+    # https://github.com/pvdlg/env-ci/blob/master/services/gitlab.js
+    - export CI_COMMIT_REF_NAME=$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
+    - unset CI_MERGE_REQUEST_ID
+  script:
+    - semantic-release -d |tee output.log
+    # Check if the bundled chart version matches the next auto-deploy-image version.
+    - next_release_version=$(cat output.log | grep -oP "The next release version is \K.*$") || true
+    - bundled_chart_version=$(cat assets/auto-deploy-app/Chart.yaml | grep -oP "version:\s*\K.*$")
+    - echo "next_release_version is $next_release_version"
+    - echo "bundled_chart_version is $bundled_chart_version"
+    - |
+      if [ -n "${next_release_version}" ] && [ -n "${bundled_chart_version}" ] && [ "${next_release_version}" != "${bundled_chart_version}" ]; then
+        cat <<- EOS
+      [WARN] auto-deploy-app chart version mismatch error.
+      This merge request triggers to create a new release, auto-deploy-image ${next_release_version}.
+      This version must be matched to the auto-deploy-app chart's version, however, currently it's set to ${bundled_chart_version}.
+      Please set ${next_release_version} to the version column in assets/auto-deploy-app/Chart.yaml to resovle this error.
+      EOS
+      exit 1
+      fi
+  rules:
+    - if: $CI_MERGE_REQUEST_IID && $CI_PROJECT_PATH == 'gitlab-org/cluster-integration/auto-deploy-image' && $CI_API_V4_URL == "https://gitlab.com/api/v4"
+  needs: []
diff --git a/.gitlab/ci/shellcheck.gitlab-ci.yml b/.gitlab/ci/shellcheck.gitlab-ci.yml
index ef110ec611b6693a57475b1fd540f4d5cb245f76..7c57e9febd7ccee9062ca9f1658c89e7def474a1 100644
--- a/.gitlab/ci/shellcheck.gitlab-ci.yml
+++ b/.gitlab/ci/shellcheck.gitlab-ci.yml
@@ -3,7 +3,7 @@ test-shellcheck:
   image: koalaman/shellcheck-alpine:stable
   needs: []
   script:
-    - shellcheck src/bin/auto-deploy test/*
+    - shellcheck src/bin/auto-deploy test/verify-application-secret test/verify-deployment-database
 
 test-shfmt:
   stage: test
@@ -12,4 +12,4 @@ test-shfmt:
     entrypoint: ["/bin/sh", "-c"]
   needs: []
   script:
-    - shfmt -i 2 -ci -l -d src/bin/auto-deploy test/*
+    - shfmt -i 2 -ci -l -d src/bin/auto-deploy test/verify-application-secret test/verify-deployment-database
diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml
index ecbe3860e04d01585ea2a803a51bb95a7c292f4d..b29d11b4a1858a00491e67d17d70fde298c3d577 100644
--- a/.gitlab/ci/test.gitlab-ci.yml
+++ b/.gitlab/ci/test.gitlab-ci.yml
@@ -57,11 +57,18 @@ test-kube-domain_error:
     - auto-deploy check_kube_domain && expected_error || failed_as_expected
 
 test-download-chart:
+  <<: *test-job
+  script:
+    - auto-deploy download_chart
+    - ./test/verify-chart-version 1
+
+test-download-chart-from-repo:
   <<: *test-job
   variables:
-    GIT_STRATEGY: none
+    AUTO_DEVOPS_CHART: gitlab/auto-deploy-app
   script:
     - auto-deploy download_chart
+    - ./test/verify-chart-version 0
 
 test-deploy-name:
   <<: *test-job
@@ -443,3 +450,30 @@ test-delete-canary-postgresql:
     - helm get production-canary && expected_error || failed_as_expected
     - helm get production
     - helm get production-postgresql
+
+test-chart-major-version-upgrade:
+  extends: test-deploy
+  script:
+    - auto-deploy initialize_tiller
+    # Downloading legacy v0 chart from charts.gitlab.io and the deployment should succeed
+    - AUTO_DEVOPS_CHART=gitlab/auto-deploy-app auto-deploy download_chart
+    - auto-deploy deploy
+    - rm -Rf chart
+    # Copying bundled chart from local storage and the deployment should fail
+    - auto-deploy download_chart
+    - "sed -i 's/version:.*/version: 10.0.0/g' chart/Chart.yaml"
+    - cat chart/Chart.yaml
+    - auto-deploy deploy| tee deploy.log || true
+    - grep -q "Detected a major version difference" deploy.log || exit 1
+    # Force deploy with the AUTO_DEVOPS_FORCE_DEPLOY option and the deployment should succeed
+    - export AUTO_DEVOPS_FORCE_DEPLOY_V10=true
+    - auto-deploy deploy| tee deploy.log
+    - grep -q "allowed to force deploy" deploy.log || exit 1
+
+rspec:
+  stage: test
+  image: ruby:2.5
+  before_script:
+    - gem install rspec
+  script:
+    - rspec test/rspec
diff --git a/Dockerfile b/Dockerfile
index 65397efe3f30555f327fa120ac639fe4df28b05d..b89144f866cc37801e42a7dd205514f84dd66191 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,14 +6,15 @@ FROM "registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/rele
 # https://github.com/sgerrand/alpine-pkg-glibc
 ARG GLIBC_VERSION
 
-COPY src/ build/
-
 # Install Dependencies
 RUN apk add --no-cache openssl curl tar gzip bash jq \
   && curl -sSL -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub \
   && curl -sSL -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk \
   && apk add glibc-${GLIBC_VERSION}.apk \
-  && apk add ruby jq \
+  && apk add ruby jq ruby-json \
   && rm glibc-${GLIBC_VERSION}.apk
 
+COPY src/ build/
+COPY assets/ assets/
+
 RUN ln -s /build/bin/* /usr/local/bin/
diff --git a/README.md b/README.md
index ad6524ba4cb8ab1d29d4f0a9352bd6cdf5c8dc8f..4c1561b0e996c8c80a41295e961a5d59124a7539 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,14 @@ template will use the Docker image generated from this project. Changes from pre
 *  All the other commands should be prepended with `auto-deploy`.
    For example, `check_kube_domain` now becomes `auto-deploy check_kube_domain`.
 
+### v1.0.0
+
+Before v1.0.0, auto-deploy-image was downloading a chart from [the chart repository](https://charts.gitlab.io/),
+which was then uploaded by the [auto-deploy-app](https://gitlab.com/gitlab-org/charts/auto-deploy-app) project.
+
+Since auto-deploy-image v1.0.0, the auto-deploy-app chart is bundled into the auto-deploy-image docker image as a local asset,
+and it no longer downloads the chart from the repository.
+
 # Generating a new auto-deploy image
 
 To generate a new image you must follow the git commit guidelines below, this
diff --git a/assets/auto-deploy-app/CONTRIBUTING.md b/assets/auto-deploy-app/CONTRIBUTING.md
new file mode 100644
index 0000000000000000000000000000000000000000..2354acbbc56c5570432f50fa7327eac208bba6e1
--- /dev/null
+++ b/assets/auto-deploy-app/CONTRIBUTING.md
@@ -0,0 +1,53 @@
+## Contributing
+
+Thank you for your interest in contributing to this GitLab project! We welcome
+all contributions. By participating in this project, you agree to abide by the
+[code of conduct](#code-of-conduct).
+
+
+## Developer Certificate of Origin + License
+
+By contributing to GitLab B.V., You accept and agree to the following terms and
+conditions for Your present and future Contributions submitted to GitLab B.V.
+Except for the license granted herein to GitLab B.V. and recipients of software
+distributed by GitLab B.V., You reserve all right, title, and interest in and to
+Your Contributions. All Contributions are subject to the following DCO + License
+terms.
+
+[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
+
+_This notice should stay as the first item in the CONTRIBUTING.md file._
+
+## Code of conduct
+
+We want to create a welcoming environment for everyone who is interested
+in contributing. Please visit our [Code of Conduct
+page](https://about.gitlab.com/contributing/code-of-conduct) to learn
+more about our commitment to an open and welcoming environment.
+
+## Merge request guidelines
+
+Below are some guidelines for merge requests:
+
+- Any new configuration option should be documented in
+  the `Configuration` section in README.md.
+- For any template changes, we encourage a test case be added or
+  updated in the
+  [template tests](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/blob/master/test/template_test.go).
+
+### Working with the tests
+
+The tests are written in [Go](https://golang.org) (version 1.13 or later,
+with [modules enabled](https://golang.org/cmd/go/#hdr-Module_support)) using
+the [Terratest](https://github.com/gruntwork-io/terratest) library. To work
+on the tests, you need to have [Helm 2](https://v2.helm.sh/docs/) and
+[Go](https://golang.org) installed.
+
+To run the tests, run the following commands from the root of your copy of `auto-deploy-app`:
+
+```shell
+helm init --client-only               # required only once
+helm dependency build .               # required only once
+cd test
+GO111MODULE=auto go test .            # required for every change to the tests or the template
+```
diff --git a/assets/auto-deploy-app/Chart.yaml b/assets/auto-deploy-app/Chart.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d6f908cffea4f5752683848ea705cc752f1067d0
--- /dev/null
+++ b/assets/auto-deploy-app/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+description: GitLab's Auto-deploy Helm Chart
+name: auto-deploy-app
+version: 1.0.0
+icon: https://gitlab.com/gitlab-com/gitlab-artwork/raw/master/logo/logo-square.png
diff --git a/assets/auto-deploy-app/LICENSE b/assets/auto-deploy-app/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..a90ea939517d05f44eb4196dcdaba0f57aab15f2
--- /dev/null
+++ b/assets/auto-deploy-app/LICENSE
@@ -0,0 +1,19 @@
+Copyright GitLab B.V.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/assets/auto-deploy-app/README.md b/assets/auto-deploy-app/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..095df11e46134d8d0a0602ea6a774e41b0375c8b
--- /dev/null
+++ b/assets/auto-deploy-app/README.md
@@ -0,0 +1,84 @@
+# GitLab's Auto-deploy Helm Chart
+
+## Deprecation Notice
+
+GitLab is moving all development for `auto-deploy-app` into [`auto-deploy-image`](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image). 
+Going forward, the `auto-deploy-app` Helm chart will be bundled with `auto-deploy-image`
+and will no longer released as a stand-alone Helm chart. Existing releases of `auto-deploy-app`
+will remain in [GitLab's chart registry](http://charts.gitlab.io/).
+
+If you have any questions, please ask in <https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/issues/70>.
+
+## Requirements
+
+- Helm `2.9.0` and above is required in order support `"helm.sh/hook-delete-policy": before-hook-creation` for migrations
+
+## Configuration
+
+| Parameter                     | Description | Default                            |
+| ---                           | ---         | ---                                |
+| replicaCount                  |             | `1`                                |
+| strategyType                  | Pod deployment [strategy](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy) | `nil` |
+| enableSelector                | If `true`, enables selector field for the deployment. Only applicable for `extensions/v1beta1`, as selector field will always be included for `apps/v1` | `nil` |
+| deploymentApiVersion          | Sets `apiVersion` field for the deployment. Can be set to either `extensions/v1beta1` or `apps/v1`. | `extensions/v1beta1` |
+| image.repository              |             | `gitlab.example.com/group/project` |
+| image.tag                     |             | `stable`                           |
+| image.pullPolicy              |             | `Always`                           |
+| image.secrets                 |             | `[name: gitlab-registry]`          |
+| podAnnotations                | Pod annotations | `{}`                           |
+| application.track             |             | `stable`                           |
+| application.tier              |             | `web`                              |
+| application.migrateCommand    | If present, this variable will run as a shell command within an application Container as a Helm pre-upgrade Hook. Intended to run migration commands. | `nil` |
+| application.initializeCommand | If present, this variable will run as shell command within an application Container as a Helm post-install Hook. Intended to run database initialization commands. When set, the Deployment resource will be skipped.| `nil` |
+| application.secretName        | Pass in the name of a Secret which the deployment will [load all key-value pairs from the Secret as environment variables](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#configure-all-key-value-pairs-in-a-configmap-as-container-environment-variables) in the application container. | `nil` |
+| application.secretChecksum    | Pass in the checksum of the secrets referenced by `application.secretName`. | `nil` |
+| hpa.enabled                   | If true, enables horizontal pod autoscaler. A resource request is also required to be set, such as `resources.requests.cpu: 200m`.| `false` |
+| hpa.minReplicas               |             | `1`                                |
+| hpa.maxReplicas               |             | `5`                                |
+| hpa.targetCPUUtilizationPercentage | Percentage threshold when HPA begins scaling out pods | `80` |
+| gitlab.app                    | GitLab project slug. | `nil` |
+| gitlab.env                    | GitLab environment slug. | `nil` |
+| gitlab.envName                | GitLab environment name. | `nil` |
+| gitlab.envURL                 | GitLab environment URL.  | `nil` |
+| service.enabled               |             | `true`                             |
+| service.annotations           | Service annotations | `{}`                       |
+| service.name                  |             | `web`                              |
+| service.type                  |             | `ClusterIP`                        |
+| service.url                   |             | `http://my.host.com/`              |
+| service.additionalHosts       | If present, this list will add additional hostnames to the server configuration. | `nil` |
+| service.commonName            | If present, this will define the ssl certificate common name to be used by CertManager. `service.url` and `service.additionalHosts` will be added as Subject Alternative Names (SANs) | `nil` |
+| service.externalPort          |             | `5000`                             |
+| service.internalPort          |             | `5000`                             |
+| ingress.enabled               | If true, enables ingress | `true`                |
+| ingress.tls.enabled           | If true, enables SSL | `true`                    |
+| ingress.tls.secretName        | Name of the secret used to terminate SSL traffic | `""` |
+| ingress.modSecurity.enabled | Enable custom configuration for modsecurity, defaulting to [the Core Rule Set](https://coreruleset.org) | `false` |
+| ingress.modSecurity.secRuleEngine | Configuration for [ModSecurity's rule engine](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)#SecRuleEngine) | `DetectionOnly` |
+| ingress.modSecurity.secRules | Configuration for custom [ModSecurity's rules](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)#secrule) | `nil` |
+| ingress.annotations           | Ingress annotations | `{kubernetes.io/tls-acme: "true", kubernetes.io/ingress.class: "nginx"}` |
+| livenessProbe.path            | Path to access on the HTTP server on periodic probe of container liveness. | `/`                                |
+| livenessProbe.scheme          | Scheme to access the HTTP server (HTTP or HTTPS). | `HTTP`                                |
+| livenessProbe.initialDelaySeconds | # of seconds after the container has started before liveness probes are initiated. | `15`                               |
+| livenessProbe.timeoutSeconds  | # of seconds after which the liveness probe times out. | `15`                               |
+| livenessProbe.probeType       | Type of [liveness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes) to use. | `httpGet`
+| livenessProbe.command         | Commands for use with probe type 'exec'. | `{}`
+| readinessProbe.path           | Path to access on the HTTP server on periodic probe of container readiness. | `/`                                |
+| readinessProbe.scheme         | Scheme to access the HTTP server (HTTP or HTTPS). | `HTTP`                                |
+| readinessProbe.initialDelaySeconds | # of seconds after the container has started before readiness probes are initiated. | `5`                                |
+| readinessProbe.timeoutSeconds | # of seconds after which the readiness probe times out. | `3`                                |
+| readinessProbe.probeType     | Type of [readiness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes) to use. | `httpGet`
+| readinessProbe.command       | Commands for use with probe type 'exec'. | `{}`
+| postgresql.enabled            |             | `true`                             |
+| postgresql.managed            | If true, this will provision a managed Postgres instance via crossplane.            | `false`                             |
+| postgresql.managedClassSelector            | This will allow provisioning a Postgres instance based on label selectors via Crossplane, eg: `managedClassSelector.matchLabels.stack: gitlab`. The `postgresql.managed` value should be true as well for this to be honoured. [Crossplane Configuration](https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane)            | `{}`                             |
+| podDisruptionBudget.enabled   |             | `false`                            |
+| podDisruptionBudget.maxUnavailable |             | `1`                            |
+| podDisruptionBudget.minAvailable | If present, this variable will configure minAvailable in the PodDisruptionBudget. :warning: if you have `replicaCount: 1` and `podDisruptionBudget.minAvailable: 1` `kubectl drain` will be blocked.              | `nil`                            |
+| prometheus.metrics            | Annotates the service for prometheus auto-discovery. Also denies access to the `/metrics` endpoint from external addresses with Ingress. | `false` |
+| networkPolicy.enabled         | Enable container network policy | `false` |
+| networkPolicy.spec            | [Network policy](https://kubernetes.io/docs/concepts/services-networking/network-policies/) definition | `{ podSelector: { matchLabels: {} }, ingress: [{ from: [{ podSelector: { matchLabels: {} } }, { namespaceSelector: { matchLabels: { app.gitlab.com/managed_by: gitlab } } }] }] }` |
+
+## PostgreSQL
+
+This chart depends on version 0.7.1 of the `stable/postgresql` chart.
+For reference the source code for this specific version can be found at https://github.com/helm/charts/tree/b90ad657e1a226eb52c3eb6a2a95ba3d6d494f58/stable/postgresql
\ No newline at end of file
diff --git a/assets/auto-deploy-app/requirements.lock b/assets/auto-deploy-app/requirements.lock
new file mode 100644
index 0000000000000000000000000000000000000000..c92fba599d5440f06d9929ac994ed1d1da7c8ab8
--- /dev/null
+++ b/assets/auto-deploy-app/requirements.lock
@@ -0,0 +1,6 @@
+dependencies:
+- name: postgresql
+  repository: https://kubernetes-charts.storage.googleapis.com/
+  version: 0.7.1
+digest: sha256:358ce85fe4d3461ea6bb96713470a80de9c1324214a2e6f97d800298c02530e2
+generated: 2017-08-28T15:22:30.690341342-05:00
diff --git a/assets/auto-deploy-app/requirements.yaml b/assets/auto-deploy-app/requirements.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8d8838411e3d3f67a9e64109787ddb205681f33f
--- /dev/null
+++ b/assets/auto-deploy-app/requirements.yaml
@@ -0,0 +1,5 @@
+dependencies:
+  - name: postgresql
+    version: "0.7.1"
+    repository: "https://kubernetes-charts.storage.googleapis.com/"
+    condition: postgresql.enabled
diff --git a/assets/auto-deploy-app/templates/NOTES.txt b/assets/auto-deploy-app/templates/NOTES.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5491ce9318e4fd2578c6c86de79f2bd268d00739
--- /dev/null
+++ b/assets/auto-deploy-app/templates/NOTES.txt
@@ -0,0 +1,12 @@
+{{- if and .Values.ingress.enabled .Values.service.enabled -}}
+Application should be accessible at
+
+    {{ .Values.service.url }}
+{{- else -}}
+Application was deployed reusing the service at
+
+    {{ .Values.service.url }}
+
+It will share a load balancer with the previous release (or be unavailable if
+no service or ingress was previously deployed).
+{{- end -}}
diff --git a/assets/auto-deploy-app/templates/_helpers.tpl b/assets/auto-deploy-app/templates/_helpers.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..87545f424bb65d305dfd257a1c4e8a688ede21cc
--- /dev/null
+++ b/assets/auto-deploy-app/templates/_helpers.tpl
@@ -0,0 +1,50 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 24 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+*/}}
+{{- define "fullname" -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- printf "%s-%s" .Release.Name $name | trimSuffix "-app" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{- define "appname" -}}
+{{- $releaseName := default .Release.Name .Values.releaseOverride -}}
+{{- printf "%s" $releaseName | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{- define "imagename" -}}
+{{- if eq .Values.image.tag "" -}}
+{{- .Values.image.repository -}}
+{{- else -}}
+{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
+{{- end -}}
+{{- end -}}
+
+{{- define "trackableappname" -}}
+{{- $trackableName := printf "%s-%s" (include "appname" .) .Values.application.track -}}
+{{- $trackableName | trimSuffix "-stable" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Get a hostname from URL
+*/}}
+{{- define "hostname" -}}
+{{- . | trimPrefix "http://" |  trimPrefix "https://" | trimSuffix "/" | quote -}}
+{{- end -}}
+
+{{/*
+Get SecRule's arguments with unescaped single&double quotes
+*/}}
+{{- define "secrule" -}}
+{{- $operator := .operator | quote | replace "\"" "\\\"" | replace "'" "\\'" -}}
+{{- $action := .action | quote | replace "\"" "\\\"" | replace "'" "\\'" -}}
+{{- printf "SecRule %s %s %s" .variable $operator $action -}}
+{{- end -}}
\ No newline at end of file
diff --git a/assets/auto-deploy-app/templates/db-initialize-job.yaml b/assets/auto-deploy-app/templates/db-initialize-job.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..535731a51a2093396cb212f1b865234dd93ec395
--- /dev/null
+++ b/assets/auto-deploy-app/templates/db-initialize-job.yaml
@@ -0,0 +1,43 @@
+{{- if .Values.application.initializeCommand -}}
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: {{ template "trackableappname" . }}-db-initialize
+  labels:
+    app: {{ template "appname" . }}
+    chart: "{{ .Chart.Name }}-{{ .Chart.Version| replace "+" "_" }}"
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+  annotations:
+    "helm.sh/hook": post-install
+    "helm.sh/hook-delete-policy": before-hook-creation
+    "helm.sh/hook-weight": "0"
+spec:
+  template:
+    metadata:
+      labels:
+        app: {{ template "appname" . }}
+        release: {{ .Release.Name }}
+    spec:
+      restartPolicy: Never
+      imagePullSecrets:
+{{ toYaml .Values.image.secrets | indent 10 }}
+      containers:
+      - name: {{ .Chart.Name }}
+        image: {{ template "imagename" . }}
+        command: ["/bin/sh"]
+        args: ["-c", "{{ .Values.application.initializeCommand }}"]
+        imagePullPolicy: {{ .Values.image.pullPolicy }}
+        {{- if .Values.application.secretName }}
+        envFrom:
+        - secretRef:
+            name: {{ .Values.application.secretName }}
+        {{- end }}
+        env:
+        - name: DATABASE_URL
+          value: {{ .Values.application.database_url | quote }}
+        - name: GITLAB_ENVIRONMENT_NAME
+          value: {{ .Values.gitlab.envName | quote }}
+        - name: GITLAB_ENVIRONMENT_URL
+          value: {{ .Values.gitlab.envURL | quote }}
+{{- end -}}
diff --git a/assets/auto-deploy-app/templates/db-migrate-hook.yaml b/assets/auto-deploy-app/templates/db-migrate-hook.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..78b871fea1421f5051acd415954d204c0316d1cf
--- /dev/null
+++ b/assets/auto-deploy-app/templates/db-migrate-hook.yaml
@@ -0,0 +1,43 @@
+{{- if .Values.application.migrateCommand -}}
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: {{ template "trackableappname" . }}-db-migrate
+  labels:
+    app: {{ template "appname" . }}
+    chart: "{{ .Chart.Name }}-{{ .Chart.Version| replace "+" "_" }}"
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+  annotations:
+    "helm.sh/hook": pre-upgrade
+    "helm.sh/hook-delete-policy": before-hook-creation
+    "helm.sh/hook-weight": "0"
+spec:
+  template:
+    metadata:
+      labels:
+        app: {{ template "appname" . }}
+        release: {{ .Release.Name }}
+    spec:
+      restartPolicy: Never
+      imagePullSecrets:
+{{ toYaml .Values.image.secrets | indent 10 }}
+      containers:
+      - name: {{ .Chart.Name }}
+        image: {{ template "imagename" . }}
+        command: ["/bin/sh"]
+        args: ["-c", "{{ .Values.application.migrateCommand }}"]
+        imagePullPolicy: {{ .Values.image.pullPolicy }}
+        {{- if .Values.application.secretName }}
+        envFrom:
+        - secretRef:
+            name: {{ .Values.application.secretName }}
+        {{- end }}
+        env:
+        - name: DATABASE_URL
+          value: {{ .Values.application.database_url | quote }}
+        - name: GITLAB_ENVIRONMENT_NAME
+          value: {{ .Values.gitlab.envName | quote }}
+        - name: GITLAB_ENVIRONMENT_URL
+          value: {{ .Values.gitlab.envURL | quote }}
+{{- end -}}
diff --git a/assets/auto-deploy-app/templates/deployment.yaml b/assets/auto-deploy-app/templates/deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c97969a22e1e21eaaa968c1c003a03fe0f4ced52
--- /dev/null
+++ b/assets/auto-deploy-app/templates/deployment.yaml
@@ -0,0 +1,117 @@
+{{- if not .Values.application.initializeCommand -}}
+apiVersion: {{ default "extensions/v1beta1" .Values.deploymentApiVersion }}
+kind: Deployment
+metadata:
+  name: {{ template "trackableappname" . }}
+  annotations:
+    {{ if .Values.gitlab.app }}app.gitlab.com/app: {{ .Values.gitlab.app | quote }}{{ end }}
+    {{ if .Values.gitlab.env }}app.gitlab.com/env: {{ .Values.gitlab.env | quote }}{{ end }}
+  labels:
+    app: {{ template "appname" . }}
+    track: "{{ .Values.application.track }}"
+    tier: "{{ .Values.application.tier }}"
+    chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+spec:
+{{- if or .Values.enableSelector (eq (default "extensions/v1beta1" .Values.deploymentApiVersion) "apps/v1") }}
+  selector:
+    matchLabels:
+      app: {{ template "appname" . }}
+      track: "{{ .Values.application.track }}"
+      tier: "{{ .Values.application.tier }}"
+      release: {{ .Release.Name }}
+{{- end }}
+  replicas: {{ .Values.replicaCount }}
+{{- if .Values.strategyType }}
+  strategy:
+    type: {{ .Values.strategyType | quote }}
+{{- end }}
+  template:
+    metadata:
+      annotations:
+        checksum/application-secrets: "{{ .Values.application.secretChecksum }}"
+        {{ if .Values.gitlab.app }}app.gitlab.com/app: {{ .Values.gitlab.app | quote }}{{ end }}
+        {{ if .Values.gitlab.env }}app.gitlab.com/env: {{ .Values.gitlab.env | quote }}{{ end }}
+{{- if .Values.podAnnotations }}
+{{ toYaml .Values.podAnnotations | indent 8 }}
+{{- end }}
+      labels:
+        app: {{ template "appname" . }}
+        track: "{{ .Values.application.track }}"
+        tier: "{{ .Values.application.tier }}"
+        release: {{ .Release.Name }}
+    spec:
+      imagePullSecrets:
+{{ toYaml .Values.image.secrets | indent 10 }}
+      containers:
+      - name: {{ .Chart.Name }}
+        image: {{ template "imagename" . }}
+        imagePullPolicy: {{ .Values.image.pullPolicy }}
+        {{- if .Values.application.secretName }}
+        envFrom:
+        - secretRef:
+            name: {{ .Values.application.secretName }}
+        {{- end }}
+        env:
+{{- if .Values.postgresql.managed }}
+        - name: POSTGRES_USER
+          valueFrom:
+            secretKeyRef:
+              name: app-postgres
+              key: username
+        - name: POSTGRES_PASSWORD
+          valueFrom:
+            secretKeyRef:
+              name: app-postgres
+              key: password
+        - name: POSTGRES_HOST
+          valueFrom:
+            secretKeyRef:
+              name: app-postgres
+              key: privateIP
+{{- end }}
+        - name: DATABASE_URL
+          value: {{ .Values.application.database_url | quote }}
+        - name: GITLAB_ENVIRONMENT_NAME
+          value: {{ .Values.gitlab.envName | quote }}
+        - name: GITLAB_ENVIRONMENT_URL
+          value: {{ .Values.gitlab.envURL | quote }}
+        ports:
+        - name: "{{ .Values.service.name }}"
+          containerPort: {{ .Values.service.internalPort }}
+        livenessProbe:
+{{- if eq .Values.livenessProbe.probeType "httpGet" }}
+          httpGet:
+            path: {{ .Values.livenessProbe.path }}
+            scheme: {{ .Values.livenessProbe.scheme }}
+            port: {{ .Values.service.internalPort }}
+{{- else if eq .Values.livenessProbe.probeType "tcpSocket" }}
+          tcpSocket:
+            port: {{ .Values.service.internalPort }}
+{{- else if eq .Values.livenessProbe.probeType "exec" }}
+          exec:
+            command:
+{{ toYaml .Values.livenessProbe.command | indent 14 }}
+{{- end }}
+          initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
+          timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
+        readinessProbe:
+{{- if eq .Values.readinessProbe.probeType "httpGet" }}
+          httpGet:
+            path: {{ .Values.readinessProbe.path }}
+            scheme: {{ .Values.readinessProbe.scheme }}
+            port: {{ .Values.service.internalPort }}
+{{- else if eq .Values.readinessProbe.probeType "tcpSocket" }}
+          tcpSocket:
+            port: {{ .Values.service.internalPort }}
+{{- else if eq .Values.readinessProbe.probeType "exec" }}
+          exec:
+            command:
+{{ toYaml .Values.readinessProbe.command | indent 14 }}
+{{- end }}
+          initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
+          timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
+        resources:
+{{ toYaml .Values.resources | indent 12 }}
+{{- end -}}
diff --git a/assets/auto-deploy-app/templates/hpa.yaml b/assets/auto-deploy-app/templates/hpa.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f4cb3adea3b5813a5d3f2b5a30eb82fe84d6a0c1
--- /dev/null
+++ b/assets/auto-deploy-app/templates/hpa.yaml
@@ -0,0 +1,19 @@
+{{- if and .Values.hpa.enabled .Values.resources.requests -}}
+apiVersion: autoscaling/v1
+kind: HorizontalPodAutoscaler
+metadata:
+  name: {{ template "fullname" . }}
+  labels:
+    app: {{ template "appname" . }}
+    chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+spec:
+  scaleTargetRef:
+    kind: Deployment
+    name: {{ template "appname" . }}
+    apiVersion: apps/v1beta1
+  minReplicas: {{ .Values.hpa.minReplicas }}
+  maxReplicas: {{ .Values.hpa.maxReplicas }}
+  targetCPUUtilizationPercentage: {{ .Values.hpa.targetCPUUtilizationPercentage }}
+{{- end -}}
diff --git a/assets/auto-deploy-app/templates/ingress.yaml b/assets/auto-deploy-app/templates/ingress.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6f39db2cecfe8d61a2e31bf2fae929966cf0d51c
--- /dev/null
+++ b/assets/auto-deploy-app/templates/ingress.yaml
@@ -0,0 +1,68 @@
+{{- if and (.Values.service.enabled) (eq .Values.application.track "stable") (or (.Values.ingress.enabled) (not (hasKey .Values.ingress "enabled"))) -}}
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: {{ template "fullname" . }}
+  labels:
+    app: {{ template "appname" . }}
+    chart: "{{ .Chart.Name }}-{{ .Chart.Version| replace "+" "_" }}"
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+  annotations:
+{{- if .Values.ingress.annotations }}
+{{ toYaml .Values.ingress.annotations | indent 4 }}
+{{- end }}
+{{- with .Values.ingress.modSecurity }}
+{{- if .enabled }}
+    nginx.ingress.kubernetes.io/modsecurity-transaction-id: "$server_name-$request_id"
+    nginx.ingress.kubernetes.io/modsecurity-snippet: |
+      SecRuleEngine {{ .secRuleEngine | default "DetectionOnly" | title }}
+{{- range $rule := .secRules }}
+{{ (include "secrule" $rule) | indent 6 }}
+{{- end }}
+{{- end }}
+{{- end }}
+{{- if .Values.prometheus.metrics }}
+    nginx.ingress.kubernetes.io/server-snippet: |-
+      location /metrics {
+          deny all;
+      }
+
+{{- end }}
+spec:
+{{- if .Values.ingress.tls.enabled }}
+  tls:
+  - hosts:
+{{- if .Values.service.commonName }}
+    - {{ template "hostname" .Values.service.commonName }}
+{{- end }}
+    - {{ template "hostname" .Values.service.url }}
+{{- if .Values.service.additionalHosts }}
+{{- range $host := .Values.service.additionalHosts }}
+    - {{ $host }}
+{{- end -}}
+{{- end }}
+    secretName: {{ .Values.ingress.tls.secretName | default (printf "%s-tls" (include "fullname" .)) }}
+{{- end }}
+  rules:
+  - host: {{ template "hostname" .Values.service.url }}
+    http:
+      &httpRule
+      paths:
+      - path: /
+        backend:
+          serviceName: {{ template "fullname" . }}
+          servicePort: {{ .Values.service.externalPort }}
+{{- if .Values.service.commonName }}
+  - host: {{ template "hostname" .Values.service.commonName }}
+    http:
+      <<: *httpRule
+{{- end -}}
+{{- if .Values.service.additionalHosts }}
+{{- range $host := .Values.service.additionalHosts }}
+  - host: {{ $host }}
+    http:
+      <<: *httpRule
+{{- end -}}
+{{- end -}}
+{{- end -}}
diff --git a/assets/auto-deploy-app/templates/network-policy.yaml b/assets/auto-deploy-app/templates/network-policy.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..79c0ed662514d8deb99bb19382105f8f445ce288
--- /dev/null
+++ b/assets/auto-deploy-app/templates/network-policy.yaml
@@ -0,0 +1,13 @@
+{{- if .Values.networkPolicy.enabled -}}
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: {{ template "fullname" . }}
+  labels:
+    app: {{ template "appname" . }}
+    chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+spec:
+{{ toYaml .Values.networkPolicy.spec | indent 2 }}
+{{- end -}}
diff --git a/assets/auto-deploy-app/templates/pdb.yaml b/assets/auto-deploy-app/templates/pdb.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..69bb2ed738e404493b6a450eb4244872409113a1
--- /dev/null
+++ b/assets/auto-deploy-app/templates/pdb.yaml
@@ -0,0 +1,22 @@
+{{- if .Values.podDisruptionBudget.enabled }}
+apiVersion: policy/v1beta1
+kind: PodDisruptionBudget
+metadata:
+  name: {{ template "fullname" . }}
+  labels:
+    app: {{ template "appname" . }}
+    chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+spec:
+{{- if .Values.podDisruptionBudget.minAvailable }}
+  minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
+{{- end }}
+{{- if .Values.podDisruptionBudget.maxUnavailable }}
+  maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
+{{- end }}
+  selector:
+    matchLabels:
+      app: {{ template "appname" . }}
+      release: {{ .Release.Name }}
+{{- end }}
diff --git a/assets/auto-deploy-app/templates/postgres-instance.yaml b/assets/auto-deploy-app/templates/postgres-instance.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..3fbeb73068a61a18772b390e5ed2c5a932b1fdda
--- /dev/null
+++ b/assets/auto-deploy-app/templates/postgres-instance.yaml
@@ -0,0 +1,14 @@
+{{- if .Values.postgresql.managed -}}
+apiVersion: database.crossplane.io/v1alpha1
+kind: PostgreSQLInstance
+metadata:
+  name: {{ template "appname" . }}
+spec:
+  engineVersion: "9.6"
+  writeConnectionSecretToRef:
+    name: app-postgres
+{{- if .Values.postgresql.managedClassSelector }}
+  classSelector:
+{{ toYaml .Values.postgresql.managedClassSelector | indent 4 }}
+{{- end }}
+{{- end -}}
diff --git a/assets/auto-deploy-app/templates/service.yaml b/assets/auto-deploy-app/templates/service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..676406be11de5feedd801876627055405ac51670
--- /dev/null
+++ b/assets/auto-deploy-app/templates/service.yaml
@@ -0,0 +1,29 @@
+{{- if and (.Values.service.enabled) (eq .Values.application.track "stable") -}}
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ template "fullname" . }}
+  annotations:
+{{- if .Values.service.annotations }}
+{{ toYaml .Values.service.annotations | indent 4 }}
+{{- end }}
+{{- if .Values.prometheus.metrics }}
+    prometheus.io/scrape: "true"
+    prometheus.io/port: "{{ .Values.service.internalPort }}"
+{{- end }}
+  labels:
+    app: {{ template "appname" . }}
+    chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+  - port: {{ .Values.service.externalPort }}
+    targetPort: {{ .Values.service.internalPort }}
+    protocol: TCP
+    name: {{ .Values.service.name }}
+  selector:
+    app: {{ template "appname" . }}
+    tier: "{{ .Values.application.tier }}"
+{{- end -}}
diff --git a/assets/auto-deploy-app/templates/worker-deployment.yaml b/assets/auto-deploy-app/templates/worker-deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..050e234195e1064dc1ace110946719a1444aded2
--- /dev/null
+++ b/assets/auto-deploy-app/templates/worker-deployment.yaml
@@ -0,0 +1,103 @@
+{{- if and (not .Values.application.initializeCommand) .Values.workers -}}
+apiVersion: v1
+kind: List
+items:
+{{- range $workerName, $workerConfig :=  .Values.workers }}
+- apiVersion: {{ default "extensions/v1beta1" $.Values.deploymentApiVersion }}
+  kind: Deployment
+  metadata:
+    name: {{ template "trackableappname" $ }}-{{ $workerName }}
+    annotations:
+      {{ if $.Values.gitlab.app }}app.gitlab.com/app: {{ $.Values.gitlab.app | quote }}{{ end }}
+      {{ if $.Values.gitlab.env }}app.gitlab.com/env: {{ $.Values.gitlab.env | quote }}{{ end }}
+    labels:
+      track: "{{ $.Values.application.track }}"
+      tier: worker
+      chart: "{{ $.Chart.Name }}-{{ $.Chart.Version | replace "+" "_" }}"
+      release: {{ $.Release.Name }}
+      heritage: {{ $.Release.Service }}
+  spec:
+  {{- if or $.Values.enableSelector (eq (default "extensions/v1beta1" $.Values.deploymentApiVersion) "apps/v1") }}
+    selector:
+      matchLabels:
+        track: "{{ $.Values.application.track }}"
+        tier: worker
+        release: {{ $.Release.Name }}
+  {{- end }}
+    replicas: {{ $workerConfig.replicaCount }}
+  {{- if $workerConfig.strategyType }}
+    strategy:
+      type: {{ $workerConfig.strategyType | quote }}
+  {{- end  }}
+    template:
+      metadata:
+        annotations:
+          checksum/application-secrets: "{{ $.Values.application.secretChecksum }}"
+          {{ if $.Values.gitlab.app }}app.gitlab.com/app: {{ $.Values.gitlab.app | quote }}{{ end }}
+          {{ if $.Values.gitlab.env }}app.gitlab.com/env: {{ $.Values.gitlab.env | quote }}{{ end }}
+  {{- if $.Values.podAnnotations }}
+  {{ toYaml $.Values.podAnnotations | indent 10 }}
+  {{- end }}
+        labels:
+          track: "{{ $.Values.application.track }}"
+          tier: worker
+          release: {{ $.Release.Name }}
+      spec:
+        imagePullSecrets:
+  {{ toYaml $.Values.image.secrets | indent 12 }}
+        terminationGracePeriodSeconds: {{ $workerConfig.terminationGracePeriodSeconds }}
+        containers:
+        - name: {{ $.Chart.Name }}-{{ $workerName }}
+          image: {{ template "imagename" $ }}
+          command:
+          {{- range $workerConfig.command }}
+          - {{ . }}
+          {{- end }}
+          imagePullPolicy: {{ $.Values.image.pullPolicy }}
+          {{- if $.Values.application.secretName }}
+          envFrom:
+          - secretRef:
+              name: {{ $.Values.application.secretName }}
+          {{- end }}
+          env:
+          - name: DATABASE_URL
+            value: {{ $.Values.application.database_url | quote }}
+          - name: GITLAB_ENVIRONMENT_NAME
+            value: {{ $.Values.gitlab.envName | quote }}
+          livenessProbe:
+{{- if eq $.Values.livenessProbe.probeType "httpGet" }}
+            httpGet:
+              path: {{ $.Values.livenessProbe.path }}
+              scheme: {{ $.Values.livenessProbe.scheme }}
+              port: {{ $.Values.service.internalPort }}
+{{- else if eq $.Values.livenessProbe.probeType "tcpSocket" }}
+            tcpSocket:
+              port: {{ $.Values.service.internalPort }}
+{{- end }}
+            initialDelaySeconds: {{ $.Values.livenessProbe.initialDelaySeconds }}
+            timeoutSeconds: {{ $.Values.livenessProbe.timeoutSeconds }}
+          readinessProbe:
+{{- if eq $.Values.readinessProbe.probeType "httpGet" }}
+            httpGet:
+              path: {{ $.Values.readinessProbe.path }}
+              scheme: {{ $.Values.readinessProbe.scheme }}
+              port: {{ $.Values.service.internalPort }}
+{{- else if eq $.Values.readinessProbe.probeType "tcpSocket" }}
+            tcpSocket:
+              port: {{ $.Values.service.internalPort }}
+{{- end }}
+            initialDelaySeconds: {{ $.Values.readinessProbe.initialDelaySeconds }}
+            timeoutSeconds: {{ $.Values.readinessProbe.timeoutSeconds }}
+          {{- if $workerConfig.preStopCommand }}
+          lifecycle:
+            preStop:
+              exec:
+                command:
+                {{- range $workerConfig.preStopCommand }}
+                - {{ . }}
+                {{- end }}
+          {{- end }}
+          resources:
+{{ toYaml $.Values.resources | indent 12 }}
+{{- end -}}
+{{- end -}}
diff --git a/assets/auto-deploy-app/test/go.mod b/assets/auto-deploy-app/test/go.mod
new file mode 100644
index 0000000000000000000000000000000000000000..4754f016d43ac382520197869527dc51191fb013
--- /dev/null
+++ b/assets/auto-deploy-app/test/go.mod
@@ -0,0 +1,10 @@
+module gitlab.com/gitlab-org/charts/auto-deploy-app/test
+
+go 1.13
+
+require (
+	github.com/gruntwork-io/terratest v0.23.0
+	github.com/stretchr/testify v1.4.0
+	k8s.io/api v0.0.0-20181110191121-a33c8200050f
+	k8s.io/apimachinery v0.0.0-20190704094520-6f131bee5e2c
+)
diff --git a/assets/auto-deploy-app/test/go.sum b/assets/auto-deploy-app/test/go.sum
new file mode 100644
index 0000000000000000000000000000000000000000..8b5b570079168ea0282e9f6e0a576479efc4fd86
--- /dev/null
+++ b/assets/auto-deploy-app/test/go.sum
@@ -0,0 +1,218 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+github.com/Azure/azure-sdk-for-go v32.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest v0.9.1/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
+github.com/Azure/go-autorest/autorest/adal v0.6.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
+github.com/Azure/go-autorest/autorest/azure/auth v0.3.0/go.mod h1:CI4BQYBct8NS7BXNBBX+RchsFsUu5+oz+OSyR/ZIi7U=
+github.com/Azure/go-autorest/autorest/azure/cli v0.3.0/go.mod h1:rNYMNAefZMRowqCV0cVhr/YDW5dD7afFq9nXAXL4ykE=
+github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
+github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
+github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
+github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA=
+github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI=
+github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
+github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/aws/aws-sdk-go v1.23.8 h1:G/azJoBN0pnhB3B+0eeC4yyVFYIIad6bbzg6wwtImqk=
+github.com/aws/aws-sdk-go v1.23.8/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
+github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
+github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s=
+github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1 h1:yY9rWGoXv1U5pl4gxqlULARMQD7x0QG85lqEXTWysik=
+github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
+github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
+github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 h1:skJKxRtNmevLqnayafdLe2AsenqRupVmzZSqrvb5caU=
+github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
+github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c h1:jWtZjFEUE/Bz0IeIhqCnyZ3HG6KRXSntXe4SjtuTH7c=
+github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk=
+github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/gruntwork-io/gruntwork-cli v0.5.1 h1:mVmVsFubUSLSCO8bGigI63HXzvzkC0uWXzm4dd9pXRg=
+github.com/gruntwork-io/gruntwork-cli v0.5.1/go.mod h1:IBX21bESC1/LGoV7jhXKUnTQTZgQ6dYRsoj/VqxUSZQ=
+github.com/gruntwork-io/terratest v0.23.0 h1:JmGeqO0r5zRLAV55T67NEmPZArz9lN3RKd0moAKhIT4=
+github.com/gruntwork-io/terratest v0.23.0/go.mod h1:+fVff0FQYuRzCF3LKpKF9ac+4w384LDcwLZt7O/KmEE=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
+github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/oracle/oci-go-sdk v7.1.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
+github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok=
+github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
+github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
+github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4 h1:Hynbrlo6LbYI3H1IqXpkVDOcX/3HiPdhVEuyj5a59RM=
+golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.9.1-0.20190821000710-329ecc3c9c34/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/api v0.0.0-20181110191121-a33c8200050f h1:BH667AnNr487/iTtY35X+m6c2S8HL02Rft1PFK93kmw=
+k8s.io/api v0.0.0-20181110191121-a33c8200050f/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
+k8s.io/apimachinery v0.0.0-20190704094520-6f131bee5e2c h1:vdEIiO5B0/3EVwZboF6qyYn5kVDdvCbaGSzr7Rcx18A=
+k8s.io/apimachinery v0.0.0-20190704094520-6f131bee5e2c/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
+k8s.io/client-go v0.0.0-20190704095228-386e588352a4 h1:hqylj4/yit+/eO496/Yhgy2YxxumFpSY94YDFX6lBoU=
+k8s.io/client-go v0.0.0-20190704095228-386e588352a4/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
diff --git a/assets/auto-deploy-app/test/template_test.go b/assets/auto-deploy-app/test/template_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..71ca24ed17bb142e53dc260d508ab698b1691584
--- /dev/null
+++ b/assets/auto-deploy-app/test/template_test.go
@@ -0,0 +1,845 @@
+package main
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/gruntwork-io/terratest/modules/helm"
+	"github.com/gruntwork-io/terratest/modules/k8s"
+	"github.com/gruntwork-io/terratest/modules/random"
+	"github.com/stretchr/testify/require"
+	appsV1 "k8s.io/api/apps/v1"
+	coreV1 "k8s.io/api/core/v1"
+	extensions "k8s.io/api/extensions/v1beta1"
+	netV1 "k8s.io/api/networking/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	chartName     = "auto-deploy-app-1.0.0"
+	helmChartPath = ".."
+)
+
+func TestDeploymentTemplate(t *testing.T) {
+	for _, tc := range []struct {
+		CaseName string
+		Release  string
+		Values   map[string]string
+
+		ExpectedName         string
+		ExpectedRelease      string
+		ExpectedStrategyType extensions.DeploymentStrategyType
+		ExpectedSelector     *metav1.LabelSelector
+	}{
+		{
+			CaseName: "happy",
+			Release:  "production",
+			Values: map[string]string{
+				"releaseOverride": "productionOverridden",
+			},
+			ExpectedName:         "productionOverridden",
+			ExpectedRelease:      "production",
+			ExpectedStrategyType: extensions.DeploymentStrategyType(""),
+		},
+		{
+			CaseName:             "long release name",
+			Release:              strings.Repeat("r", 80),
+			ExpectedName:         strings.Repeat("r", 63),
+			ExpectedRelease:      strings.Repeat("r", 80),
+			ExpectedStrategyType: extensions.DeploymentStrategyType(""),
+		},
+		{
+			CaseName: "strategyType",
+			Release:  "production",
+			Values: map[string]string{
+				"strategyType": "Recreate",
+			},
+			ExpectedName:         "production",
+			ExpectedRelease:      "production",
+			ExpectedStrategyType: extensions.RecreateDeploymentStrategyType,
+		},
+		{
+			CaseName: "enableSelector",
+			Release:  "production",
+			Values: map[string]string{
+				"enableSelector": "true",
+			},
+			ExpectedName:         "production",
+			ExpectedRelease:      "production",
+			ExpectedStrategyType: extensions.DeploymentStrategyType(""),
+			ExpectedSelector: &metav1.LabelSelector{
+				MatchLabels: map[string]string{
+					"app":     "production",
+					"release": "production",
+					"tier":    "web",
+					"track":   "stable",
+				},
+			},
+		},
+	} {
+		t.Run(tc.CaseName, func(t *testing.T) {
+			namespaceName := "minimal-ruby-app-" + strings.ToLower(random.UniqueId())
+
+			values := map[string]string{
+				"gitlab.app": "auto-devops-examples/minimal-ruby-app",
+				"gitlab.env": "prod",
+			}
+
+			mergeStringMap(values, tc.Values)
+
+			options := &helm.Options{
+				SetValues:      values,
+				KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
+			}
+
+			output := helm.RenderTemplate(t, options, helmChartPath, tc.Release, []string{"templates/deployment.yaml"})
+
+			var deployment extensions.Deployment
+			helm.UnmarshalK8SYaml(t, output, &deployment)
+
+			require.Equal(t, tc.ExpectedName, deployment.Name)
+			require.Equal(t, tc.ExpectedStrategyType, deployment.Spec.Strategy.Type)
+
+			require.Equal(t, map[string]string{
+				"app.gitlab.com/app": "auto-devops-examples/minimal-ruby-app",
+				"app.gitlab.com/env": "prod",
+			}, deployment.Annotations)
+			require.Equal(t, map[string]string{
+				"app":      tc.ExpectedName,
+				"chart":    chartName,
+				"heritage": "Tiller",
+				"release":  tc.ExpectedRelease,
+				"tier":     "web",
+				"track":    "stable",
+			}, deployment.Labels)
+
+			require.Equal(t, tc.ExpectedSelector, deployment.Spec.Selector)
+
+			require.Equal(t, map[string]string{
+				"app.gitlab.com/app":           "auto-devops-examples/minimal-ruby-app",
+				"app.gitlab.com/env":           "prod",
+				"checksum/application-secrets": "",
+			}, deployment.Spec.Template.Annotations)
+			require.Equal(t, map[string]string{
+				"app":     tc.ExpectedName,
+				"release": tc.ExpectedRelease,
+				"tier":    "web",
+				"track":   "stable",
+			}, deployment.Spec.Template.Labels)
+		})
+	}
+
+	for _, tc := range []struct {
+		CaseName                string
+		Release                 string
+		Values                  map[string]string
+		ExpectedImageRepository string
+	}{
+		{
+			CaseName: "skaffold",
+			Release:  "production",
+			Values: map[string]string{
+				"image.repository": "skaffold",
+				"image.tag":        "",
+			},
+			ExpectedImageRepository: "skaffold",
+		},
+		{
+			CaseName: "skaffold",
+			Release:  "production",
+			Values: map[string]string{
+				"image.repository": "skaffold",
+				"image.tag":        "stable",
+			},
+			ExpectedImageRepository: "skaffold:stable",
+		},
+	} {
+		t.Run(tc.CaseName, func(t *testing.T) {
+			namespaceName := "minimal-ruby-app-" + strings.ToLower(random.UniqueId())
+
+			values := map[string]string{
+				"gitlab.app": "auto-devops-examples/minimal-ruby-app",
+				"gitlab.env": "prod",
+			}
+
+			mergeStringMap(values, tc.Values)
+
+			options := &helm.Options{
+				SetValues:      values,
+				KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
+			}
+
+			output := helm.RenderTemplate(t, options, helmChartPath, tc.Release, []string{"templates/deployment.yaml"})
+
+			var deployment appsV1.Deployment
+			helm.UnmarshalK8SYaml(t, output, &deployment)
+
+			require.Equal(t, tc.ExpectedImageRepository, deployment.Spec.Template.Spec.Containers[0].Image)
+		})
+	}
+
+	for _, tc := range []struct {
+		CaseName string
+		Release  string
+		Values   map[string]string
+
+		ExpectedName         string
+		ExpectedRelease      string
+		ExpectedStrategyType appsV1.DeploymentStrategyType
+		ExpectedSelector     *metav1.LabelSelector
+	}{
+		{
+			CaseName: "appsv1",
+			Release:  "production",
+			Values: map[string]string{
+				"deploymentApiVersion": "apps/v1",
+			},
+			ExpectedName:         "production",
+			ExpectedRelease:      "production",
+			ExpectedStrategyType: appsV1.DeploymentStrategyType(""),
+			ExpectedSelector: &metav1.LabelSelector{
+				MatchLabels: map[string]string{
+					"app":     "production",
+					"release": "production",
+					"tier":    "web",
+					"track":   "stable",
+				},
+			},
+		},
+	} {
+		t.Run(tc.CaseName, func(t *testing.T) {
+			namespaceName := "minimal-ruby-app-" + strings.ToLower(random.UniqueId())
+
+			values := map[string]string{
+				"gitlab.app": "auto-devops-examples/minimal-ruby-app",
+				"gitlab.env": "prod",
+			}
+
+			mergeStringMap(values, tc.Values)
+
+			options := &helm.Options{
+				SetValues:      values,
+				KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
+			}
+
+			output := helm.RenderTemplate(t, options, helmChartPath, tc.Release, []string{"templates/deployment.yaml"})
+
+			var deployment appsV1.Deployment
+			helm.UnmarshalK8SYaml(t, output, &deployment)
+
+			require.Equal(t, tc.ExpectedName, deployment.Name)
+			require.Equal(t, tc.ExpectedStrategyType, deployment.Spec.Strategy.Type)
+			require.Equal(t, map[string]string{
+				"app.gitlab.com/app": "auto-devops-examples/minimal-ruby-app",
+				"app.gitlab.com/env": "prod",
+			}, deployment.Annotations)
+			require.Equal(t, map[string]string{
+				"app":      tc.ExpectedName,
+				"chart":    chartName,
+				"heritage": "Tiller",
+				"release":  tc.ExpectedRelease,
+				"tier":     "web",
+				"track":    "stable",
+			}, deployment.Labels)
+
+			require.Equal(t, tc.ExpectedSelector, deployment.Spec.Selector)
+
+			require.Equal(t, map[string]string{
+				"app.gitlab.com/app":           "auto-devops-examples/minimal-ruby-app",
+				"app.gitlab.com/env":           "prod",
+				"checksum/application-secrets": "",
+			}, deployment.Spec.Template.Annotations)
+			require.Equal(t, map[string]string{
+				"app":     tc.ExpectedName,
+				"release": tc.ExpectedRelease,
+				"tier":    "web",
+				"track":   "stable",
+			}, deployment.Spec.Template.Labels)
+		})
+	}
+}
+
+func TestWorkerDeploymentTemplate(t *testing.T) {
+	for _, tc := range []struct {
+		CaseName string
+		Release  string
+		Values   map[string]string
+
+		ExpectedName        string
+		ExpectedRelease     string
+		ExpectedDeployments []workerDeploymentTestCase
+	}{
+		{
+			CaseName: "happy",
+			Release:  "production",
+			Values: map[string]string{
+				"releaseOverride":            "productionOverridden",
+				"workers.worker1.command[0]": "echo",
+				"workers.worker1.command[1]": "worker1",
+				"workers.worker2.command[0]": "echo",
+				"workers.worker2.command[1]": "worker2",
+			},
+			ExpectedName:    "productionOverridden",
+			ExpectedRelease: "production",
+			ExpectedDeployments: []workerDeploymentTestCase{
+				{
+					ExpectedName:         "productionOverridden-worker1",
+					ExpectedCmd:          []string{"echo", "worker1"},
+					ExpectedStrategyType: extensions.DeploymentStrategyType(""),
+				},
+				{
+					ExpectedName:         "productionOverridden-worker2",
+					ExpectedCmd:          []string{"echo", "worker2"},
+					ExpectedStrategyType: extensions.DeploymentStrategyType(""),
+				},
+			},
+		},
+		{
+			CaseName: "long release name",
+			Release:  strings.Repeat("r", 80),
+			Values: map[string]string{
+				"workers.worker1.command[0]": "echo",
+				"workers.worker1.command[1]": "worker1",
+			},
+			ExpectedName:    strings.Repeat("r", 63),
+			ExpectedRelease: strings.Repeat("r", 80),
+			ExpectedDeployments: []workerDeploymentTestCase{
+				{
+					ExpectedName:         strings.Repeat("r", 63) + "-worker1",
+					ExpectedCmd:          []string{"echo", "worker1"},
+					ExpectedStrategyType: extensions.DeploymentStrategyType(""),
+				},
+			},
+		},
+		{
+			CaseName: "strategyType",
+			Release:  "production",
+			Values: map[string]string{
+				"workers.worker1.command[0]":   "echo",
+				"workers.worker1.command[1]":   "worker1",
+				"workers.worker1.strategyType": "Recreate",
+			},
+			ExpectedName:    "production",
+			ExpectedRelease: "production",
+			ExpectedDeployments: []workerDeploymentTestCase{
+				{
+					ExpectedName:         "production" + "-worker1",
+					ExpectedCmd:          []string{"echo", "worker1"},
+					ExpectedStrategyType: extensions.RecreateDeploymentStrategyType,
+				},
+			},
+		},
+		{
+			CaseName: "enableSelector",
+			Release:  "production",
+			Values: map[string]string{
+				"enableSelector":             "true",
+				"workers.worker1.command[0]": "echo",
+				"workers.worker1.command[1]": "worker1",
+				"workers.worker2.command[0]": "echo",
+				"workers.worker2.command[1]": "worker2",
+			},
+			ExpectedName:    "production",
+			ExpectedRelease: "production",
+			ExpectedDeployments: []workerDeploymentTestCase{
+				{
+					ExpectedName:         "production-worker1",
+					ExpectedCmd:          []string{"echo", "worker1"},
+					ExpectedStrategyType: extensions.DeploymentStrategyType(""),
+					ExpectedSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"release": "production",
+							"tier":    "worker",
+							"track":   "stable",
+						},
+					},
+				},
+				{
+					ExpectedName:         "production-worker2",
+					ExpectedCmd:          []string{"echo", "worker2"},
+					ExpectedStrategyType: extensions.DeploymentStrategyType(""),
+					ExpectedSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"release": "production",
+							"tier":    "worker",
+							"track":   "stable",
+						},
+					},
+				},
+			},
+		},
+	} {
+		t.Run(tc.CaseName, func(t *testing.T) {
+			namespaceName := "minimal-ruby-app-" + strings.ToLower(random.UniqueId())
+
+			values := map[string]string{
+				"gitlab.app": "auto-devops-examples/minimal-ruby-app",
+				"gitlab.env": "prod",
+			}
+
+			mergeStringMap(values, tc.Values)
+
+			options := &helm.Options{
+				SetValues:      values,
+				KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
+			}
+
+			output := helm.RenderTemplate(t, options, helmChartPath, tc.Release, []string{"templates/worker-deployment.yaml"})
+
+			var deployments deploymentList
+			helm.UnmarshalK8SYaml(t, output, &deployments)
+
+			require.Len(t, deployments.Items, len(tc.ExpectedDeployments))
+			for i, expectedDeployment := range tc.ExpectedDeployments {
+				deployment := deployments.Items[i]
+
+				require.Equal(t, expectedDeployment.ExpectedName, deployment.Name)
+				require.Equal(t, expectedDeployment.ExpectedStrategyType, deployment.Spec.Strategy.Type)
+
+				require.Equal(t, map[string]string{
+					"app.gitlab.com/app": "auto-devops-examples/minimal-ruby-app",
+					"app.gitlab.com/env": "prod",
+				}, deployment.Annotations)
+				require.Equal(t, map[string]string{
+					"chart":    chartName,
+					"heritage": "Tiller",
+					"release":  tc.ExpectedRelease,
+					"tier":     "worker",
+					"track":    "stable",
+				}, deployment.Labels)
+
+				require.Equal(t, expectedDeployment.ExpectedSelector, deployment.Spec.Selector)
+
+				require.Equal(t, map[string]string{
+					"app.gitlab.com/app":           "auto-devops-examples/minimal-ruby-app",
+					"app.gitlab.com/env":           "prod",
+					"checksum/application-secrets": "",
+				}, deployment.Spec.Template.Annotations)
+				require.Equal(t, map[string]string{
+					"release": tc.ExpectedRelease,
+					"tier":    "worker",
+					"track":   "stable",
+				}, deployment.Spec.Template.Labels)
+
+				require.Len(t, deployment.Spec.Template.Spec.Containers, 1)
+				require.Equal(t, expectedDeployment.ExpectedCmd, deployment.Spec.Template.Spec.Containers[0].Command)
+			}
+		})
+	}
+
+	for _, tc := range []struct {
+		CaseName string
+		Release  string
+		Values   map[string]string
+
+		ExpectedName        string
+		ExpectedRelease     string
+		ExpectedDeployments []workerDeploymentAppsV1TestCase
+	}{
+		{
+			CaseName: "appsv1",
+			Release:  "production",
+			Values: map[string]string{
+				"deploymentApiVersion":       "apps/v1",
+				"workers.worker1.command[0]": "echo",
+				"workers.worker1.command[1]": "worker1",
+				"workers.worker2.command[0]": "echo",
+				"workers.worker2.command[1]": "worker2",
+			},
+			ExpectedName:    "production",
+			ExpectedRelease: "production",
+			ExpectedDeployments: []workerDeploymentAppsV1TestCase{
+				{
+					ExpectedName:         "production-worker1",
+					ExpectedCmd:          []string{"echo", "worker1"},
+					ExpectedStrategyType: appsV1.DeploymentStrategyType(""),
+					ExpectedSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"release": "production",
+							"tier":    "worker",
+							"track":   "stable",
+						},
+					},
+				},
+				{
+					ExpectedName:         "production-worker2",
+					ExpectedCmd:          []string{"echo", "worker2"},
+					ExpectedStrategyType: appsV1.DeploymentStrategyType(""),
+					ExpectedSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"release": "production",
+							"tier":    "worker",
+							"track":   "stable",
+						},
+					},
+				},
+			},
+		},
+	} {
+		t.Run(tc.CaseName, func(t *testing.T) {
+			namespaceName := "minimal-ruby-app-" + strings.ToLower(random.UniqueId())
+
+			values := map[string]string{
+				"gitlab.app": "auto-devops-examples/minimal-ruby-app",
+				"gitlab.env": "prod",
+			}
+
+			mergeStringMap(values, tc.Values)
+
+			options := &helm.Options{
+				SetValues:      values,
+				KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
+			}
+
+			output := helm.RenderTemplate(t, options, helmChartPath, tc.Release, []string{"templates/worker-deployment.yaml"})
+
+			var deployments deploymentAppsV1List
+			helm.UnmarshalK8SYaml(t, output, &deployments)
+
+			require.Len(t, deployments.Items, len(tc.ExpectedDeployments))
+			for i, expectedDeployment := range tc.ExpectedDeployments {
+				deployment := deployments.Items[i]
+
+				require.Equal(t, expectedDeployment.ExpectedName, deployment.Name)
+				require.Equal(t, expectedDeployment.ExpectedStrategyType, deployment.Spec.Strategy.Type)
+
+				require.Equal(t, map[string]string{
+					"app.gitlab.com/app": "auto-devops-examples/minimal-ruby-app",
+					"app.gitlab.com/env": "prod",
+				}, deployment.Annotations)
+				require.Equal(t, map[string]string{
+					"chart":    chartName,
+					"heritage": "Tiller",
+					"release":  tc.ExpectedRelease,
+					"tier":     "worker",
+					"track":    "stable",
+				}, deployment.Labels)
+
+				require.Equal(t, expectedDeployment.ExpectedSelector, deployment.Spec.Selector)
+
+				require.Equal(t, map[string]string{
+					"app.gitlab.com/app":           "auto-devops-examples/minimal-ruby-app",
+					"app.gitlab.com/env":           "prod",
+					"checksum/application-secrets": "",
+				}, deployment.Spec.Template.Annotations)
+				require.Equal(t, map[string]string{
+					"release": tc.ExpectedRelease,
+					"tier":    "worker",
+					"track":   "stable",
+				}, deployment.Spec.Template.Labels)
+
+				require.Len(t, deployment.Spec.Template.Spec.Containers, 1)
+				require.Equal(t, expectedDeployment.ExpectedCmd, deployment.Spec.Template.Spec.Containers[0].Command)
+			}
+		})
+	}
+}
+
+func TestNetworkPolicyDeployment(t *testing.T) {
+	releaseName := "network-policy-test"
+	templates := []string{"templates/network-policy.yaml"}
+	expectedLabels := map[string]string{
+		"app":      releaseName,
+		"chart":    chartName,
+		"release":  releaseName,
+		"heritage": "Tiller",
+	}
+
+	tcs := []struct {
+		name       string
+		valueFiles []string
+		values     map[string]string
+
+		meta        metav1.ObjectMeta
+		podSelector metav1.LabelSelector
+		policyTypes []netV1.PolicyType
+		ingress     []netV1.NetworkPolicyIngressRule
+		egress      []netV1.NetworkPolicyEgressRule
+	}{
+		{
+			name: "defaults",
+		},
+		{
+			name:        "with default policy",
+			values:      map[string]string{"networkPolicy.enabled": "true"},
+			meta:        metav1.ObjectMeta{Name: releaseName + "-auto-deploy", Labels: expectedLabels},
+			podSelector: metav1.LabelSelector{MatchLabels: map[string]string{}},
+			ingress: []netV1.NetworkPolicyIngressRule{
+				{
+					From: []netV1.NetworkPolicyPeer{
+						{PodSelector: &metav1.LabelSelector{MatchLabels: map[string]string{}}},
+						{NamespaceSelector: &metav1.LabelSelector{
+							MatchLabels: map[string]string{"app.gitlab.com/managed_by": "gitlab"},
+						}},
+					},
+				},
+			},
+		},
+		{
+			name:        "with custom policy",
+			valueFiles:  []string{"./testdata/custom-policy.yaml"},
+			meta:        metav1.ObjectMeta{Name: releaseName + "-auto-deploy", Labels: expectedLabels},
+			podSelector: metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}},
+			ingress: []netV1.NetworkPolicyIngressRule{
+				{
+					From: []netV1.NetworkPolicyPeer{
+						{PodSelector: &metav1.LabelSelector{MatchLabels: map[string]string{}}},
+						{NamespaceSelector: &metav1.LabelSelector{
+							MatchLabels: map[string]string{"name": "foo"},
+						}},
+					},
+				},
+			},
+		},
+		{
+			name:        "with full spec policy",
+			valueFiles:  []string{"./testdata/full-spec-policy.yaml"},
+			meta:        metav1.ObjectMeta{Name: releaseName + "-auto-deploy", Labels: expectedLabels},
+			podSelector: metav1.LabelSelector{MatchLabels: map[string]string{}},
+			policyTypes: []netV1.PolicyType{"Ingress", "Egress"},
+			ingress: []netV1.NetworkPolicyIngressRule{
+				{
+					From: []netV1.NetworkPolicyPeer{
+						{PodSelector: &metav1.LabelSelector{MatchLabels: map[string]string{}}},
+					},
+				},
+			},
+			egress: []netV1.NetworkPolicyEgressRule{
+				{
+					To: []netV1.NetworkPolicyPeer{
+						{NamespaceSelector: &metav1.LabelSelector{
+							MatchLabels: map[string]string{"name": "gitlab-managed-apps"},
+						}},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			opts := &helm.Options{
+				ValuesFiles: tc.valueFiles,
+				SetValues:   tc.values,
+			}
+			output := helm.RenderTemplate(t, opts, helmChartPath, releaseName, templates)
+
+			policy := new(netV1.NetworkPolicy)
+			helm.UnmarshalK8SYaml(t, output, policy)
+
+			require.Equal(t, tc.meta, policy.ObjectMeta)
+			require.Equal(t, tc.podSelector, policy.Spec.PodSelector)
+			require.Equal(t, tc.policyTypes, policy.Spec.PolicyTypes)
+			require.Equal(t, tc.ingress, policy.Spec.Ingress)
+			require.Equal(t, tc.egress, policy.Spec.Egress)
+		})
+	}
+}
+
+func TestIngressTemplate_ModSecurity(t *testing.T) {
+	templates := []string{"templates/ingress.yaml"}
+	modSecuritySnippet := "SecRuleEngine DetectionOnly\n"
+	modSecuritySnippetWithSecRules := modSecuritySnippet + `SecRule REQUEST_HEADERS:User-Agent \"scanner\" \"log,deny,id:107,status:403,msg:\'Scanner Identified\'\"
+SecRule REQUEST_HEADERS:Content-Type \"text/plain\" \"log,deny,id:\'20010\',status:403,msg:\'Text plain not allowed\'\"
+`
+	defaultAnnotations := map[string]string{
+		"kubernetes.io/ingress.class": "nginx",
+		"kubernetes.io/tls-acme":      "true",
+	}
+	defaultModSecurityAnnotations := map[string]string{
+		"nginx.ingress.kubernetes.io/modsecurity-transaction-id": "$server_name-$request_id",
+	}
+	modSecurityAnnotations := make(map[string]string)
+	secRulesAnnotations := make(map[string]string)
+	mergeStringMap(modSecurityAnnotations, defaultAnnotations)
+	mergeStringMap(modSecurityAnnotations, defaultModSecurityAnnotations)
+	mergeStringMap(secRulesAnnotations, defaultAnnotations)
+	mergeStringMap(secRulesAnnotations, defaultModSecurityAnnotations)
+	modSecurityAnnotations["nginx.ingress.kubernetes.io/modsecurity-snippet"] = modSecuritySnippet
+	secRulesAnnotations["nginx.ingress.kubernetes.io/modsecurity-snippet"] = modSecuritySnippetWithSecRules
+
+	tcs := []struct {
+		name       string
+		valueFiles []string
+		values     map[string]string
+		meta       metav1.ObjectMeta
+	}{
+		{
+			name: "defaults",
+			meta: metav1.ObjectMeta{Annotations: defaultAnnotations},
+		},
+		{
+			name:   "with modSecurity enabled without custom secRules",
+			values: map[string]string{"ingress.modSecurity.enabled": "true"},
+			meta:   metav1.ObjectMeta{Annotations: modSecurityAnnotations},
+		},
+		{
+			name:       "with custom secRules",
+			valueFiles: []string{"./testdata/modsecurity-ingress.yaml"},
+			meta:       metav1.ObjectMeta{Annotations: secRulesAnnotations},
+		},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			opts := &helm.Options{
+				ValuesFiles: tc.valueFiles,
+				SetValues:   tc.values,
+			}
+			output := helm.RenderTemplate(t, opts, helmChartPath, "", templates)
+
+			ingress := new(extensions.Ingress)
+			helm.UnmarshalK8SYaml(t, output, ingress)
+
+			require.Equal(t, tc.meta.Annotations, ingress.ObjectMeta.Annotations)
+		})
+	}
+}
+
+func TestIngressTemplate_Disable(t *testing.T) {
+	templates := []string{"templates/ingress.yaml"}
+	releaseName := "ingress-disable-test"
+	tcs := []struct {
+		name   string
+		values map[string]string
+
+		expectedrelease string
+	}{
+		{
+			name:            "defaults",
+			expectedrelease: releaseName + "-auto-deploy",
+		},
+		{
+			name:            "with ingress.enabled key undefined, but service is enabled",
+			values:          map[string]string{"ingress.enabled": "null", "service.enabled": "true"},
+			expectedrelease: releaseName + "-auto-deploy",
+		},
+		{
+			name:            "with service enabled and track non-stable",
+			values:          map[string]string{"service.enabled": "true", "application.track": "non-stable"},
+			expectedrelease: "",
+		},
+		{
+			name:            "with service disabled and track stable",
+			values:          map[string]string{"service.enabled": "false", "application.track": "stable"},
+			expectedrelease: "",
+		},
+		{
+			name:            "with service disabled and track non-stable",
+			values:          map[string]string{"service.enabled": "false", "application.track": "non-stable"},
+			expectedrelease: "",
+		},
+		{
+			name:            "with ingress disabled",
+			values:          map[string]string{"ingress.enabled": "false"},
+			expectedrelease: "",
+		},
+		{
+			name:            "with ingress enabled and track non-stable",
+			values:          map[string]string{"ingress.enabled": "true", "application.track": "non-stable"},
+			expectedrelease: "",
+		},
+		{
+			name:            "with ingress enabled and service disabled",
+			values:          map[string]string{"ingress.enabled": "true", "service.enabled": "false"},
+			expectedrelease: "",
+		},
+		{
+			name:            "with ingress disabled and service enabled and track stable",
+			values:          map[string]string{"ingress.enabled": "false", "service.enabled": "true", "application.track": "stable"},
+			expectedrelease: "",
+		},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			opts := &helm.Options{
+				SetValues: tc.values,
+			}
+			output := helm.RenderTemplate(t, opts, helmChartPath, releaseName, templates)
+
+			ingress := new(extensions.Ingress)
+
+			helm.UnmarshalK8SYaml(t, output, ingress)
+			require.Equal(t, tc.expectedrelease, ingress.ObjectMeta.Name)
+		})
+	}
+}
+
+func TestServiceTemplate_Disable(t *testing.T) {
+	templates := []string{"templates/service.yaml"}
+	releaseName := "service-disable-test"
+	tcs := []struct {
+		name   string
+		values map[string]string
+
+		expectedrelease string
+	}{
+		{
+			name:            "defaults",
+			expectedrelease: releaseName + "-auto-deploy",
+		},
+		{
+			name:            "with service enabled and track non-stable",
+			values:          map[string]string{"service.enabled": "true", "application.track": "non-stable"},
+			expectedrelease: "",
+		},
+		{
+			name:            "with service disabled and track stable",
+			values:          map[string]string{"service.enabled": "false", "application.track": "stable"},
+			expectedrelease: "",
+		},
+		{
+			name:            "with service disabled and track non-stable",
+			values:          map[string]string{"service.enabled": "false", "application.track": "non-stable"},
+			expectedrelease: "",
+		},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			opts := &helm.Options{
+				SetValues: tc.values,
+			}
+			output := helm.RenderTemplate(t, opts, helmChartPath, releaseName, templates)
+
+			service := new(coreV1.Service)
+
+			helm.UnmarshalK8SYaml(t, output, service)
+
+			require.Equal(t, tc.expectedrelease, service.ObjectMeta.Name)
+		})
+	}
+}
+
+type workerDeploymentTestCase struct {
+	ExpectedName         string
+	ExpectedCmd          []string
+	ExpectedStrategyType extensions.DeploymentStrategyType
+	ExpectedSelector     *metav1.LabelSelector
+}
+
+type workerDeploymentAppsV1TestCase struct {
+	ExpectedName         string
+	ExpectedCmd          []string
+	ExpectedStrategyType appsV1.DeploymentStrategyType
+	ExpectedSelector     *metav1.LabelSelector
+}
+
+type deploymentList struct {
+	metav1.TypeMeta `json:",inline"`
+
+	Items []extensions.Deployment `json:"items" protobuf:"bytes,2,rep,name=items"`
+}
+
+type deploymentAppsV1List struct {
+	metav1.TypeMeta `json:",inline"`
+
+	Items []appsV1.Deployment `json:"items" protobuf:"bytes,2,rep,name=items"`
+}
+
+func mergeStringMap(dst, src map[string]string) {
+	for k, v := range src {
+		dst[k] = v
+	}
+}
diff --git a/assets/auto-deploy-app/test/testdata/custom-policy.yaml b/assets/auto-deploy-app/test/testdata/custom-policy.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8cc487aa725b2bb91a704caf5e39c3a3882be055
--- /dev/null
+++ b/assets/auto-deploy-app/test/testdata/custom-policy.yaml
@@ -0,0 +1,13 @@
+networkPolicy:
+  enabled: true
+  spec:
+    podSelector:
+      matchLabels:
+        foo: bar
+    ingress:
+    - from:
+      - podSelector:
+          matchLabels: {}
+      - namespaceSelector:
+          matchLabels:
+            name: foo
diff --git a/assets/auto-deploy-app/test/testdata/full-spec-policy.yaml b/assets/auto-deploy-app/test/testdata/full-spec-policy.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..25254b5d2c51802230fd383f206e1848a9517879
--- /dev/null
+++ b/assets/auto-deploy-app/test/testdata/full-spec-policy.yaml
@@ -0,0 +1,17 @@
+networkPolicy:
+  enabled: true
+  spec:
+    podSelector:
+      matchLabels: {}
+    policyTypes:
+    - Ingress
+    - Egress
+    ingress:
+    - from:
+      - podSelector:
+          matchLabels: {}
+    egress:
+    - to:
+      - namespaceSelector:
+          matchLabels:
+            name: gitlab-managed-apps
diff --git a/assets/auto-deploy-app/test/testdata/modsecurity-ingress.yaml b/assets/auto-deploy-app/test/testdata/modsecurity-ingress.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5b8b469c1ece14cd1fdbdda8b503b5a38026141f
--- /dev/null
+++ b/assets/auto-deploy-app/test/testdata/modsecurity-ingress.yaml
@@ -0,0 +1,10 @@
+ingress:
+  modSecurity:
+    enabled: true
+    secRules:
+      - variable: "REQUEST_HEADERS:User-Agent"
+        operator: "scanner"
+        action: "log,deny,id:107,status:403,msg:'Scanner Identified'"
+      - variable: "REQUEST_HEADERS:Content-Type"
+        operator: "text/plain"
+        action: "log,deny,id:'20010',status:403,msg:'Text plain not allowed'"
\ No newline at end of file
diff --git a/assets/auto-deploy-app/values.yaml b/assets/auto-deploy-app/values.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b7b44df77f6c48c420767fd2cd446cfe6aea0e44
--- /dev/null
+++ b/assets/auto-deploy-app/values.yaml
@@ -0,0 +1,123 @@
+# Default values for chart.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+replicaCount: 1
+strategyType:
+enableSelector:
+deploymentApiVersion: extensions/v1beta1
+image:
+  repository: gitlab.example.com/group/project
+  tag: stable
+  pullPolicy: IfNotPresent
+  secrets:
+    - name: gitlab-registry
+podAnnotations: {}
+application:
+  track: stable
+  tier: web
+  migrateCommand:
+  initializeCommand:
+  secretName:
+  secretChecksum:
+hpa:
+  enabled: false
+  minReplicas: 1
+  maxReplicas: 5
+  targetCPUUtilizationPercentage: 80
+gitlab:
+  app:
+  env:
+  envName:
+  envURL:
+service:
+  enabled: true
+  annotations: {}
+  name: web
+  type: ClusterIP
+  url: http://my.host.com/
+  additionalHosts:
+  commonName:
+  externalPort: 5000
+  internalPort: 5000
+ingress:
+  enabled: true
+  tls:
+    enabled: true
+    secretName: ""
+  annotations:
+    kubernetes.io/tls-acme: "true"
+    kubernetes.io/ingress.class: "nginx"
+  modSecurity:
+    enabled: false
+    secRuleEngine: "DetectionOnly"
+    # secRules:
+    #   - variable: ""
+    #     operator: ""
+    #     action: ""
+prometheus:
+  metrics: false
+livenessProbe:
+  path: "/"
+  initialDelaySeconds: 15
+  timeoutSeconds: 15
+  scheme: "HTTP"
+  probeType: "httpGet"
+readinessProbe:
+  path: "/"
+  initialDelaySeconds: 5
+  timeoutSeconds: 3
+  scheme: "HTTP"
+  probeType: "httpGet"
+postgresql:
+  enabled: true
+  managed: false
+  managedClassSelector:
+    #   matchLabels:
+    #     stack: gitlab (This is an example. The labels should match the labels on the CloudSQLInstanceClass)
+
+resources:
+#  limits:
+#    cpu: 100m
+#    memory: 128Mi
+  requests:
+#    cpu: 100m
+#    memory: 128Mi
+
+## Configure PodDisruptionBudget
+## ref: https://kubernetes.io/docs/concepts/workloads/pods/disruptions/
+#
+podDisruptionBudget:
+  enabled: false
+  # minAvailable: 1
+  maxUnavailable: 1
+
+## Configure NetworkPolicy
+## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/
+#
+networkPolicy:
+  enabled: false
+  spec:
+    podSelector:
+      matchLabels: {}
+    ingress:
+    - from:
+      - podSelector:
+          matchLabels: {}
+      - namespaceSelector:
+          matchLabels:
+            app.gitlab.com/managed_by: gitlab
+
+workers: {}
+  # worker:
+  #   replicaCount: 1
+  #   terminationGracePeriodSeconds: 60
+  #   command:
+  #   - /bin/herokuish
+  #   - procfile
+  #   - start
+  #   - worker
+  #   preStopCommand:
+  #   - /bin/herokuish
+  #   - procfile
+  #   - start
+  #   - stop_worker
diff --git a/src/bin/auto-deploy b/src/bin/auto-deploy
index b347c2c82dff52106ce27748d703f71880f438fb..3d5307079c0e8338f229509570b8e9d225360366 100755
--- a/src/bin/auto-deploy
+++ b/src/bin/auto-deploy
@@ -13,6 +13,9 @@ if [[ "$AUTO_DEVOPS_POSTGRES_CHANNEL" == "2" ]]; then
 elif [[ "$AUTO_DEVOPS_POSTGRES_CHANNEL" == "1" ]]; then
   export POSTGRES_VERSION="${POSTGRES_VERSION:-"9.6.2"}"
 fi
+export BIN_DIR="/build/bin"
+export ASSETS_DIR='/assets'
+export ASSETS_CHART_DIR="${ASSETS_DIR}/auto-deploy-app"
 
 function check_kube_domain() {
   if [[ -z "$KUBE_INGRESS_BASE_DOMAIN" ]]; then
@@ -28,28 +31,31 @@ function check_kube_domain() {
 }
 
 function download_chart() {
-  local auto_chart
-  local auto_chart_name
-  if [[ ! -d chart ]]; then
+  helm init --client-only
+
+  if [[ -d chart ]]; then
+    echo "Download is skipped. The bundled chart in user's repository will be used."
+  elif [[ -n "${AUTO_DEVOPS_CHART}${AUTO_DEVOPS_CHART_REPOSITORY_NAME}${AUTO_DEVOPS_CHART_REPOSITORY}" ]]; then
+    echo "Downloading the chart from the chart repository..."
+    local auto_chart
+    local auto_chart_name
+
     auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app}
     # shellcheck disable=SC2086 # double quote variables to prevent globbing
     auto_chart_name=$(basename $auto_chart)
     auto_chart_name=${auto_chart_name%.tgz}
     auto_chart_name=${auto_chart_name%.tar.gz}
-  else
-    auto_chart="chart"
-    auto_chart_name="chart"
-  fi
 
-  helm init --client-only
-  # shellcheck disable=SC2086 # double quote variables to prevent globbing
-  # shellcheck disable=SC2140 # ambiguous quoting warning
-  helm repo add ${AUTO_DEVOPS_CHART_REPOSITORY_NAME:-gitlab} ${AUTO_DEVOPS_CHART_REPOSITORY:-https://charts.gitlab.io} ${AUTO_DEVOPS_CHART_REPOSITORY_USERNAME:+"--username" "$AUTO_DEVOPS_CHART_REPOSITORY_USERNAME"} ${AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD:+"--password" "$AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD"}
-  if [[ ! -d "$auto_chart" ]]; then
-    helm fetch ${auto_chart} --untar
-  fi
-  if [ "$auto_chart_name" != "chart" ]; then
-    mv ${auto_chart_name} chart
+    # shellcheck disable=SC2086 # double quote variables to prevent globbing
+    # shellcheck disable=SC2140 # ambiguous quoting warning
+    helm repo add ${AUTO_DEVOPS_CHART_REPOSITORY_NAME:-gitlab} ${AUTO_DEVOPS_CHART_REPOSITORY:-https://charts.gitlab.io} ${AUTO_DEVOPS_CHART_REPOSITORY_USERNAME:+"--username" "$AUTO_DEVOPS_CHART_REPOSITORY_USERNAME"} ${AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD:+"--password" "$AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD"}
+    helm fetch "${auto_chart}" --untar
+    if [ "$auto_chart_name" != "chart" ]; then
+      mv "${auto_chart_name}" chart
+    fi
+  else
+    echo "Download is skipped. The bundled chart in auto-deploy-image will be used."
+    cp -R $ASSETS_CHART_DIR chart
   fi
 
   helm dependency update chart/
@@ -203,6 +209,8 @@ channel 1 database.'
     old_postgres_enabled="$POSTGRES_ENABLED"
   fi
 
+  ${BIN_DIR}/validate-chart-version "$(helm list --output json)" "chart" "$name"
+
   local database_url
   database_url=$(auto_database_url)
 
@@ -435,7 +443,7 @@ function create_application_secret() {
 
   k8s_secrets_file=$(mktemp)
 
-  /build/bin/auto-deploy-application-secrets-yaml "$k8s_secrets_file"
+  ${BIN_DIR}/auto-deploy-application-secrets-yaml "$k8s_secrets_file"
 
   kubectl replace -f "$k8s_secrets_file" -n "$KUBE_NAMESPACE" --force
 
@@ -507,7 +515,6 @@ function get_replicas() {
     echo 1
   fi
 }
-
 ##
 ## End Helper functions
 
diff --git a/src/bin/helpers/gitlab/chart.rb b/src/bin/helpers/gitlab/chart.rb
new file mode 100644
index 0000000000000000000000000000000000000000..47d2b1a8ceaf42fbcf31f106b07f9a2650e1e93e
--- /dev/null
+++ b/src/bin/helpers/gitlab/chart.rb
@@ -0,0 +1,71 @@
+require 'yaml'
+require 'json'
+
+module Gitlab
+  # This class represents auto-deploy-app chart managed by GitLab.
+  class Chart
+    attr_reader :major, :minor, :patch
+
+    GITLAB_MANAGED_CHART_NAME = 'auto-deploy-app'.freeze
+
+    class << self
+      # Load a chart from `helm list` output
+      #
+      # @param [String] data                    JSON formatted `helm list` output
+      # @param [String] release_name            The release created by a chart in question
+      def load_from_helm_ls(data, release_name)
+        # In Helm 2, `helm ls --output json` returns an empty string when there are no releases
+        return if data.empty?
+
+        release = JSON.parse(data)['Releases'].find { |r| r['Name'] == release_name }
+
+        return if release.nil?
+
+        name, major, minor, patch = release['Chart'].scan(/\A(.+)-(\d+)\.(\d+)\.(\d+)/).first
+
+        return unless gitlab_managed_chart?(name, major, minor, patch)
+
+        self.new(major, minor, patch)
+      end
+
+      # Load a chart from Chart.yaml
+      #
+      # @param [String] chart_dir                The path to the chart directory
+      def load_from_chart_yml(chart_dir)
+        chart_hash = YAML.load_file(File.join(chart_dir, 'Chart.yaml'))
+
+        name = chart_hash['name']
+        major, minor, patch = chart_hash['version'].scan(/\A(\d+)\.(\d+)\.(\d)+/).first
+
+        return unless gitlab_managed_chart?(name, major, minor, patch)
+
+        self.new(major, minor, patch)
+      end
+
+      def gitlab_managed_chart?(name, major, minor, patch)
+        name == GITLAB_MANAGED_CHART_NAME && major && minor && patch
+      end
+    end
+
+    def initialize(major, minor, patch)
+      @major = major.to_i
+      @minor = minor.to_i
+      @patch = patch.to_i
+    end
+
+    def to_s
+      "v#{major}.#{minor}.#{patch}"
+    end
+
+    def compatible?(previous_chart)
+      # v0 and v1 charts are compatible
+      return true if major == 1 && previous_chart.major == 0
+
+      major == previous_chart.major
+    end
+
+    def allowed_to_force_deploy?
+      ENV["AUTO_DEVOPS_FORCE_DEPLOY_V#{major}"]
+    end
+  end
+end
diff --git a/src/bin/validate-chart-version b/src/bin/validate-chart-version
new file mode 100755
index 0000000000000000000000000000000000000000..e4a2638df63086b7d330253422f8432c8c04e9b5
--- /dev/null
+++ b/src/bin/validate-chart-version
@@ -0,0 +1,43 @@
+#!/usr/bin/ruby
+
+require_relative 'helpers/gitlab/chart'
+
+helm_release_list_json = ARGV[0]
+current_chart_dir = ARGV[1]
+release_name = ARGV[2]
+
+puts "Validating chart version..."
+
+print "Fetching the previously deployed chart version..."
+previous_chart = Gitlab::Chart.load_from_helm_ls(helm_release_list_json, release_name)
+puts " #{previous_chart}"
+
+print "Fetching the deploying chart version..."
+current_chart = Gitlab::Chart.load_from_chart_yml(current_chart_dir)
+puts " #{current_chart}"
+
+return unless previous_chart && current_chart
+
+if current_chart.compatible?(previous_chart)
+  puts "The current chart is compatible with the previously deployed chart"
+elsif current_chart.allowed_to_force_deploy?
+  puts "The current chart is not compatible with the previously deployed chart, however, allowed to force deploy."
+else
+  puts <<~EOS
+    *************************************************************************************
+                                       [WARNING]                                         
+    Detected a major version difference between the the chart that is currently deploying (#{current_chart.to_s}), and the previously deployed chart (#{previous_chart.to_s}).
+    A new major version might not be backward compatible with the current release (#{release_name}). The deployment could fail or be stuck in an unrecoverable status.
+    Please follow the appropriate instructions:
+
+    - To proceed with the new chart version, follow the manual upgrade guide
+      https://docs.gitlab.com/ee/topics/autodevops/upgrading_chart.html#upgrade-guide and redeploy.
+
+    - To continue using the previously deployed chart, see the following instructions:
+      https://docs.gitlab.com/ee/topics/autodevops/upgrading_chart.html#keep-using-a-specific-version-of-chart and redeploy.
+
+      For more information, please read https://docs.gitlab.com/ee/topics/autodevops/upgrading_chart.html.
+    *************************************************************************************
+  EOS
+  exit 1
+end
diff --git a/test/rspec/helpers/gitlab/chart_spec.rb b/test/rspec/helpers/gitlab/chart_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0f82ce443f434358a4b76cc3233e67e885c16874
--- /dev/null
+++ b/test/rspec/helpers/gitlab/chart_spec.rb
@@ -0,0 +1,158 @@
+require_relative '../../../../src/bin/helpers/gitlab/chart.rb'
+require 'tmpdir'
+
+describe Gitlab::Chart do
+  describe '.load_from_helm_ls' do
+    subject { described_class.load_from_helm_ls(data, release_name) }
+
+    let(:release_name) { 'production' }
+
+    let(:data) do
+      <<~EOS
+      {
+        "Next": "",
+        "Releases": [
+          {
+            "Name": "production",
+            "Revision": 1,
+            "Updated": "Wed Jul  1 08:07:07 2020",
+            "Status": "DEPLOYED",
+            "Chart": "auto-deploy-app-1.2.3",
+            "AppVersion": "",
+            "Namespace": "new-sentimentality-19561312-production"
+          },
+          {
+            "Name": "production-canary",
+            "Revision": 2,
+            "Updated": "Wed Jul  1 11:45:16 2020",
+            "Status": "DEPLOYED",
+            "Chart": "auto-deploy-app-4.5.6",
+            "AppVersion": "",
+            "Namespace": "new-sentimentality-19561312-production"
+          },
+          {
+            "Name": "production-postgresql",
+            "Revision": 9,
+            "Updated": "Mon Jul 13 11:37:20 2020",
+            "Status": "DEPLOYED",
+            "Chart": "postgresql-8.2.1",
+            "AppVersion": "11.6.0",
+            "Namespace": "new-sentimentality-19561312-production"
+          }
+        ]
+      }      
+      EOS
+    end
+
+    it 'correctly loads the chart' do
+      expect(subject.major).to eq(1)
+      expect(subject.minor).to eq(2)
+      expect(subject.patch).to eq(3)
+    end
+
+    context 'when release name is canary' do
+      let(:release_name) { 'production-canary' }
+
+      it 'correctly loads the chart' do
+        expect(subject.major).to eq(4)
+        expect(subject.minor).to eq(5)
+        expect(subject.patch).to eq(6)
+      end
+    end
+
+    context 'when release name does not exist' do
+      let(:release_name) { 'production-unknown' }
+
+      it 'returns nil' do
+        expect(subject).to be_nil
+      end
+    end
+
+    context 'when chart is not gitlab managed chart' do
+      let(:release_name) { 'production-postgresql' }
+
+      it 'returns nil' do
+        expect(subject).to be_nil
+      end
+    end
+
+    context 'when data is empty' do
+      let(:data) { '' }
+
+      it 'returns nil' do
+        expect(subject).to be_nil
+      end
+    end
+
+    context 'when data is nil' do
+      let(:data) { nil }
+
+      it 'raises an error' do
+        expect { subject }.to raise_error(NoMethodError)
+      end
+    end
+
+    context 'when data is not formatted in json' do
+      let(:data) { 'test' }
+
+      it 'raises an error' do
+        expect { subject }.to raise_error(JSON::ParserError)
+      end
+    end
+  end
+
+  describe '.load_from_chart_yml' do
+    let(:chart_yaml) do
+      <<~EOS
+      apiVersion: v1
+      description: GitLab's Auto-deploy Helm Chart
+      name: auto-deploy-app
+      version: 1.0.0-beta.0
+      icon: https://gitlab.com/gitlab-com/gitlab-artwork/raw/master/logo/logo-square.png    
+      EOS
+    end
+
+    it 'correctly loads the chart' do
+      in_chart_dir do |dir|
+        chart = described_class.load_from_chart_yml(dir)
+
+        expect(chart.major).to eq(1)
+        expect(chart.minor).to eq(0)
+        expect(chart.patch).to eq(0)
+      end
+    end
+
+    context 'when chart is not gitlab managed chart' do
+      let(:chart_yaml) do
+        <<~EOS
+        apiVersion: v1
+        description: GitLab's Auto-deploy Helm Chart
+        name: custom-chart
+        version: 1.0.0-beta.0
+        icon: https://gitlab.com/gitlab-com/gitlab-artwork/raw/master/logo/logo-square.png    
+        EOS
+      end
+
+      it 'returns nil' do
+        in_chart_dir do |dir|
+          chart = described_class.load_from_chart_yml(dir)
+  
+          expect(chart).to be_nil
+        end
+      end
+    end
+
+    context 'when chart yaml is not found' do
+      it 'raises an error' do
+        expect { described_class.load_from_chart_yml('test') }.to raise_error(Errno::ENOENT)
+      end
+    end
+
+    def in_chart_dir
+      Dir.mktmpdir do |dir|
+        File.write("#{dir}/Chart.yaml", chart_yaml)
+        yield dir
+      end
+    end
+  end
+end
diff --git a/test/verify-chart-version b/test/verify-chart-version
new file mode 100755
index 0000000000000000000000000000000000000000..8f0b259474473b7cf50edd04b5f665f6ec3aa980
--- /dev/null
+++ b/test/verify-chart-version
@@ -0,0 +1,12 @@
+#!/usr/bin/ruby
+
+require_relative '../src/bin/helpers/gitlab/chart'
+
+expected_major_version = ARGV[0].to_i
+
+chart = Gitlab::Chart.load_from_chart_yml('chart')
+
+unless expected_major_version == chart.major
+  raise "The chart version is different from the expected version. " \
+        "expected: #{expected_major_version} got: #{chart.major}"
+end