Repository: GoogleContainerTools/skaffold
Pull request #8096 (2022-11-15 00:38:48)

feat: implement cleanup for Cloud Run Jobs
**Description**
Implements Cleanup for Cloud Run Jobs, as well as support cleaning up multiple resources.

This is still fire-and-forget deleting so if Cloud Run encounters any errors asynchronously during the deletion, this won't catch them. Also, if multiple errors occur, it only returns the first one.

**User facing changes (remove if N/A)**
Support cleanup for Cloud Run Jobs.

**Follow-up Work (remove if N/A)**
Improve error handling, add integration test.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 124d4b842..18f95b522 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,29 +1,4 @@
-# v1.39.4 Release - 11/11/2022
-**Linux amd64**
-`curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v1.39.4/skaffold-linux-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
-
-**Linux arm64**
-`curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v1.39.4/skaffold-linux-arm64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
-
-**macOS amd64**
-`curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v1.39.4/skaffold-darwin-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
-
-**macOS arm64**
-`curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v1.39.4/skaffold-darwin-arm64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
-
-**Windows**
-https://storage.googleapis.com/skaffold/releases/v1.39.4/skaffold-windows-amd64.exe
-
-**Docker image**
-`gcr.io/k8s-skaffold/skaffold:v1.39.4`
-
-Fixes:
-* fix: resolve issue where skaffold apply didn't respect config - statusCheck:false (#8077)
-
-Updates and Refactors:
-* chore: update skaffold deps image dependencies (#8067)
-
-# v2.0.2 Release - 11/09/2022
+# v2.0.2 Release - 11/15/2022
 **Linux amd64**
 `curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v2.0.2/skaffold-linux-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
 
@@ -52,6 +27,7 @@ New Features and Additions:
 * feat: context copy retry. If we add a for loop and execute the kubectlcli.Run method for copying context to kaniko pod , this makes more reliable and less prune to network failures [#7887](https://github.com/GoogleContainerTools/skaffold/pull/7887)
 * feat: new cloudbuild slim [#8004](https://github.com/GoogleContainerTools/skaffold/pull/8004)
 * feat: new dockerfiles and cloudbuild config for slim image [#7996](https://github.com/GoogleContainerTools/skaffold/pull/7996)
+* feat: completion for the Fish shell [#8097](https://github.com/GoogleContainerTools/skaffold/pull/8097)
 
 Fixes:
 * fix: add description to StatusCheck TaskEvent [#8017](https://github.com/GoogleContainerTools/skaffold/pull/8017)
@@ -64,6 +40,10 @@ Fixes:
 * fix: properly add RemoteManifests support to skaffold v2 [#8036](https://github.com/GoogleContainerTools/skaffold/pull/8036)
 * fix: use std lib signal handling [#8046](https://github.com/GoogleContainerTools/skaffold/pull/8046)
 * fix(sec): upgrade runc version v1.0.2 -> v1.1.2 [#8050](https://github.com/GoogleContainerTools/skaffold/pull/8050)
+* fix: add unique tag to test image to avoid collisions with other tests [#8087](https://github.com/GoogleContainerTools/skaffold/pull/8087)
+* fix: correct issues with current upgrade logic for artifactOverrides with helm imageStrategy [#8066](https://github.com/GoogleContainerTools/skaffold/pull/8066)
+* fix: override protocols argument pass to helm post-renderer [#8083](https://github.com/GoogleContainerTools/skaffold/pull/8083)
+* fix: resolve issue where skaffold filter command did not properly configure the filter allow & deny lists [#8085](https://github.com/GoogleContainerTools/skaffold/pull/8085)
 
 Updates and Refactors:
 * chore: add v1.39.3 release CHANGELOG.md entry [#7991](https://github.com/GoogleContainerTools/skaffold/pull/7991)
@@ -74,6 +54,9 @@ Updates and Refactors:
 * chore: upgrade jib plugin versions to 3.3.1 [#8003](https://github.com/GoogleContainerTools/skaffold/pull/8003)
 * chore(deps): bump some .github/workflows deps [#8051](https://github.com/GoogleContainerTools/skaffold/pull/8051)
 * chore(doc): note that filesync works for debug [#8044](https://github.com/GoogleContainerTools/skaffold/pull/8044)
+* chore: reduce gcp integration test time [#8080](https://github.com/GoogleContainerTools/skaffold/pull/8080)
+* chore: remove uncessary server bins [#8092](https://github.com/GoogleContainerTools/skaffold/pull/8092)
+* refactor: use exclude directives instead of replace directives for pinning [#8056](https://github.com/GoogleContainerTools/skaffold/pull/8056)
 
 Docs, Test, and Release Updates:
 * docs: Linked to the Google Cloud Solutions Template [#8054](https://github.com/GoogleContainerTools/skaffold/pull/8054)
@@ -81,13 +64,16 @@ Docs, Test, and Release Updates:
 * docs: update skaffold examples and documentation to properly reflect v1 ->v2 artifactOverrides changes - 2nd attempt [#8019](https://github.com/GoogleContainerTools/skaffold/pull/8019)
 * docs: update skaffold examples and documentation to properly reflect v1 ->v2 artifactOverrides changes [#8013](https://github.com/GoogleContainerTools/skaffold/pull/8013)
 * docs: update skaffold.gliffy diagram with updated information for skaffold v2 [#8035](https://github.com/GoogleContainerTools/skaffold/pull/8035)
+* docs: add detailed information on how to use helm rendering with v2.X.Y as well as how post-renderer usage works [#8093](https://github.com/GoogleContainerTools/skaffold/pull/8093)
 
 Huge thanks goes out to all of our contributors for this release:
 
 - Aaron Prindle
 - agarwalnit
+- b4nks
 - Benjamin Kaplan
 - Brian de Alwis
+- Bryan C. Mills
 - dependabot[bot]
 - Emily Wang
 - ericzzzzzzz
@@ -101,10 +87,36 @@ Huge thanks goes out to all of our contributors for this release:
 - Renzo Rojas
 - Santiago Nuñez-Cacho
 - Sergei Kononov
+- Sergei Morozov
 - Steven Powell
 - techchickk
 - Tomás Mota
 
+# v1.39.4 Release - 11/11/2022
+**Linux amd64**
+`curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v1.39.4/skaffold-linux-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
+
+**Linux arm64**
+`curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v1.39.4/skaffold-linux-arm64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
+
+**macOS amd64**
+`curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v1.39.4/skaffold-darwin-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
+
+**macOS arm64**
+`curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v1.39.4/skaffold-darwin-arm64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
+
+**Windows**
+https://storage.googleapis.com/skaffold/releases/v1.39.4/skaffold-windows-amd64.exe
+
+**Docker image**
+`gcr.io/k8s-skaffold/skaffold:v1.39.4`
+
+Fixes:
+* fix: resolve issue where skaffold apply didn't respect config - statusCheck:false (#8077)
+
+Updates and Refactors:
+* chore: update skaffold deps image dependencies (#8067)
+
 # v2.0.1 Release - 10/27/2022
 **Linux amd64**
 `curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v2.0.1/skaffold-linux-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
diff --git a/cmd/skaffold/app/cmd/completion.go b/cmd/skaffold/app/cmd/completion.go
index 95d37cff6..0cf46232d 100644
--- a/cmd/skaffold/app/cmd/completion.go
+++ b/cmd/skaffold/app/cmd/completion.go
@@ -27,19 +27,21 @@ import (
 
 const (
 	longDescription = `
-	Outputs shell completion for the given shell (bash or zsh)
+	Outputs shell completion for the given shell (bash, fish or zsh)
 
 	This depends on the bash-completion binary.  Example installation instructions:
 	OS X:
 		$ brew install bash-completion
 		$ source $(brew --prefix)/etc/bash_completion
 		$ skaffold completion bash > ~/.skaffold-completion  # for bash users
+		$ skaffold completion fish > ~/.skaffold-completion  # for fish users
 		$ skaffold completion zsh > ~/.skaffold-completion   # for zsh users
 		$ source ~/.skaffold-completion
 	Ubuntu:
 		$ apt-get install bash-completion
 		$ source /etc/bash-completion
 		$ source <(skaffold completion bash) # for bash users
+		$ skaffold completion fish | source  # for fish users
 		$ source <(skaffold completion zsh)  # for zsh users
 
 	Additionally, you may want to output the completion to a file and source in your .bashrc
@@ -52,6 +54,8 @@ func completion(cmd *cobra.Command, args []string) {
 	switch args[0] {
 	case "bash":
 		rootCmd(cmd).GenBashCompletion(os.Stdout)
+	case "fish":
+		rootCmd(cmd).GenFishCompletion(os.Stdout, true)
 	case "zsh":
 		runCompletionZsh(cmd, os.Stdout)
 	}
@@ -67,8 +71,8 @@ func NewCmdCompletion() *cobra.Command {
 			}
 			return cobra.OnlyValidArgs(cmd, args)
 		},
-		ValidArgs: []string{"bash", "zsh"},
-		Short:     "Output shell completion for the given shell (bash or zsh)",
+		ValidArgs: []string{"bash", "fish", "zsh"},
+		Short:     "Output shell completion for the given shell (bash, fish or zsh)",
 		Long:      longDescription,
 		Run:       completion,
 	}
diff --git a/deploy/skaffold/Dockerfile.deps b/deploy/skaffold/Dockerfile.deps
index e4448175d..09322b35e 100644
--- a/deploy/skaffold/Dockerfile.deps
+++ b/deploy/skaffold/Dockerfile.deps
@@ -151,7 +151,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
     build-essential \
     python-setuptools \
     lsb-release \
-    openjdk-11-jdk \
+    openjdk-17-jdk \
     software-properties-common \
     jq \
     apt-transport-https && \
diff --git a/deploy/skaffold/Dockerfile.deps.lts b/deploy/skaffold/Dockerfile.deps.lts
index cbd7d4ea4..ee40c65a7 100644
--- a/deploy/skaffold/Dockerfile.deps.lts
+++ b/deploy/skaffold/Dockerfile.deps.lts
@@ -95,7 +95,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
     build-essential \
     python-setuptools \
     lsb-release \
-    openjdk-11-jdk \
+    openjdk-17-jdk \
     software-properties-common \
     jq \
     apt-transport-https && \
diff --git a/deploy/skaffold/Dockerfile.deps.slim b/deploy/skaffold/Dockerfile.deps.slim
index cbd7d4ea4..ee40c65a7 100644
--- a/deploy/skaffold/Dockerfile.deps.slim
+++ b/deploy/skaffold/Dockerfile.deps.slim
@@ -95,7 +95,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
     build-essential \
     python-setuptools \
     lsb-release \
-    openjdk-11-jdk \
+    openjdk-17-jdk \
     software-properties-common \
     jq \
     apt-transport-https && \
diff --git a/docs-v2/content/en/docs/pipeline-stages/deployers/cloudrun.md b/docs-v2/content/en/docs/pipeline-stages/deployers/cloudrun.md
index e6f8e0976..13d791a81 100644
--- a/docs-v2/content/en/docs/pipeline-stages/deployers/cloudrun.md
+++ b/docs-v2/content/en/docs/pipeline-stages/deployers/cloudrun.md
@@ -15,6 +15,7 @@ Cloud Run is a managed compute platform on Google Cloud that allows you to run c
 ## Deploying applications to Cloud Run
 
 Skaffold can deploy services to Cloud Run. If this deployer is used, all provided manifests must be valid Cloud Run services, using the serving.knative.dev/v1 schema.
+See the [Cloud Run YAML reference](https://cloud.google.com/run/docs/reference/yaml/v1) for supported fields.
 
 This deployer will use the [application default credentials](https://cloud.google.com/docs/authentication/production#automatically) to deploy.  You can configure this to use your user credentials by running `gcloud auth application-default login`.
 
@@ -37,4 +38,4 @@ artifacts to Cloud Run:
 Images listed to be deployed with the Cloud Run deployer must be present in Google Artifact
 Registry or Google Container Registry. If you are using Skaffold to build the images, ensure `push` is 
 set to true.
-{{< /alert >}}
\ No newline at end of file
+{{< /alert >}}
diff --git a/docs-v2/content/en/docs/pipeline-stages/deployers/helm.md b/docs-v2/content/en/docs/pipeline-stages/deployers/helm.md
index 7791b8e9a..90a456589 100644
--- a/docs-v2/content/en/docs/pipeline-stages/deployers/helm.md
+++ b/docs-v2/content/en/docs/pipeline-stages/deployers/helm.md
@@ -16,12 +16,26 @@ To use `helm` with Skaffold, the `helm` binary must be installed on your machine
 
 # Configuring your Helm Project with Skaffold
 
-Skaffold supports projects set up to deploy with Helm, but certain aspects of the project need to be configured correctly in order for Skaffold to work properly. This guide should demystify some of the nuance around using Skaffold with Helm to help you get started quickly.
+Skaffold supports projects set up to render and/or deploy with Helm, but certain aspects of the project need to be configured correctly in order for Skaffold to work properly. This guide should demystify some of the nuance around using Skaffold with Helm to help you get started quickly.
 
-{{< alert title="No more `artifactOverrides`" >}}
-Skaffold no longer requires the intricate configuring of `artifactOverrides` and image naming strategies.
+{{< alert title="No more `artifactOverrides` or `imageStrategy`" >}}
+Skaffold no longer requires the intricate configuring of `artifactOverrides` or `imageStrategy` fields. See docs [here]({{< relref "#image-reference-strategies" >}}) on how old `artifactOverrides` and `imageStrategy` values translate to `setValues` entires in the latest Skaffold schemas (`apiVersion: skaffold/v3alpha1` or skaffold binary version `v2.0.0` onwards)
 {{< /alert >}}
 
+{{< alert title="In Skaffold `v2` the primary difference between the helm renderer (`manifest.helm.*`) and the helm deployer `deploy.helm.*` is the use of `helm install` vs `helm install`" >}}
+{{< /alert >}}
+
+## How `helm` render support works in Skaffold
+In the latest version of Skaffold, the primary methods of using `helm` templating with Skaffold involve the `deploy.helm.setValues` and the `deploy.helm.setValueTemplates` fields.  `deploy.helm.setValues` supplies the key:value pair to substitute from a users `values.yaml` file (a standard `helm` file for rendering).  `deploy.helm.setValueTemplates` does a similar thing only the key:value value comes from an environment variable instead of a given value.  The thing to note here is that when using a value that is the same name as an artifact name, that value will be replaced by the fully qualified artifact name (eg: `image: gcr.io/example-repo/skaffold-helm-image:latest@sha256:<sha256-hash>`) for the current build (or the specified value from additional configuration eg: CLI flags or config from `skaffold.yaml`).  Depending on how a user's `values.yaml` and charts specify `image: $IMAGE_TEMPLATE`, the docs [here]({{< relref "#image-reference-strategies" >}})  explain the proper `setValues` to use.  When migrating from schema version `v2beta29` or less, Skaffold will automatically configure these values to continue to work.
+
+
+`helm` render support in Skaffold is accomplished by calling `helm install ...` and using the `skaffold` binary as a `helm` `--post-renderer`.  This works by having Skaffold run `helm install ...` taking into consideration all of the supplied flags, skaffold.yaml configuration, etc. and creating an intermediate yaml manifest with all helm replacements except that the fully qualified image from the current run is NOT added but instead a placeholder with the artifact name - eg: `skaffold-helm-image`.  Then the skaffold post-renderer is called to convert `image: skaffold-helm-image` -> `image: gcr.io/example-repo/skaffold-helm-image:latest@sha256:<sha256-hash>` in specified locations (specific allowlisted k8s objects and/or k8s object fields).  This above replacement is nearly identical to how it works for values.yaml files using only the `image` key in `values.yaml` - eg:
+`image: "{{.Values.image}}"`
+
+When using `image.repoistory` + `image.tag` or `image.registry` + `image.repository` + `image.tag` - eg:
+`image: "{{.Values.image.repository}}:{{.Values.image.tag}}"`
+`image: "{{.Values.image.registry}}/{{.Values.image.repository}}:{{.Values.image.tag}}"`
+there is some specialized logic that the skaffold `post-renderer` uses to properly handling these cases.  See the docs [here]({{< relref "#image-reference-strategies" >}}) on the correct way to specify these for Skaffold using `setValues`
 
 ## Image Configuration
 The normal Helm convention for defining image references is through the `values.yaml` file. Often, image information is configured through an `image` stanza in the values file, which might look something like this:
@@ -29,7 +43,7 @@ The normal Helm convention for defining image references is through the `values.
 ```project_root/values.yaml```
 ```yaml
 image:
-  repository: gcr.io/my-project/my-image
+  repository: my-image
   tag: v1.2.0
   pullPolicy: IfNotPresent
 ```
@@ -54,22 +68,22 @@ Associating the Helm image key allows Skaffold to track the image being built, a
 ```yaml
 build:
   artifacts:
-    - image: gcr.io/my-project/my-image # must match in setValues
+    - image: my-image # must match in setValues
+    - image: my-image-2 # must match in setValues
 deploy:
   helm:
     releases:
     - name: my-release
       setValues:
-        image: gcr.io/my-project/my-image # no tag present!
-      imageStrategy:
-        helm: {}
+        image: my-image # no tag present!
+        image2: my-image-2 # no tag present!
 ```
 
-The `setValues` configuration binds a Helm key to the specified value.  The `imageStrategy` configures the image reference strategy for informing Helm of the image reference to a newly built artifact.
+The `setValues` configuration binds a Helm key to the specified value.
 
 ### Multiple image overrides
 
-To override multiple images (ie a Pod with a side car) you can simply add additional variables. For example, the following helm template:
+To override multiple images (ie a Pod with a side car) you can simply add additional variables. For example, the following helm install:
 
 ```yaml
 spec:
@@ -84,7 +98,7 @@ spec:
 
 can be overriden with:
 
-```
+```yaml
 deploy:
   helm:
     releases:
@@ -92,8 +106,150 @@ deploy:
       setValues:
         firstContainerImage: gcr.io/my-project/first-image # no tag present!
         secondContainerImage: gcr.io/my-project/second-image # no tag present!
-      imageStrategy:
-        helm: {}
+```
+
+### Image reference strategies
+
+Skaffold supports three _image reference strategies_ for Helm:
+
+1. `fqn`: provides a fully-qualified image reference (default);
+2. `helm`: provides separate repository and tag portions (shown above);
+3. `helm+explicitRegistry`: provides separate registry, repository, and tag portions.
+
+#### `fqn` strategy: single fully-qualified name (default)
+
+With the fully-qualified name strategy, Skaffold configures Helm by setting a key to the fully-tagged image reference.
+
+The `skaffold.yaml` setup:
+```yaml
+build:
+  artifacts:
+    - image: my-image  # must match in setValues
+    - image: my-image-2  # must match in setValues
+deploy:
+  helm:
+    releases:
+      - name: my-chart
+        chartPath: helm
+        setValues:
+          image: my-image # no tag present!
+          image2: my-image-2 # no tag present!
+```
+The `values.yaml` (note that Skaffold overrides this value):
+```
+image: gcr.io/other-project/other-image:latest
+image2: gcr.io/other-project/other-image:latest
+```
+
+The chart template:
+```yaml
+spec:
+  containers:
+    - name: {{ .Chart.Name }}
+      image: "{{.Values.image}}"
+    - name: {{ .Chart.Name }}
+      image: "{{.Values.image2}}"
+```
+
+Skaffold will invoke
+```
+helm install <chart> <chart-path> --set-string image=<artifact-name>,image2=<artifact-name> --post-renderer=<path-to-skaffold-binary-from-original-invocation>
+```
+
+#### `helm` strategy: split repository and tag
+
+Skaffold can be configured to provide Helm with a separate repository and tag.  The key used in the `artifactOverrides` is used as base portion producing two keys `{key}.repository` and `{key}.tag`.
+
+The `skaffold.yaml` setup:
+```yaml
+build:
+  artifacts:
+    - image: my-image # must match in setValues
+    - image: my-image-2 # must match in setValues
+deploy:
+  helm:
+    releases:
+      - name: my-chart
+        chartPath: helm
+        setValues:
+          image.repository: my-image
+          image.tag: my-image
+          image2.repository: my-image-2
+          image2.tag: my-image-2
+```
+
+The `values.yaml` (note that Skaffold overrides these values):
+```
+image:
+  repository: gcr.io/other-project/other-image
+  tag: latest
+image2:
+  repository: gcr.io/other-project/other-image
+  tag: latest
+```
+
+The chart template:
+```yaml
+spec:
+  containers:
+    - name: {{ .Chart.Name }}
+      image: "{{.Values.image.repository}}:{{.Values.image.tag}}"
+    - name: {{ .Chart.Name }}
+      image: "{{.Values.image2.repository}}:{{.Values.image2.tag}}"
+```
+
+Skaffold will invoke
+```
+helm install <chart> <chart-path> --set-string image.repository=<artifact-name>,image.tag=<artifact-name>,image2.repository=<artifact-name>,image2.tag=<artifact-name>  --post-renderer=<path-to-skaffold-binary-from-original-invocation>
+```
+
+#### `helm`+`explicitRegistry` strategy: split registry, repository, and tag
+
+Skaffold can also be configured to provide Helm with a separate repository and tag.  The key used in the `artifactOverrides` is used as base portion producing three keys: `{key}.registry`, `{key}.repository`, and `{key}.tag`.
+
+The `skaffold.yaml` setup:
+```yaml
+build:
+  artifacts:
+    - image: my-image # must match in setValues
+    - image: my-image-2 # must match in setValues
+deploy:
+  helm:
+    releases:
+      - name: my-chart
+        chartPath: helm
+        setValues:
+          image.registry: my-image
+          image.repository: my-image
+          image.tag: my-image
+          image2.registry: my-image-2
+          image2.repository: my-image-2
+          image2.tag: my-image-2
+```
+
+The `values.yaml` (note that Skaffold overrides these values):
+```
+image:
+  registry: gcr.io
+  repository: other-project/other-image
+  tag: latest
+image2:
+  registry: gcr.io
+  repository: other-project/other-image
+  tag: latest
+```
+
+The chart template:
+```yaml
+spec:
+  containers:
+    - name: {{ .Chart.Name }}
+      image: "{{.Values.image.registry}}/{{.Values.image.repository}}:{{.Values.image.tag}}"
+```
+
+Skaffold will invoke
+```
+helm install <chart> <chart-path> --set-string image.registry=<artifact-name>,image.repository=<artifact-name>,tag=<artifcact-name> --post-renderer=<path-to-skaffold-binary-from-original-invocation>
 ```
 
 ### Helm Build Dependencies
diff --git a/docs-v2/content/en/docs/pipeline-stages/renderers/helm.md b/docs-v2/content/en/docs/pipeline-stages/renderers/helm.md
index e28b9152c..0d8a71dce 100644
--- a/docs-v2/content/en/docs/pipeline-stages/renderers/helm.md
+++ b/docs-v2/content/en/docs/pipeline-stages/renderers/helm.md
@@ -1,6 +1,6 @@
 ---
-title: "Helm"
-linkTitle: "Helm"
+title: "Helm [UPDATED]"
+linkTitle: "Helm [UPDATED]"
 weight: 40
 featureId: render
 ---
@@ -13,36 +13,259 @@ for projects configured to use helm.
 To use `helm` with Skaffold, the `helm` binary must be installed on your machine. Skaffold will not install it for you.
 {{< /alert >}}
 
-# Rendering with helm
-[`helm template`](https://helm.sh/docs/helm/helm_template/) allows Kubernetes
-developers to locally render templates. Skaffold relies on `helm template --post-renderer` functionality to substitute the images
-in the rendered charts with Skaffold built images.
 
+# Configuring your Helm Project with Skaffold
 
-{{< alert title="Note" >}}
-If you wish to deploy using helm, please see [Configuring Helm Deployer Section]({{< relref "/docs/pipeline-stages/deployers/helm.md" >}})
+Skaffold supports projects set up to render and/or deploy with Helm, but certain aspects of the project need to be configured correctly in order for Skaffold to work properly. This guide should demystify some of the nuance around using Skaffold with Helm to help you get started quickly.
+
+{{< alert title="No more `artifactOverrides` or `imageStrategy`" >}}
+Skaffold no longer requires the intricate configuring of `artifactOverrides` or `imageStrategy` fields. See docs [here]({{< relref "#image-reference-strategies" >}}) on how old `artifactOverrides` and `imageStrategy` values translate to `setValues` entires in the latest Skaffold schemas (`apiVersion: skaffold/v3alpha1` or skaffold binary version `v2.0.0` onwards)
 {{< /alert >}}
 
-### Configuration
+{{< alert title="In Skaffold `v2` the primary difference between the helm renderer (`manifest.helm.*`) and the helm deployer `deploy.helm.*` is the use of `helm template` vs `helm install`" >}}
+{{< /alert >}}
+
+## How `helm` render support works in Skaffold
+In the latest version of Skaffold, the primary methods of using `helm` templating with Skaffold involve the `manifests.helm.setValues` and the `manifests.helm.setValueTemplates` fields.  `manifests.helm.setValues` supplies the key:value pair to substitute from a users `values.yaml` file (a standard `helm` file for rendering).  `manifests.helm.setValueTemplates` does a similar thing only the key:value value comes from an environment variable instead of a given value.  The thing to note here is that when using a value that is the same name as an artifact name, that value will be replaced by the fully qualified artifact name (eg: `image: gcr.io/example-repo/skaffold-helm-image:latest@sha256:<sha256-hash>`) for the current build (or the specified value from additional configuration eg: CLI flags or config from `skaffold.yaml`).  Depending on how a user's `values.yaml` and charts specify `image: $IMAGE_TEMPLATE`, the docs [here]({{< relref "#image-reference-strategies" >}})  explain the proper `setValues` to use.  When migrating from schema version `v2beta29` or less, Skaffold will automatically configure these values to continue to work.
+
+
+`helm` render support in Skaffold is accomplished by calling `helm template ...` and using the `skaffold` binary as a `helm` `--post-renderer`.  This works by having Skaffold run `helm template ...` taking into consideration all of the supplied flags, skaffold.yaml configuration, etc. and creating an intermediate yaml manifest with all helm replacements except that the fully qualified image from the current run is NOT added but instead a placeholder with the artifact name - eg: `skaffold-helm-image`.  Then the skaffold post-renderer is called to convert `image: skaffold-helm-image` -> `image: gcr.io/example-repo/skaffold-helm-image:latest@sha256:<sha256-hash>` in specified locations (specific allowlisted k8s objects and/or k8s object fields).  This above replacement is nearly identical to how it works for values.yaml files using only the `image` key in `values.yaml` - eg:
+`image: "{{.Values.image}}"`
+
+When using `image.repoistory` + `image.tag` or `image.registry` + `image.repository` + `image.tag` - eg:
+`image: "{{.Values.image.repository}}:{{.Values.image.tag}}"`
+`image: "{{.Values.image.registry}}/{{.Values.image.repository}}:{{.Values.image.tag}}"`
+there is some specialized logic that the skaffold `post-renderer` uses to properly handling these cases.  See the docs [here]({{< relref "#image-reference-strategies" >}}) on the correct way to specify these for Skaffold using `setValues`
+
+## Image Configuration
+The normal Helm convention for defining image references is through the `values.yaml` file. Often, image information is configured through an `image` stanza in the values file, which might look something like this:
+
+```project_root/values.yaml```
+```yaml
+image:
+  repository: my-image
+  tag: v1.2.0
+  pullPolicy: IfNotPresent
+```
+
+This image would then be referenced in a templated resource file, maybe like this:
+
+```project_root/templates/deployment.yaml:```
+```yaml
+spec:
+  template:
+    spec:
+      containers:
+        - name: {{ .Chart.Name }}
+          image: {{ .Values.image.repository }}:{{ .Values.image.tag}}
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+```
+
+**IMPORTANT: To get Skaffold to work with Helm, the `image` key must be configured in the skaffold.yaml.**
+
+Associating the Helm image key allows Skaffold to track the image being built, and then configure Helm to substitute it in the proper resource definitions to be deployed to your cluster. In practice, this looks something like this:
+
+```yaml
+build:
+  artifacts:
+    - image: my-image # must match in setValues
+    - image: my-image-2 # must match in setValues
+manifests:
+  helm:
+    releases:
+    - name: my-release
+      setValues:
+        image: my-image # no tag present!
+        image2: my-image-2 # no tag present!
+```
+
+The `setValues` configuration binds a Helm key to the specified value.
+
+### Multiple image overrides
+
+To override multiple images (ie a Pod with a side car) you can simply add additional variables. For example, the following helm template:
+
+```yaml
+spec:
+  containers:
+    - name: firstContainer
+      image: "{{.Values.firstContainerImage}}"
+      ....
+    - name: secondContainer
+      image: "{{.Values.secondContainerImage}}"
+      ...
+```
+
+can be overriden with:
+
+```yaml
+manifests:
+  helm:
+    releases:
+    - name: my-release
+      setValues:
+        firstContainerImage: gcr.io/my-project/first-image # no tag present!
+        secondContainerImage: gcr.io/my-project/second-image # no tag present!
+```
 
-To use render using helm but deploy via kubectl deployer define your helm charts under
-`helm` in `manifests` section of `skaffold.yaml`.
+### Image reference strategies
 
+Skaffold supports three _image reference strategies_ for Helm:
 
-### Example
-The following `manifests` section instructs Skaffold to render
-artifacts using helm.
+1. `fqn`: provides a fully-qualified image reference (default);
+2. `helm`: provides separate repository and tag portions (shown above);
+3. `helm+explicitRegistry`: provides separate registry, repository, and tag portions.
 
-{{% readfile file="samples/renderers/helm.yaml" %}}
+#### `fqn` strategy: single fully-qualified name (default)
 
+With the fully-qualified name strategy, Skaffold configures Helm by setting a key to the fully-tagged image reference.
+
+The `skaffold.yaml` setup:
+```yaml
+build:
+  artifacts:
+    - image: my-image  # must match in setValues
+    - image: my-image-2  # must match in setValues
+manifests:
+  helm:
+    releases:
+      - name: my-chart
+        chartPath: helm
+        setValues:
+          image: my-image # no tag present!
+          image2: my-image-2 # no tag present!
+```
+The `values.yaml` (note that Skaffold overrides this value):
+```
+image: gcr.io/other-project/other-image:latest
+image2: gcr.io/other-project/other-image:latest
+```
+
+The chart template:
+```yaml
+spec:
+  containers:
+    - name: {{ .Chart.Name }}
+      image: "{{.Values.image}}"
+    - name: {{ .Chart.Name }}
+      image: "{{.Values.image2}}"
+```
+
+Skaffold will invoke
+```
+helm template <chart> <chart-path> --set-string image=<artifact-name>,image2=<artifact-name> --post-renderer=<path-to-skaffold-binary-from-original-invocation>
+```
+
+#### `helm` strategy: split repository and tag
+
+Skaffold can be configured to provide Helm with a separate repository and tag.  The key used in the `artifactOverrides` is used as base portion producing two keys `{key}.repository` and `{key}.tag`.
+
+The `skaffold.yaml` setup:
+```yaml
+build:
+  artifacts:
+    - image: my-image # must match in setValues
+    - image: my-image-2 # must match in setValues
+manifests:
+  helm:
+    releases:
+      - name: my-chart
+        chartPath: helm
+        setValues:
+          image.repository: my-image
+          image.tag: my-image
+          image2.repository: my-image-2
+          image2.tag: my-image-2
+```
+
+The `values.yaml` (note that Skaffold overrides these values):
+```
+image:
+  repository: gcr.io/other-project/other-image
+  tag: latest
+image2:
+  repository: gcr.io/other-project/other-image
+  tag: latest
+```
+
+The chart template:
+```yaml
+spec:
+  containers:
+    - name: {{ .Chart.Name }}
+      image: "{{.Values.image.repository}}:{{.Values.image.tag}}"
+    - name: {{ .Chart.Name }}
+      image: "{{.Values.image2.repository}}:{{.Values.image2.tag}}"
+```
+
+Skaffold will invoke
+```
+helm template <chart> <chart-path> --set-string image.repository=<artifact-name>,image.tag=<artifact-name>,image2.repository=<artifact-name>,image2.tag=<artifact-name>  --post-renderer=<path-to-skaffold-binary-from-original-invocation>
+```
+
+#### `helm`+`explicitRegistry` strategy: split registry, repository, and tag
+
+Skaffold can also be configured to provide Helm with a separate repository and tag.  The key used in the `artifactOverrides` is used as base portion producing three keys: `{key}.registry`, `{key}.repository`, and `{key}.tag`.
+
+The `skaffold.yaml` setup:
+```yaml
+build:
+  artifacts:
+    - image: my-image # must match in setValues
+    - image: my-image-2 # must match in setValues
+manifests:
+  helm:
+    releases:
+      - name: my-chart
+        chartPath: helm
+        setValues:
+          image.registry: my-image
+          image.repository: my-image
+          image.tag: my-image
+          image2.registry: my-image-2
+          image2.repository: my-image-2
+          image2.tag: my-image-2
+```
+
+The `values.yaml` (note that Skaffold overrides these values):
+```
+image:
+  registry: gcr.io
+  repository: other-project/other-image
+  tag: latest
+image2:
+  registry: gcr.io
+  repository: other-project/other-image
+  tag: latest
+```
+
+The chart template:
+```yaml
+spec:
+  containers:
+    - name: {{ .Chart.Name }}
+      image: "{{.Values.image.registry}}/{{.Values.image.repository}}:{{.Values.image.tag}}"
+```
+
+Skaffold will invoke
+```
+helm template <chart> <chart-path> --set-string image.registry=<artifact-name>,image.repository=<artifact-name>,tag=<artifcact-name> --post-renderer=<path-to-skaffold-binary-from-original-invocation>
+```
+
+### Helm Build Dependencies
+
+The `skipBuildDependencies` flag toggles whether dependencies of the Helm chart are built with the `helm dep build` command. This command manipulates files inside the `charts` subfolder of the specified Helm chart.
+
+If `skipBuildDependencies` is `false` then `skaffold dev` does **not** watch the `charts` subfolder of the Helm chart, in order to prevent a build loop - the actions of `helm dep build` always trigger another build.
+
+If `skipBuildDependencies` is `true` then `skaffold dev` watches all files inside the Helm chart.
 
 ### `skaffold.yaml` Configuration
 
 The `helm` type offers the following options:
 
-{{< schema root="manifests-helm" >}}
+{{< schema root="HelmDeploy" >}}
 
 Each `release` includes the following fields:
 
 {{< schema root="HelmRelease" >}}
-
diff --git a/docs-v2/content/en/docs/upgrading/_index.md b/docs-v2/content/en/docs/upgrading/_index.md
index 4d727617f..46b5205ee 100644
--- a/docs-v2/content/en/docs/upgrading/_index.md
+++ b/docs-v2/content/en/docs/upgrading/_index.md
@@ -37,6 +37,7 @@ Outside of the above, there are currently no known other regressions when migrat
 - `helm` renderer/deployer usage (see [helm docs]({{< relref "/docs/pipeline-stages/renderers/helm" >}}) for more details)
 - v1 `kpt` deployer usage 
 - `skaffold render` flags usage (see [render docs]({{< relref "/docs/pipeline-stages/renderers" >}}) and [render schema]({{< relref "/docs/references/yaml#manifests" >}}) for more details)
+- `helm` hooks support is currently not available in the latest version of skaffold due to helm not supporting hooks when using a `--post-renderer` [Skaffold Helm hook issue](https://github.com/GoogleContainerTools/skaffold/issues/7989), [Helm post-renderer hook issue](https://github.com/helm/helm/issues/7891)
 
 
 If you encounter any issues using skaffold `v2.0.0-beta3`, particularly any regressions that used to work differently or succeed in `v1`, please file an issue at[GoogleContainerTools/skaffold](https://github.com/GoogleContainerTools/skaffold/issues).
diff --git a/docs/content/en/docs/references/cli/_index.md b/docs/content/en/docs/references/cli/_index.md
index ce95ad380..96391a227 100644
--- a/docs/content/en/docs/references/cli/_index.md
+++ b/docs/content/en/docs/references/cli/_index.md
@@ -85,7 +85,7 @@ Getting Started With a New Project:
   init              Generate configuration for deploying an application
 
 Other Commands:
-  completion        Output shell completion for the given shell (bash or zsh)
+  completion        Output shell completion for the given shell (bash, fish or zsh)
   config            Interact with the global Skaffold config file (defaults to `$HOME/.skaffold/config`)
   diagnose          Run a diagnostic on Skaffold
   fix               Update old configuration to a newer schema version
@@ -280,7 +280,7 @@ Env vars:
 
 ### skaffold completion
 
-Output shell completion for the given shell (bash or zsh)
+Output shell completion for the given shell (bash, fish or zsh)
 
 ```
 
diff --git a/examples/bazel/skaffold.yaml b/examples/bazel/skaffold.yaml
index 1a8e92672..6de5da1d0 100644
--- a/examples/bazel/skaffold.yaml
+++ b/examples/bazel/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/buildpacks-java/skaffold.yaml b/examples/buildpacks-java/skaffold.yaml
index cb115cbd7..799b8dea7 100644
--- a/examples/buildpacks-java/skaffold.yaml
+++ b/examples/buildpacks-java/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/buildpacks-node/skaffold.yaml b/examples/buildpacks-node/skaffold.yaml
index 9b4dc7e01..1b5bfdc24 100644
--- a/examples/buildpacks-node/skaffold.yaml
+++ b/examples/buildpacks-node/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/buildpacks-python/skaffold.yaml b/examples/buildpacks-python/skaffold.yaml
index 00cbad0e2..3389161f9 100644
--- a/examples/buildpacks-python/skaffold.yaml
+++ b/examples/buildpacks-python/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/buildpacks/skaffold.yaml b/examples/buildpacks/skaffold.yaml
index 1e1460dd5..7a1dff63b 100644
--- a/examples/buildpacks/skaffold.yaml
+++ b/examples/buildpacks/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/cross-platform-builds/skaffold.yaml b/examples/cross-platform-builds/skaffold.yaml
index 5c866f824..8734e0f8b 100644
--- a/examples/cross-platform-builds/skaffold.yaml
+++ b/examples/cross-platform-builds/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/custom-buildx/skaffold.yaml b/examples/custom-buildx/skaffold.yaml
index 5b6ce230b..fc1468424 100644
--- a/examples/custom-buildx/skaffold.yaml
+++ b/examples/custom-buildx/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3alpha1
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   platforms: ["linux/amd64", "linux/arm64"]
diff --git a/examples/custom/skaffold.yaml b/examples/custom/skaffold.yaml
index 4603b4913..3c412806d 100644
--- a/examples/custom/skaffold.yaml
+++ b/examples/custom/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/dev-journey-buildpacks/skaffold.yaml b/examples/dev-journey-buildpacks/skaffold.yaml
index c4079a815..392ff1edf 100644
--- a/examples/dev-journey-buildpacks/skaffold.yaml
+++ b/examples/dev-journey-buildpacks/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/docker-deploy/skaffold.yaml b/examples/docker-deploy/skaffold.yaml
index ba6dd2a59..a26cf99a4 100644
--- a/examples/docker-deploy/skaffold.yaml
+++ b/examples/docker-deploy/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   local:
diff --git a/examples/gcb-kaniko/skaffold.yaml b/examples/gcb-kaniko/skaffold.yaml
index cd34896ee..7ec7712ee 100644
--- a/examples/gcb-kaniko/skaffold.yaml
+++ b/examples/gcb-kaniko/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   googleCloudBuild:
diff --git a/examples/generate-pipeline/skaffold.yaml b/examples/generate-pipeline/skaffold.yaml
index 66c30cbdd..71362b72f 100644
--- a/examples/generate-pipeline/skaffold.yaml
+++ b/examples/generate-pipeline/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/getting-started-kustomize/skaffold.yaml b/examples/getting-started-kustomize/skaffold.yaml
index b25b514ef..bdf689add 100644
--- a/examples/getting-started-kustomize/skaffold.yaml
+++ b/examples/getting-started-kustomize/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 metadata:
   name: getting-started-kustomize
diff --git a/examples/getting-started/skaffold.yaml b/examples/getting-started/skaffold.yaml
index 3d30914b3..8d176cf81 100644
--- a/examples/getting-started/skaffold.yaml
+++ b/examples/getting-started/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/google-cloud-build/skaffold.yaml b/examples/google-cloud-build/skaffold.yaml
index 180cf56d6..59f9df8c4 100644
--- a/examples/google-cloud-build/skaffold.yaml
+++ b/examples/google-cloud-build/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   googleCloudBuild:
diff --git a/examples/helm-deployment-dependencies/skaffold.yaml b/examples/helm-deployment-dependencies/skaffold.yaml
index ef6d9e3f9..760b58cea 100644
--- a/examples/helm-deployment-dependencies/skaffold.yaml
+++ b/examples/helm-deployment-dependencies/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   tagPolicy:
diff --git a/examples/helm-deployment/skaffold.yaml b/examples/helm-deployment/skaffold.yaml
index a0cda4add..e4d8ee327 100644
--- a/examples/helm-deployment/skaffold.yaml
+++ b/examples/helm-deployment/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/helm-remote-repo/skaffold.yaml b/examples/helm-remote-repo/skaffold.yaml
index 903ccd590..b37f1e8f2 100644
--- a/examples/helm-remote-repo/skaffold.yaml
+++ b/examples/helm-remote-repo/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 deploy:
   helm:
diff --git a/examples/helm-render/skaffold.yaml b/examples/helm-render/skaffold.yaml
index 9d99949d2..9edb3b5bf 100644
--- a/examples/helm-render/skaffold.yaml
+++ b/examples/helm-render/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/hot-reload/skaffold.yaml b/examples/hot-reload/skaffold.yaml
index 49cf8331a..d62e05205 100644
--- a/examples/hot-reload/skaffold.yaml
+++ b/examples/hot-reload/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/jib-gradle/skaffold.yaml b/examples/jib-gradle/skaffold.yaml
index 1d820fd94..4d2c0b7fa 100644
--- a/examples/jib-gradle/skaffold.yaml
+++ b/examples/jib-gradle/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/jib-multimodule/skaffold.yaml b/examples/jib-multimodule/skaffold.yaml
index c4818a532..1f83b06f3 100644
--- a/examples/jib-multimodule/skaffold.yaml
+++ b/examples/jib-multimodule/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/jib-sync/skaffold-gradle.yaml b/examples/jib-sync/skaffold-gradle.yaml
index fd29a5118..448024005 100644
--- a/examples/jib-sync/skaffold-gradle.yaml
+++ b/examples/jib-sync/skaffold-gradle.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/jib-sync/skaffold-maven.yaml b/examples/jib-sync/skaffold-maven.yaml
index 4fb33df59..3eef86c1a 100644
--- a/examples/jib-sync/skaffold-maven.yaml
+++ b/examples/jib-sync/skaffold-maven.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/jib/skaffold.yaml b/examples/jib/skaffold.yaml
index a622df80f..a4eb39e93 100644
--- a/examples/jib/skaffold.yaml
+++ b/examples/jib/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/kaniko/skaffold.yaml b/examples/kaniko/skaffold.yaml
index 076b6aa88..bba3648af 100644
--- a/examples/kaniko/skaffold.yaml
+++ b/examples/kaniko/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/ko-sync-infer/skaffold.yaml b/examples/ko-sync-infer/skaffold.yaml
index aea40621e..b8d465aeb 100644
--- a/examples/ko-sync-infer/skaffold.yaml
+++ b/examples/ko-sync-infer/skaffold.yaml
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/ko/skaffold.yaml b/examples/ko/skaffold.yaml
index 646cc7911..826187cdf 100644
--- a/examples/ko/skaffold.yaml
+++ b/examples/ko/skaffold.yaml
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/kustomize/skaffold-kustomize-args.yaml b/examples/kustomize/skaffold-kustomize-args.yaml
index 8a7e696f3..aece1007a 100644
--- a/examples/kustomize/skaffold-kustomize-args.yaml
+++ b/examples/kustomize/skaffold-kustomize-args.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 deploy:
   kustomize:
diff --git a/examples/kustomize/skaffold.yaml b/examples/kustomize/skaffold.yaml
index a7c2b2e36..7a8f5310f 100644
--- a/examples/kustomize/skaffold.yaml
+++ b/examples/kustomize/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 manifests:
   kustomize:
diff --git a/examples/lifecycle-hooks/skaffold.yaml b/examples/lifecycle-hooks/skaffold.yaml
index 59fc39792..8826595a9 100644
--- a/examples/lifecycle-hooks/skaffold.yaml
+++ b/examples/lifecycle-hooks/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 manifests:
   rawYaml:
diff --git a/examples/microservices/skaffold.yaml b/examples/microservices/skaffold.yaml
index 950c1006e..7decb4fe8 100644
--- a/examples/microservices/skaffold.yaml
+++ b/examples/microservices/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/multi-config-microservices/base/skaffold.yaml b/examples/multi-config-microservices/base/skaffold.yaml
index 95fdea9c9..21fba868e 100644
--- a/examples/multi-config-microservices/base/skaffold.yaml
+++ b/examples/multi-config-microservices/base/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/multi-config-microservices/leeroy-app/skaffold.yaml b/examples/multi-config-microservices/leeroy-app/skaffold.yaml
index ab67f4f12..bc36907f4 100644
--- a/examples/multi-config-microservices/leeroy-app/skaffold.yaml
+++ b/examples/multi-config-microservices/leeroy-app/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 metadata:
   name: app-config
diff --git a/examples/multi-config-microservices/leeroy-web/skaffold.yaml b/examples/multi-config-microservices/leeroy-web/skaffold.yaml
index bcadcdd2c..8f99a5891 100644
--- a/examples/multi-config-microservices/leeroy-web/skaffold.yaml
+++ b/examples/multi-config-microservices/leeroy-web/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 metadata:
   name: web-config
diff --git a/examples/multi-config-microservices/skaffold.yaml b/examples/multi-config-microservices/skaffold.yaml
index 7190ba6de..db4776baf 100644
--- a/examples/multi-config-microservices/skaffold.yaml
+++ b/examples/multi-config-microservices/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 requires:
 - path: ./leeroy-app
diff --git a/examples/multiple-renderers/skaffold.yaml b/examples/multiple-renderers/skaffold.yaml
index 38927c3d8..6a9581ecd 100644
--- a/examples/multiple-renderers/skaffold.yaml
+++ b/examples/multiple-renderers/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 metadata:
   name: go-guestbook
diff --git a/examples/nodejs/skaffold.yaml b/examples/nodejs/skaffold.yaml
index 3220b475a..adfc179d3 100644
--- a/examples/nodejs/skaffold.yaml
+++ b/examples/nodejs/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 
 build:
diff --git a/examples/profile-patches/skaffold.yaml b/examples/profile-patches/skaffold.yaml
index 4604fa59e..750cb1f8e 100644
--- a/examples/profile-patches/skaffold.yaml
+++ b/examples/profile-patches/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   # only build and deploy "base-service" on main profile
diff --git a/examples/profiles/skaffold.yaml b/examples/profiles/skaffold.yaml
index ecc530fd9..ba8e5b16d 100644
--- a/examples/profiles/skaffold.yaml
+++ b/examples/profiles/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   # only build and deploy "world-service" on main profile
diff --git a/examples/react-reload-docker/skaffold.yaml b/examples/react-reload-docker/skaffold.yaml
index 4cf90a3d1..de1c1941d 100644
--- a/examples/react-reload-docker/skaffold.yaml
+++ b/examples/react-reload-docker/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   local:
diff --git a/examples/react-reload/skaffold.yaml b/examples/react-reload/skaffold.yaml
index 73bfe1826..cd97a1aa1 100644
--- a/examples/react-reload/skaffold.yaml
+++ b/examples/react-reload/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/remote-multi-config-microservices/skaffold.yaml b/examples/remote-multi-config-microservices/skaffold.yaml
index 64541a923..9e77b69fe 100644
--- a/examples/remote-multi-config-microservices/skaffold.yaml
+++ b/examples/remote-multi-config-microservices/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 requires:
 - git:
diff --git a/examples/ruby/skaffold.yaml b/examples/ruby/skaffold.yaml
index 6f57a9540..70c39eb5d 100644
--- a/examples/ruby/skaffold.yaml
+++ b/examples/ruby/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/simple-artifact-dependency/skaffold.yaml b/examples/simple-artifact-dependency/skaffold.yaml
index 7af08b0a8..ac150b80a 100644
--- a/examples/simple-artifact-dependency/skaffold.yaml
+++ b/examples/simple-artifact-dependency/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/tagging-with-environment-variables/skaffold.yaml b/examples/tagging-with-environment-variables/skaffold.yaml
index 4ceafd6c2..35550f900 100644
--- a/examples/tagging-with-environment-variables/skaffold.yaml
+++ b/examples/tagging-with-environment-variables/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/templated-fields/skaffold.yaml b/examples/templated-fields/skaffold.yaml
index 1fa18ec1a..260255990 100644
--- a/examples/templated-fields/skaffold.yaml
+++ b/examples/templated-fields/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 metadata:
   name: my-app
diff --git a/examples/typescript/skaffold.yaml b/examples/typescript/skaffold.yaml
index 6317caf1a..235e8dd79 100644
--- a/examples/typescript/skaffold.yaml
+++ b/examples/typescript/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 
 build:
diff --git a/integration/util.go b/integration/util.go
index 8d210e36e..2804787c5 100644
--- a/integration/util.go
+++ b/integration/util.go
@@ -20,6 +20,7 @@ import (
 	"bufio"
 	"context"
 	"fmt"
+	"hash/fnv"
 	"io"
 	"os"
 	"os/exec"
@@ -35,7 +36,6 @@ import (
 	typedappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
 	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
 
-	"github.com/GoogleContainerTools/skaffold/integration/binpack"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
 	kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
@@ -50,6 +50,7 @@ const (
 	CanRunWithoutGcp TestType = iota
 	NeedsGcp
 )
+const numberOfPartition = 4
 
 func MarkIntegrationTest(t *testing.T, testType TestType) {
 	t.Helper()
@@ -67,11 +68,11 @@ func MarkIntegrationTest(t *testing.T, testType TestType) {
 		t.Skip("skipping non-GCP integration test")
 	}
 
-	if partition() && testType == CanRunWithoutGcp && !matchesPartition(t, t.Name(), binpack.Timings, binpack.MaxBinTime) {
+	if partition() && testType == CanRunWithoutGcp && !matchesPartition(t) {
 		t.Skipf("skipping non-GCP integration test that doesn't match partition %s", getPartition())
 	}
 
-	if partition() && testType == NeedsGcp && !matchesPartition(t, t.Name(), binpack.GCPTimings, binpack.MaxGCPBinTime) {
+	if partition() && testType == NeedsGcp && !matchesPartition(t) {
 		t.Skipf("Skipping GCP integration test that doesn't match partition %s", getPartition())
 	}
 }
@@ -84,16 +85,17 @@ func getPartition() string {
 	return os.Getenv("IT_PARTITION")
 }
 
-func matchesPartition(t *testing.T, testName string, timings []binpack.Timing, maxBinTime float64) bool {
-	var partition int
-	m, lastPartition := binpack.Partitions(timings, maxBinTime)
-	if p, ok := m[testName]; ok {
-		partition = p
-	} else {
-		partition = lastPartition
-	}
-	t.Logf("Test partition: %d", partition)
-	return strconv.Itoa(partition) == getPartition()
+func matchesPartition(t *testing.T) bool {
+	partition := hash(t.Name()) % numberOfPartition
+	t.Logf("Assinged test %s to partition: %d", t.Name(), partition)
+
+	return strconv.FormatUint(partition, 10) == getPartition()
+}
+
+func hash(s string) uint64 {
+	h := fnv.New64a()
+	h.Write([]byte(s))
+	return h.Sum64()
 }
 
 func Run(t *testing.T, dir, command string, args ...string) {
diff --git a/pkg/skaffold/deploy/cloudrun/deploy.go b/pkg/skaffold/deploy/cloudrun/deploy.go
index f1fb48e8c..6411a0fc8 100644
--- a/pkg/skaffold/deploy/cloudrun/deploy.go
+++ b/pkg/skaffold/deploy/cloudrun/deploy.go
@@ -49,12 +49,13 @@ type Config interface {
 	PortForwardResources() []*latest.PortForwardResource
 	PortForwardOptions() config.PortForwardOptions
 	Mode() config.RunMode
+	Tail() bool
 }
 
 // Deployer deploys code to Google Cloud Run.
 type Deployer struct {
 	configName string
-	logger     log.Logger
+	logger     *LogAggregator
 	accessor   *RunAccessor
 	monitor    *Monitor
 	labeller   *label.DefaultLabeller
@@ -74,7 +75,7 @@ func NewDeployer(cfg Config, labeller *label.DefaultLabeller, crDeploy *latest.C
 		Project:    crDeploy.ProjectID,
 		Region:     crDeploy.Region,
 		// TODO: implement logger for Cloud Run.
-		logger:        &log.NoopLogger{},
+		logger:        NewLoggerAggregator(cfg, labeller.GetRunID()),
 		accessor:      NewAccessor(cfg, labeller.GetRunID()),
 		labeller:      labeller,
 		useGcpOptions: true,
@@ -104,7 +105,7 @@ func (d *Deployer) Dependencies() ([]string, error) {
 
 // Cleanup deletes the created Cloud Run services
 func (d *Deployer) Cleanup(ctx context.Context, out io.Writer, dryRun bool, byConfig manifest.ManifestListByConfig) error {
-	return d.deleteRunService(ctx, out, dryRun, byConfig.GetForConfig(d.configName))
+	return d.cleanupRun(ctx, out, dryRun, byConfig.GetForConfig(d.configName))
 }
 
 // GetDebugger Get the Debugger for Cloud Run. Not supported by this deployer.
@@ -233,6 +234,7 @@ func (d *Deployer) deployService(crclient *run.APIService, manifest []byte, out
 	parent := fmt.Sprintf("projects/%s/locations/%s", service.Metadata.Namespace, d.Region)
 
 	sName := resName.String()
+	d.logger.AddResource(resName)
 	getCall := crclient.Projects.Locations.Services.Get(sName)
 	_, err := getCall.Do()
 
@@ -325,16 +327,47 @@ func (d *Deployer) deployJob(crclient *run.APIService, manifest []byte, out io.W
 	return &resName, nil
 }
 
-func (d *Deployer) deleteRunService(ctx context.Context, out io.Writer, dryRun bool, manifests manifest.ManifestList) error {
-	if len(manifests) != 1 {
-		return sErrors.NewError(fmt.Errorf("unexpected manifest for Cloud Run"),
-			&proto.ActionableErr{
-				Message: "Cloud Run expected a single Service manifest.",
-				ErrCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
-			})
+func (d *Deployer) cleanupRun(ctx context.Context, out io.Writer, dryRun bool, manifests manifest.ManifestList) error {
+	var errors []error
+	cOptions := d.clientOptions
+	if d.useGcpOptions {
+		cOptions = append(cOptions, option.WithEndpoint(fmt.Sprintf("%s-run.googleapis.com", d.Region)))
+		cOptions = append(gcp.ClientOptions(ctx), cOptions...)
+	}
+	crclient, err := run.NewService(ctx, cOptions...)
+	if err != nil {
+		return sErrors.NewError(fmt.Errorf("unable to create Cloud Run Client"), &proto.ActionableErr{
+			Message: err.Error(),
+			ErrCode: proto.StatusCode_DEPLOY_GET_CLOUD_RUN_CLIENT_ERR,
+		})
+	}
+	for _, manifest := range manifests {
+		tpe, err := getTypeFromManifest(manifest)
+		switch {
+		case err != nil:
+			errors = append(errors, err)
+		case tpe == typeService:
+			err := d.deleteRunService(crclient, out, dryRun, manifest)
+			if err != nil {
+				errors = append(errors, err)
+			}
+		case tpe == typeJob:
+			err := d.deleteRunJob(crclient, out, dryRun, manifest)
+			if err != nil {
+				errors = append(errors, err)
+			}
+		}
+	}
+	if len(errors) != 0 {
+		// TODO: is there a good way to report all of the errors?
+		return errors[0]
 	}
+	return nil
+}
+
+func (d *Deployer) deleteRunService(crclient *run.APIService, out io.Writer, dryRun bool, manifest []byte) error {
 	service := &run.Service{}
-	if err := k8syaml.Unmarshal(manifests[0], service); err != nil {
+	if err := k8syaml.Unmarshal(manifest, service); err != nil {
 		return sErrors.NewError(fmt.Errorf("unable to unmarshal Cloud Run Service config"), &proto.ActionableErr{
 			Message: err.Error(),
 			ErrCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
@@ -360,20 +393,76 @@ func (d *Deployer) deleteRunService(ctx context.Context, out io.Writer, dryRun b
 		output.Yellow.Fprintln(out, sName)
 		return nil
 	}
-	crclient, err := run.NewService(ctx, append(gcp.ClientOptions(ctx), d.clientOptions...)...)
+
+	delCall := crclient.Projects.Locations.Services.Delete(sName)
+	_, err := delCall.Do()
 	if err != nil {
-		return sErrors.NewError(fmt.Errorf("unable to create Cloud Run Client"), &proto.ActionableErr{
+		return sErrors.NewError(fmt.Errorf("unable to delete Cloud Run Service"), &proto.ActionableErr{
 			Message: err.Error(),
-			ErrCode: proto.StatusCode_DEPLOY_GET_CLOUD_RUN_CLIENT_ERR,
+			ErrCode: proto.StatusCode_DEPLOY_CLOUD_RUN_DELETE_SERVICE_ERR,
 		})
 	}
-	delCall := crclient.Projects.Locations.Services.Delete(sName)
-	_, err = delCall.Do()
+	return nil
+}
+
+func (d *Deployer) deleteRunJob(crclient *run.APIService, out io.Writer, dryRun bool, manifest []byte) error {
+	job := &run.Job{}
+	if err := k8syaml.Unmarshal(manifest, job); err != nil {
+		return sErrors.NewError(fmt.Errorf("unable to unmarshal Cloud Run Service config"), &proto.ActionableErr{
+			Message: err.Error(),
+			ErrCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
+		})
+	}
+	var projectID string
+	switch {
+	case d.Project != "":
+		projectID = d.Project
+	case job.Metadata.Namespace != "":
+		projectID = job.Metadata.Namespace
+	default:
+		// no project specified, we don't know what to delete.
+		return sErrors.NewError(fmt.Errorf("unable to determine Google Cloud Project"), &proto.ActionableErr{
+			Message: "No Google Cloud Project found in Cloud Run manifest or Skaffold Manifest.",
+			ErrCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
+		})
+	}
+	parent := fmt.Sprintf("namespaces/%s", projectID)
+	sName := fmt.Sprintf("%s/jobs/%s", parent, job.Metadata.Name)
+	if dryRun {
+		output.Yellow.Fprintln(out, sName)
+		return nil
+	}
+
+	delCall := crclient.Namespaces.Jobs.Delete(sName)
+	_, err := delCall.Do()
 	if err != nil {
-		return sErrors.NewError(fmt.Errorf("unable to delete Cloud Run Service"), &proto.ActionableErr{
+		return sErrors.NewError(fmt.Errorf("unable to delete Cloud Run Job"), &proto.ActionableErr{
 			Message: err.Error(),
 			ErrCode: proto.StatusCode_DEPLOY_CLOUD_RUN_DELETE_SERVICE_ERR,
 		})
 	}
 	return nil
 }
+
+func getTypeFromManifest(manifest []byte) (string, error) {
+	resource := &unstructured.Unstructured{}
+	if err := k8syaml.Unmarshal(manifest, resource); err != nil {
+		return "", sErrors.NewError(fmt.Errorf("unable to unmarshal Cloud Run Service config"), &proto.ActionableErr{
+			Message: err.Error(),
+			ErrCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
+		})
+	}
+	switch {
+	case resource.GetAPIVersion() == "serving.knative.dev/v1" && resource.GetKind() == "Service":
+		return typeService, nil
+	case resource.GetAPIVersion() == "run.googleapis.com/v1" && resource.GetKind() == "Job":
+		return typeJob, nil
+	default:
+		err := sErrors.NewError(fmt.Errorf("unsupported Kind for Cloud Run Deployer: %s/%s", resource.GetAPIVersion(), resource.GetKind()),
+			&proto.ActionableErr{
+				Message: "Kind is not supported",
+				ErrCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
+			})
+		return "", err
+	}
+}
diff --git a/pkg/skaffold/deploy/cloudrun/formatter.go b/pkg/skaffold/deploy/cloudrun/formatter.go
new file mode 100644
index 000000000..037dff881
--- /dev/null
+++ b/pkg/skaffold/deploy/cloudrun/formatter.go
@@ -0,0 +1,39 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cloudrun
+
+import (
+	"io"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
+)
+
+type LogFormatter struct {
+	prefix      string
+	outputColor output.Color
+}
+
+func (formatter *LogFormatter) Name() string {
+	return formatter.prefix
+}
+func (formatter *LogFormatter) PrintLine(out io.Writer, line string) {
+	if output.IsColorable(out) {
+		formatter.outputColor.Fprintf(out, "[%s] %s", formatter.prefix, line)
+	} else {
+		output.Default.Fprintf(out, "[%s] %s", formatter.prefix, line)
+	}
+}
diff --git a/pkg/skaffold/deploy/cloudrun/log.go b/pkg/skaffold/deploy/cloudrun/log.go
new file mode 100644
index 000000000..686bf78a7
--- /dev/null
+++ b/pkg/skaffold/deploy/cloudrun/log.go
@@ -0,0 +1,223 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cloudrun
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"sync/atomic"
+	"time"
+
+	"golang.org/x/sync/singleflight"
+
+	sErrors "github.com/GoogleContainerTools/skaffold/pkg/skaffold/errors"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
+	"github.com/GoogleContainerTools/skaffold/proto/v1"
+)
+
+type logTailer interface {
+	Start(ctx context.Context, out io.Writer) error
+	Stop()
+}
+
+type logTailerResource struct {
+	name      RunResourceName
+	cancel    context.CancelFunc
+	isTailing bool
+	cmd       *exec.Cmd
+	formatter LogFormatter
+}
+
+type loggerTracker struct {
+	resources map[RunResourceName]*logTailerResource
+}
+
+type LogAggregator struct {
+	singleRun     singleflight.Group
+	resources     *loggerTracker
+	logTailers    []logTailer
+	muted         int32
+	label         string
+	serviceColors map[string]output.Color
+}
+
+func (r *LogAggregator) Mute() {
+	if r == nil {
+		// Logs are not activated.
+		return
+	}
+
+	atomic.StoreInt32(&r.muted, 1)
+}
+
+func (r *LogAggregator) Unmute() {
+	if r == nil {
+		// Logs are not activated.
+		return
+	}
+
+	atomic.StoreInt32(&r.muted, 0)
+}
+
+func (r *LogAggregator) SetSince(time time.Time) {
+}
+
+func (r *LogAggregator) RegisterArtifacts(artifacts []graph.Artifact) {
+}
+
+func NewLoggerAggregator(cfg Config, label string) *LogAggregator {
+	var logTailers []logTailer
+	resources := &loggerTracker{}
+	if cfg.Tail() {
+		logTailers = append(logTailers, &runLogTailer{resources: resources})
+	}
+	a := &LogAggregator{logTailers: logTailers, resources: resources, label: label, singleRun: singleflight.Group{}, serviceColors: make(map[string]output.Color)}
+	return a
+}
+
+func (r *LogAggregator) AddResource(resource RunResourceName) {
+	if r.resources.resources == nil {
+		r.resources.resources = make(map[RunResourceName]*logTailerResource)
+	}
+	if _, present := r.resources.resources[resource]; !present {
+		r.addServiceColor(resource.Service)
+		r.resources.resources[resource] = &logTailerResource{name: resource, formatter: LogFormatter{resource.Service, r.serviceColors[resource.Service]}}
+	} else {
+		r.resources.resources[resource].isTailing = true
+	}
+}
+
+func (r *LogAggregator) addServiceColor(serviceName string) {
+	if _, present := r.serviceColors[serviceName]; !present {
+		r.serviceColors[serviceName] = output.DefaultColorCodes[(len(r.serviceColors))&len(output.DefaultColorCodes)]
+	}
+}
+
+func (r *LogAggregator) Start(ctx context.Context, out io.Writer) error {
+	if r == nil {
+		return nil
+	}
+	_, err, _ := r.singleRun.Do(r.label, func() (interface{}, error) {
+		return struct{}{}, r.start(ctx, out)
+	})
+	return err
+}
+
+func (r *LogAggregator) start(ctx context.Context, out io.Writer) error {
+	for _, logTail := range r.logTailers {
+		if err := logTail.Start(ctx, out); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r *LogAggregator) Stop() {
+	for _, logTailer := range r.logTailers {
+		logTailer.Stop()
+	}
+}
+
+type runLogTailer struct {
+	resources *loggerTracker
+}
+
+func (r *runLogTailer) Start(ctx context.Context, out io.Writer) error {
+	if !gcloudInstalled() {
+		output.Red.Fprintln(out, "gcloud not found on path. Unable to set up Cloud Run port forwarding")
+		return sErrors.NewError(fmt.Errorf("gcloud not found"), &proto.ActionableErr{ErrCode: proto.StatusCode_LOG_STREAMING_RUN_GCLOUD_NOT_FOUND})
+	}
+	if r.resources.resources == nil {
+		return nil
+	}
+	go func() {
+		for _, resource := range r.resources.resources {
+			if !resource.isTailing {
+				cctx, cancel := context.WithCancel(ctx)
+				cmd := exec.CommandContext(cctx, "gcloud", getGcloudTailArgs(resource.name)...)
+				cmd.Env = os.Environ()
+				// gcloud uses buffered stream by default
+				cmd.Env = append(cmd.Env, "PYTHONUNBUFFERED=1") // gcloud defaults streaming output as buffered
+				r, w := io.Pipe()
+				cmd.Stderr = w
+				cmd.Stdout = w
+				resource.cancel = cancel
+				if err := cmd.Start(); err != nil {
+					output.Red.Fprintf(out, "failed to start log streaming on service %s\n", resource.name.Service)
+				}
+				if err := streamLog(ctx, out, r, resource.formatter); err != nil {
+					output.Red.Fprintf(out, "log streaming failed: %s\n", err)
+				}
+				go func() {
+					if err := cmd.Wait(); err != nil {
+						output.Red.Fprintf(out, "terminated\n")
+					}
+				}()
+				resource.isTailing = true
+				resource.cmd = cmd
+			}
+		}
+	}()
+	return nil
+}
+
+func streamLog(ctx context.Context, out io.Writer, rc io.Reader, formatter LogFormatter) error {
+	reader := bufio.NewReader(rc)
+	for {
+		select {
+		case <-ctx.Done():
+			output.Yellow.Fprintln(out, "log streaming was interrupted")
+			return nil
+		default:
+			line, err := reader.ReadString('\n')
+			if err == io.EOF {
+				return nil
+			}
+			if err != nil {
+				output.Red.Fprintf(out, "error reading bytes form log streaming: %v", err)
+				return err
+			}
+			formatter.PrintLine(out, line)
+		}
+	}
+}
+
+func (r *runLogTailer) Stop() {
+	for _, resource := range r.resources.resources {
+		if resource.cancel != nil {
+			if resource.cmd != nil {
+				if err := resource.cmd.Process.Signal(os.Interrupt); err != nil {
+					// signaling didn't work, force cancel
+					resource.cancel()
+				}
+			} else {
+				resource.cancel()
+			}
+			resource.cancel = nil
+			resource.cmd = nil
+		}
+	}
+}
+
+func getGcloudTailArgs(resource RunResourceName) []string {
+	return []string{"alpha", "run", "services", "logs", "tail", resource.Service, "--project", resource.Project, "--region", resource.Region}
+}
diff --git a/pkg/skaffold/kubernetes/manifest/images.go b/pkg/skaffold/kubernetes/manifest/images.go
index 8a8ee85a9..b8bb4e7f8 100644
--- a/pkg/skaffold/kubernetes/manifest/images.go
+++ b/pkg/skaffold/kubernetes/manifest/images.go
@@ -19,6 +19,7 @@ package manifest
 import (
 	"context"
 	"strconv"
+	"strings"
 
 	apimachinery "k8s.io/apimachinery/pkg/runtime/schema"
 
@@ -197,9 +198,39 @@ func (r *imageReplacer) Visit(gk apimachinery.GroupKind, navpath string, o map[s
 
 	parsed, err := docker.ParseReference(image)
 	if err != nil {
-		log.Entry(context.TODO()).Debugf("Couldn't parse image [%s]: %s", image, err.Error())
-		return false
+		// this is a hack to properly support `imageStrategy=helm` & `imageStrategy=helm+explicitRegistry` from Skaffold v1.X.X
+		// which have the form:
+		// helm - image: "{{.Values.image.repository}}:{{.Values.image.tag}}"
+		// helm+explicitRegistry - image: "{{.Values.image.registry}}/{{.Values.image.repository}}:{{.Values.image.tag}}"
+		// when the artifact name has a fully qualified path - gcr.io/example-repo/skaffold-helm-image
+		// works by looking for intermediate helm replacement of the form image:
+		// helm - image: <artifactName>/<artifactName>:<artifactName>
+		// helm+explicitRegistry - image: <artifactName>:<artifactName>
+		// and treating that as just <artifact> by modifying the parsed representation.
+		tagSplit := strings.Split(image, ":")
+		if len(tagSplit) == 2 && strings.HasPrefix(image, tagSplit[1]) {
+			if _, present := r.tagsByImageName[tagSplit[1]]; present {
+				parsed = &docker.ImageReference{
+					BaseName: tagSplit[1],
+				}
+			}
+		} else {
+			log.Entry(context.TODO()).Debugf("Couldn't parse image [%s]: %s", image, err.Error())
+			return false
+		}
+	}
+	// this is a hack to properly support `imageStrategy=helm+explicitRegistry` from Skaffold v1.X.X which has the form:
+	// image: "{{.Values.image.registry}}/{{.Values.image.repository}}:{{.Values.image.tag}}"
+	// when the artifact name is not fully qualified - skaffold-helm-image
+	// works by looking for intermediate helm replacement of the form image:
+	// image: <artifactName>/<artifactName>:<artifactName>
+	// and treating that as just <artifact> by modifying the parsed representation.
+	if parsed.Domain != "" && parsed.Domain == parsed.Repo {
+		if _, present := r.tagsByImageName[parsed.Repo]; present {
+			parsed.BaseName = parsed.Repo
+		}
 	}
+
 	if imageName, tag, selected := r.selector(r.tagsByImageName, parsed); selected {
 		r.found[imageName] = true
 		o[k] = tag
diff --git a/pkg/skaffold/schema/v2beta29/upgrade.go b/pkg/skaffold/schema/v2beta29/upgrade.go
index cc843b540..02c06b303 100644
--- a/pkg/skaffold/schema/v2beta29/upgrade.go
+++ b/pkg/skaffold/schema/v2beta29/upgrade.go
@@ -118,20 +118,11 @@ func upgradeOnePipeline(oldPipeline, newPipeline interface{}) error {
 			if oldPL.Deploy.HelmDeploy.Releases[i].ImageStrategy.HelmConventionConfig != nil {
 				// is 'helm' imageStrategy
 				for k, v := range aos {
-					if k == "image" {
-						if svts == nil {
-							svts = map[string]string{}
-						}
-						svts["image.tag"] = "{{.IMAGE_TAG}}@{{.IMAGE_DIGEST}}"
-						svts["image.repository"] = "{{.IMAGE_REPO}}"
-						if oldPL.Deploy.HelmDeploy.Releases[i].ImageStrategy.HelmConventionConfig.ExplicitRegistry {
-							// is 'helm' imageStrategy + explicitRegistry
-							svts["image.registry"] = "{{.IMAGE_DOMAIN}}"
-							svts["image.repository"] = "{{.IMAGE_REPO_NO_DOMAIN}}"
-						}
-						continue
+					svs[k+".repository"] = v
+					svs[k+".tag"] = v
+					if oldPL.Deploy.HelmDeploy.Releases[i].ImageStrategy.HelmConventionConfig.ExplicitRegistry {
+						svs[k+".registry"] = v
 					}
-					svs[k] = v
 				}
 			} else {
 				// is 'fqn' imageStrategy
diff --git a/proto/enums/enums.pb.go b/proto/enums/enums.pb.go
index 3e7fa4f8a..858662ffb 100644
--- a/proto/enums/enums.pb.go
+++ b/proto/enums/enums.pb.go
@@ -886,6 +886,7 @@ const (
 	StatusCode_INSPECT_PROFILE_NOT_FOUND_ERR      StatusCode = 1304
 	StatusCode_PORT_FORWARD_RUN_GCLOUD_NOT_FOUND  StatusCode = 1601
 	StatusCode_PORT_FORWARD_RUN_PROXY_START_ERROR StatusCode = 1602
+	StatusCode_LOG_STREAM_RUN_GCLOUD_NOT_FOUND    StatusCode = 1603
 )
 
 // Enum value maps for StatusCode.
diff --git a/proto/v1/skaffold.pb.go b/proto/v1/skaffold.pb.go
index 5f54fbf6b..5a9d7ba7f 100644
--- a/proto/v1/skaffold.pb.go
+++ b/proto/v1/skaffold.pb.go
@@ -307,6 +307,7 @@ const StatusCode_INSPECT_BUILD_ENV_INCORRECT_TYPE_ERR = enums.StatusCode_INSPECT
 const StatusCode_INSPECT_PROFILE_NOT_FOUND_ERR = enums.StatusCode_INSPECT_PROFILE_NOT_FOUND_ERR
 const StatusCode_PORT_FORWARD_RUN_GCLOUD_NOT_FOUND = enums.StatusCode_PORT_FORWARD_RUN_GCLOUD_NOT_FOUND
 const StatusCode_PORT_FORWARD_RUN_PROXY_START_ERROR = enums.StatusCode_PORT_FORWARD_RUN_PROXY_START_ERROR
+const StatusCode_LOG_STREAMING_RUN_GCLOUD_NOT_FOUND = enums.StatusCode_LOG_STREAM_RUN_GCLOUD_NOT_FOUND
 
 var StatusCode_name = enums.StatusCode_name
 var StatusCode_value = enums.StatusCode_value


Test patch:
diff --git a/examples/custom-tests/skaffold.yaml b/examples/custom-tests/skaffold.yaml
index b8ca98003..fc4be9bfb 100644
--- a/examples/custom-tests/skaffold.yaml
+++ b/examples/custom-tests/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/examples/grpc-e2e-tests/skaffold.yaml b/examples/grpc-e2e-tests/skaffold.yaml
index 198be875e..8bd59dcb0 100644
--- a/examples/grpc-e2e-tests/skaffold.yaml
+++ b/examples/grpc-e2e-tests/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 metadata:
   name: visitor-counter-e2e
diff --git a/examples/structure-tests/skaffold.yaml b/examples/structure-tests/skaffold.yaml
index 6da269ce4..3cba01924 100644
--- a/examples/structure-tests/skaffold.yaml
+++ b/examples/structure-tests/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v3
+apiVersion: skaffold/v4beta1
 kind: Config
 build:
   artifacts:
diff --git a/integration/apply_test.go b/integration/apply_test.go
index b3b0693c8..bb150d5d0 100644
--- a/integration/apply_test.go
+++ b/integration/apply_test.go
@@ -35,7 +35,7 @@ func TestDiagnoseRenderApply(t *testing.T) {
 		MarkIntegrationTest(t.T, NeedsGcp)
 		ns, client := SetupNamespace(t.T)
 
-		out := skaffold.Diagnose("--yaml-only").InDir("examples/multi-config-microservices").RunOrFailOutput(t.T)
+		out := skaffold.Diagnose("--yaml-only").InDir("testdata/multi-config-pods").RunOrFailOutput(t.T)
 
 		tmpDir := testutil.NewTempDir(t.T)
 		tmpDir.Chdir()
@@ -47,11 +47,10 @@ func TestDiagnoseRenderApply(t *testing.T) {
 
 		skaffold.Apply("render.yaml", "-f", "skaffold-diagnose.yaml").InNs(ns.Name).RunOrFail(t.T)
 
-		depApp := client.GetDeployment("leeroy-app")
-		t.CheckNotNil(depApp)
-
-		depWeb := client.GetDeployment("leeroy-web")
-		t.CheckNotNil(depWeb)
+		pod1 := client.GetPod("module1")
+		t.CheckNotNil(pod1)
+		pod2 := client.GetPod("module2")
+		t.CheckNotNil(pod2)
 	})
 }
 
@@ -91,10 +90,13 @@ func TestApplyStatusCheckFailure(t *testing.T) {
 			description: "status check for statefulset resources",
 			profile:     "statefulset",
 		},
-		{
-			description: "status check for config connector resources",
-			profile:     "configconnector",
-		},
+		//{
+		// config connector resource status doesn't distinguish between resource that is making progress towards reconciling from one that is doomed.
+		// This is tracked in b/187759279 internally. The test currently passes due to status check timeout, it's not what we want to test, hence
+		// commenting this out at the moment.
+		// description: "status check for config connector resources",
+		// profile:     "configconnector",
+		// },
 		{
 			description: "status check for standalone pods",
 			profile:     "pod",
diff --git a/integration/build_test.go b/integration/build_test.go
index 0c681e1a7..25e9ffbd6 100644
--- a/integration/build_test.go
+++ b/integration/build_test.go
@@ -153,8 +153,6 @@ func TestBuildWithWithPlatform(t *testing.T) {
 }
 
 func TestBuildWithMultiPlatforms(t *testing.T) {
-	MarkIntegrationTest(t, NeedsGcp)
-
 	tests := []struct {
 		description       string
 		dir               string
@@ -172,6 +170,7 @@ func TestBuildWithMultiPlatforms(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.description, func(t *testing.T) {
+			MarkIntegrationTest(t, NeedsGcp)
 			tmpfile := testutil.TempFile(t, "", []byte{})
 			args := append(test.args, "--file-output", tmpfile)
 			skaffold.Build(args...).InDir(test.dir).RunOrFail(t)
@@ -186,7 +185,6 @@ func TestBuildWithMultiPlatforms(t *testing.T) {
 
 // TestExpectedBuildFailures verifies that `skaffold build` fails in expected ways
 func TestExpectedBuildFailures(t *testing.T) {
-	MarkIntegrationTest(t, NeedsGcp)
 	if !jib.JVMFound(context.Background()) {
 		t.Fatal("test requires Java VM")
 	}
@@ -207,6 +205,7 @@ func TestExpectedBuildFailures(t *testing.T) {
 	}
 	for _, test := range tests {
 		t.Run(test.description, func(t *testing.T) {
+			MarkIntegrationTest(t, NeedsGcp)
 			if out, err := skaffold.Build(test.args...).InDir(test.dir).RunWithCombinedOutput(t); err == nil {
 				t.Fatal("expected build to fail")
 			} else if !strings.Contains(string(out), test.expected) {
diff --git a/integration/deploy_cloudrun_test.go b/integration/deploy_cloudrun_test.go
index 58e6e590d..1aa3a03dd 100644
--- a/integration/deploy_cloudrun_test.go
+++ b/integration/deploy_cloudrun_test.go
@@ -29,7 +29,6 @@ import (
 
 func TestDeployCloudRun(t *testing.T) {
 	MarkIntegrationTest(t, NeedsGcp)
-
 	// Other integration tests run with the --default-repo option.
 	// This one explicitly specifies the full image name.
 	skaffold.Deploy().InDir("testdata/deploy-cloudrun").RunOrFail(t)
diff --git a/integration/deploy_test.go b/integration/deploy_test.go
index c3410dc92..26d6afb40 100644
--- a/integration/deploy_test.go
+++ b/integration/deploy_test.go
@@ -38,29 +38,10 @@ func TestBuildDeploy(t *testing.T) {
 
 	ns, client := SetupNamespace(t)
 
-	outputBytes := skaffold.Build("--quiet", "--platform=linux/arm64,linux/amd64").InDir("examples/microservices").InNs(ns.Name).RunOrFailOutput(t)
+	outputBytes := skaffold.Build("--quiet", "--platform=linux/arm64,linux/amd64").InDir("examples/nodejs").InNs(ns.Name).RunOrFailOutput(t)
 	// Parse the Build Output
 	buildArtifacts, err := flags.ParseBuildOutput(outputBytes)
 	failNowIfError(t, err)
-	if len(buildArtifacts.Builds) != 3 {
-		t.Fatalf("expected 3 artifacts to be built, but found %d", len(buildArtifacts.Builds))
-	}
-
-	var webTag, appTag string
-	for _, a := range buildArtifacts.Builds {
-		if a.ImageName == "leeroy-web" {
-			webTag = a.Tag
-		}
-		if a.ImageName == "leeroy-app" {
-			appTag = a.Tag
-		}
-	}
-	if webTag == "" {
-		t.Fatalf("expected to find a tag for leeroy-web but found none %s", webTag)
-	}
-	if appTag == "" {
-		t.Fatalf("expected to find a tag for leeroy-app but found none %s", appTag)
-	}
 
 	tmpDir := testutil.NewTempDir(t)
 	buildOutputFile := tmpDir.Path("build.out")
@@ -68,13 +49,10 @@ func TestBuildDeploy(t *testing.T) {
 
 	// Run Deploy using the build output
 	// See https://github.com/GoogleContainerTools/skaffold/issues/2372 on why status-check=false
-	skaffold.Deploy("--build-artifacts", buildOutputFile, "--status-check=false").InDir("examples/microservices").InNs(ns.Name).RunOrFail(t)
-
-	depApp := client.GetDeployment("leeroy-app")
-	testutil.CheckDeepEqual(t, appTag, depApp.Spec.Template.Spec.Containers[0].Image)
+	skaffold.Deploy("--build-artifacts", buildOutputFile, "--status-check=false").InDir("examples/nodejs").InNs(ns.Name).RunOrFail(t)
 
-	depWeb := client.GetDeployment("leeroy-web")
-	testutil.CheckDeepEqual(t, webTag, depWeb.Spec.Template.Spec.Containers[0].Image)
+	depApp := client.GetDeployment("node")
+	testutil.CheckDeepEqual(t, buildArtifacts.Builds[0].Tag, depApp.Spec.Template.Spec.Containers[0].Image)
 }
 
 func TestDeploy(t *testing.T) {
diff --git a/integration/helm_test.go b/integration/helm_test.go
index 78b646fc4..0dd234912 100644
--- a/integration/helm_test.go
+++ b/integration/helm_test.go
@@ -18,6 +18,8 @@ package integration
 
 import (
 	"fmt"
+	"os"
+	"path/filepath"
 	"testing"
 
 	"github.com/GoogleContainerTools/skaffold/integration/skaffold"
@@ -38,3 +40,48 @@ func TestHelmDeploy(t *testing.T) {
 
 	skaffold.Delete().InDir("testdata/helm").InNs(ns.Name).WithEnv(env).RunOrFail(t)
 }
+
+func TestDevHelmMultiConfig(t *testing.T) {
+	MarkIntegrationTest(t, CanRunWithoutGcp)
+	var tests = []struct {
+		description  string
+		dir          string
+		args         []string
+		deployments  []string
+		pods         []string
+		env          []string
+		targetLogOne string
+		targetLogTwo string
+	}{
+		{
+			description:  "helm-multi-config",
+			dir:          "testdata/helm-multi-config/skaffold",
+			deployments:  []string{"app1", "app2"},
+			targetLogOne: "app1",
+			targetLogTwo: "app2",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.description, func(t *testing.T) {
+			if test.targetLogOne == "" || test.targetLogTwo == "" {
+				t.SkipNow()
+			}
+			if test.dir == emptydir {
+				err := os.MkdirAll(filepath.Join(test.dir, "emptydir"), 0755)
+				t.Log("Creating empty directory")
+				if err != nil {
+					t.Errorf("Error creating empty dir: %s", err)
+				}
+			}
+
+			ns, _ := SetupNamespace(t)
+
+			out := skaffold.Run(test.args...).InDir(test.dir).InNs(ns.Name).WithEnv(test.env).RunLive(t)
+			defer skaffold.Delete().InDir(test.dir).InNs(ns.Name).WithEnv(test.env).Run(t)
+
+			WaitForLogs(t, out, test.targetLogOne)
+			WaitForLogs(t, out, test.targetLogTwo)
+		})
+	}
+}
diff --git a/integration/multiplatform_test.go b/integration/multiplatform_test.go
index 5eb2f9de6..2b9fda980 100644
--- a/integration/multiplatform_test.go
+++ b/integration/multiplatform_test.go
@@ -38,7 +38,6 @@ const (
 )
 
 func TestMultiPlatformWithRun(t *testing.T) {
-	MarkIntegrationTest(t, NeedsGcp)
 	isRunningInHybridCluster := os.Getenv("GKE_CLUSTER_NAME") == hybridClusterName
 	type image struct {
 		name string
@@ -73,6 +72,7 @@ func TestMultiPlatformWithRun(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.description, func(t *testing.T) {
+			MarkIntegrationTest(t, NeedsGcp)
 			platforms := platformsCliValue(test.expectedPlatforms)
 			ns, client := SetupNamespace(t)
 			tag := fmt.Sprintf("%s-%s", test.tag, uuid.New().String())
@@ -95,7 +95,6 @@ func TestMultiPlatformWithRun(t *testing.T) {
 }
 
 func TestMultiplatformWithDevAndDebug(t *testing.T) {
-	MarkIntegrationTest(t, NeedsGcp)
 	const platformsExpectedInNodeAffinity = 1
 	const platformsExpectedInCreatedImage = 1
 	isRunningInHybridCluster := os.Getenv("GKE_CLUSTER_NAME") == hybridClusterName
@@ -155,6 +154,7 @@ func TestMultiplatformWithDevAndDebug(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.description, func(t *testing.T) {
+			MarkIntegrationTest(t, NeedsGcp)
 			platforms := platformsCliValue(test.expectedPlatforms)
 			tag := fmt.Sprintf("%s-%s", test.tag, uuid.New().String())
 			ns, client := SetupNamespace(t)
@@ -193,7 +193,6 @@ func TestMultiplatformWithDevAndDebug(t *testing.T) {
 }
 
 func TestMultiplatformWithDeploy(t *testing.T) {
-	MarkIntegrationTest(t, NeedsGcp)
 	isRunningInHybridCluster := os.Getenv("GKE_CLUSTER_NAME") == hybridClusterName
 	type image struct {
 		name string
@@ -228,6 +227,7 @@ func TestMultiplatformWithDeploy(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.description, func(t *testing.T) {
+			MarkIntegrationTest(t, NeedsGcp)
 			tmpfile := testutil.TempFile(t, "", []byte{})
 			tag := fmt.Sprintf("%s-%s", test.tag, uuid.New().String())
 			platforms := platformsCliValue(test.expectedPlatforms)
diff --git a/integration/run_test.go b/integration/run_test.go
index b9b733621..0bc4cd4d2 100644
--- a/integration/run_test.go
+++ b/integration/run_test.go
@@ -280,7 +280,7 @@ func TestRunTailTolerateFailuresUntilDeadline(t *testing.T) {
 
 			args := append(test.args, "--tail")
 			out := skaffold.Run(args...).InDir(test.dir).InNs(ns.Name).WithEnv(test.env).RunLive(t)
-			defer skaffold.Delete().InDir(test.dir).WithEnv(test.env).RunOrFail(t)
+			defer skaffold.Delete().InDir(test.dir).InNs(ns.Name).WithEnv(test.env).Run(t)
 			WaitForLogs(t, out, test.targetLogOne)
 			WaitForLogs(t, out, test.targetLogTwo)
 		})
@@ -316,8 +316,6 @@ func TestRunRenderOnly(t *testing.T) {
 }
 
 func TestRunGCPOnly(t *testing.T) {
-	MarkIntegrationTest(t, NeedsGcp)
-
 	tests := []struct {
 		description string
 		dir         string
@@ -342,9 +340,9 @@ func TestRunGCPOnly(t *testing.T) {
 		},
 		{
 			description: "Google Cloud Build with source artifact dependencies",
-			dir:         "examples/microservices",
+			dir:         "testdata/multi-config-pods",
 			args:        []string{"-p", "gcb"},
-			deployments: []string{"leeroy-app", "leeroy-web"},
+			pods:        []string{"module1", "module2"},
 		},
 		// {
 		//	description: "Google Cloud Build with Kaniko",
@@ -395,6 +393,7 @@ func TestRunGCPOnly(t *testing.T) {
 			continue // buildpacks doesn't support arm64 builds, so skip run on these clusters
 		}
 		t.Run(test.description, func(t *testing.T) {
+			MarkIntegrationTest(t, NeedsGcp)
 			ns, client := SetupNamespace(t)
 
 			test.args = append(test.args, "--tag", uuid.New().String())
diff --git a/integration/testdata/helm-multi-config/Makefile b/integration/testdata/helm-multi-config/Makefile
new file mode 100644
index 000000000..5c5203028
--- /dev/null
+++ b/integration/testdata/helm-multi-config/Makefile
@@ -0,0 +1,22 @@
+build:
+	cd skaffold; \
+	DOCKER_HOST="unix:///Users/$$(whoami)/.docker/run/docker.sock" \
+	skaffold build \
+		--verbosity info \
+		--default-repo docker.io/bringes
+
+dev:
+	cd skaffold; \
+	DOCKER_HOST="unix:///Users/$$(whoami)/.docker/run/docker.sock" \
+	skaffold dev \
+		--verbosity info \
+		--default-repo docker.io/bringes \
+		--kubeconfig ../kubeconfig.yml
+
+render:
+	cd skaffold; \
+	DOCKER_HOST="unix:///Users/$$(whoami)/.docker/run/docker.sock" \
+	skaffold render \
+		--verbosity info \
+		--default-repo docker.io/bringes \
+        -vdebug
diff --git a/integration/testdata/helm-multi-config/app1/Dockerfile b/integration/testdata/helm-multi-config/app1/Dockerfile
new file mode 100644
index 000000000..74bea5194
--- /dev/null
+++ b/integration/testdata/helm-multi-config/app1/Dockerfile
@@ -0,0 +1,2 @@
+FROM alpine
+ENTRYPOINT echo "app1" && tail -f /dev/null
\ No newline at end of file
diff --git a/integration/testdata/helm-multi-config/app2/Dockerfile b/integration/testdata/helm-multi-config/app2/Dockerfile
new file mode 100644
index 000000000..98a5f78de
--- /dev/null
+++ b/integration/testdata/helm-multi-config/app2/Dockerfile
@@ -0,0 +1,2 @@
+FROM alpine
+ENTRYPOINT echo "app2" && tail -f /dev/null
\ No newline at end of file
diff --git a/integration/testdata/helm-multi-config/charts/app/.helmignore b/integration/testdata/helm-multi-config/charts/app/.helmignore
new file mode 100644
index 000000000..0e8a0eb36
--- /dev/null
+++ b/integration/testdata/helm-multi-config/charts/app/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/integration/testdata/helm-multi-config/charts/app/Chart.yaml b/integration/testdata/helm-multi-config/charts/app/Chart.yaml
new file mode 100644
index 000000000..b2beb1917
--- /dev/null
+++ b/integration/testdata/helm-multi-config/charts/app/Chart.yaml
@@ -0,0 +1,24 @@
+apiVersion: v2
+name: app
+description: A Helm chart for Kubernetes
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 0.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+# It is recommended to use it with quotes.
+appVersion: "1.16.0"
diff --git a/integration/testdata/helm-multi-config/charts/app/templates/NOTES.txt b/integration/testdata/helm-multi-config/charts/app/templates/NOTES.txt
new file mode 100644
index 000000000..01a7d7b17
--- /dev/null
+++ b/integration/testdata/helm-multi-config/charts/app/templates/NOTES.txt
@@ -0,0 +1,22 @@
+1. Get the application URL by running these commands:
+{{- if .Values.ingress.enabled }}
+{{- range $host := .Values.ingress.hosts }}
+  {{- range .paths }}
+  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
+  {{- end }}
+{{- end }}
+{{- else if contains "NodePort" .Values.service.type }}
+  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "app.fullname" . }})
+  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+  echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "LoadBalancer" .Values.service.type }}
+     NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+           You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "app.fullname" . }}'
+  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "app.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
+  echo http://$SERVICE_IP:{{ .Values.service.port }}
+{{- else if contains "ClusterIP" .Values.service.type }}
+  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
+  export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
+  echo "Visit http://127.0.0.1:8080 to use your application"
+  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
+{{- end }}
diff --git a/integration/testdata/helm-multi-config/charts/app/templates/_helpers.tpl b/integration/testdata/helm-multi-config/charts/app/templates/_helpers.tpl
new file mode 100644
index 000000000..6e13b3a0d
--- /dev/null
+++ b/integration/testdata/helm-multi-config/charts/app/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "app.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | 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).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "app.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "app.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "app.labels" -}}
+helm.sh/chart: {{ include "app.chart" . }}
+{{ include "app.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "app.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "app.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "app.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "app.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/integration/testdata/helm-multi-config/charts/app/templates/deployment.yaml b/integration/testdata/helm-multi-config/charts/app/templates/deployment.yaml
new file mode 100644
index 000000000..ab382043d
--- /dev/null
+++ b/integration/testdata/helm-multi-config/charts/app/templates/deployment.yaml
@@ -0,0 +1,48 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "app.fullname" . }}
+  labels:
+    {{- include "app.labels" . | nindent 4 }}
+spec:
+  {{- if not .Values.autoscaling.enabled }}
+  replicas: {{ .Values.replicaCount }}
+  {{- end }}
+  selector:
+    matchLabels:
+      {{- include "app.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        {{- include "app.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      containers:
+        - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+      {{- with .Values.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
diff --git a/integration/testdata/helm-multi-config/charts/app/values.yaml b/integration/testdata/helm-multi-config/charts/app/values.yaml
new file mode 100644
index 000000000..132701e3a
--- /dev/null
+++ b/integration/testdata/helm-multi-config/charts/app/values.yaml
@@ -0,0 +1,82 @@
+# Default values for app.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+replicaCount: 1
+
+image:
+  repository: dev-registry.internal.io/app
+  pullPolicy: IfNotPresent
+  # Overrides the image tag whose default is the chart appVersion.
+  tag: ""
+
+imagePullSecrets: []
+nameOverride: ""
+fullnameOverride: ""
+
+serviceAccount:
+  # Specifies whether a service account should be created
+  create: true
+  # Annotations to add to the service account
+  annotations: {}
+  # The name of the service account to use.
+  # If not set and create is true, a name is generated using the fullname template
+  name: ""
+
+podAnnotations: {}
+
+podSecurityContext: {}
+  # fsGroup: 2000
+
+securityContext: {}
+  # capabilities:
+  #   drop:
+  #   - ALL
+  # readOnlyRootFilesystem: true
+  # runAsNonRoot: true
+  # runAsUser: 1000
+
+service:
+  type: ClusterIP
+  port: 80
+
+ingress:
+  enabled: false
+  className: ""
+  annotations: {}
+    # kubernetes.io/ingress.class: nginx
+    # kubernetes.io/tls-acme: "true"
+  hosts:
+    - host: chart-example.local
+      paths:
+        - path: /
+          pathType: ImplementationSpecific
+  tls: []
+  #  - secretName: chart-example-tls
+  #    hosts:
+  #      - chart-example.local
+
+resources: {}
+  # We usually recommend not to specify default resources and to leave this as a conscious
+  # choice for the user. This also increases chances charts run on environments with little
+  # resources, such as Minikube. If you do want to specify resources, uncomment the following
+  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+  # limits:
+  #   cpu: 100m
+  #   memory: 128Mi
+  # requests:
+  #   cpu: 100m
+  #   memory: 128Mi
+
+autoscaling:
+  enabled: false
+  minReplicas: 1
+  maxReplicas: 100
+  targetCPUUtilizationPercentage: 80
+  # targetMemoryUtilizationPercentage: 80
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
diff --git a/integration/testdata/helm-multi-config/skaffold/app1/skaffold.yml b/integration/testdata/helm-multi-config/skaffold/app1/skaffold.yml
new file mode 100644
index 000000000..3e7afcc70
--- /dev/null
+++ b/integration/testdata/helm-multi-config/skaffold/app1/skaffold.yml
@@ -0,0 +1,19 @@
+apiVersion: skaffold/v3
+kind: Config
+build:
+  artifacts:
+    - context: ../../app1/
+      image: app1
+  tagPolicy:
+    inputDigest: {}
+deploy:
+  helm: {}
+manifests:
+  helm:
+    releases:
+      - chartPath: ../../charts/app
+        createNamespace: true
+        name: app1
+        setValues:
+          image.repository: app1
+          image.tag: app1
diff --git a/integration/testdata/helm-multi-config/skaffold/app2/skaffold.yml b/integration/testdata/helm-multi-config/skaffold/app2/skaffold.yml
new file mode 100644
index 000000000..062c31102
--- /dev/null
+++ b/integration/testdata/helm-multi-config/skaffold/app2/skaffold.yml
@@ -0,0 +1,19 @@
+apiVersion: skaffold/v3
+kind: Config
+build:
+  artifacts:
+    - context: ../../app2/
+      image: app2
+  tagPolicy:
+    inputDigest: {}
+deploy:
+  helm: {}
+manifests:
+  helm:
+    releases:
+      - chartPath: ../../charts/app
+        createNamespace: true
+        name: app2
+        setValues:
+          image.repository: app2
+          image.tag: app2
\ No newline at end of file
diff --git a/integration/testdata/helm-multi-config/skaffold/skaffold.yml b/integration/testdata/helm-multi-config/skaffold/skaffold.yml
new file mode 100644
index 000000000..abfe2567a
--- /dev/null
+++ b/integration/testdata/helm-multi-config/skaffold/skaffold.yml
@@ -0,0 +1,5 @@
+apiVersion: skaffold/v3
+kind: Config
+requires:
+  - path: ./app1/skaffold.yml
+  - path: ./app2/skaffold.yml
diff --git a/integration/testdata/multi-config-pods/skaffold.yaml b/integration/testdata/multi-config-pods/skaffold.yaml
index 042637817..bfe9203e2 100644
--- a/integration/testdata/multi-config-pods/skaffold.yaml
+++ b/integration/testdata/multi-config-pods/skaffold.yaml
@@ -2,4 +2,8 @@ apiVersion: skaffold/v4beta1
 kind: Config
 requires:
   - path: ./module1
-  - path: ./module2
\ No newline at end of file
+  - path: ./module2
+profiles:
+  - name: gcb
+    build:
+      googleCloudBuild: {}
diff --git a/pkg/skaffold/deploy/cloudrun/accessor_test.go b/pkg/skaffold/deploy/cloudrun/accessor_test.go
index f007fbf7e..6719130b8 100644
--- a/pkg/skaffold/deploy/cloudrun/accessor_test.go
+++ b/pkg/skaffold/deploy/cloudrun/accessor_test.go
@@ -39,6 +39,9 @@ func (t *testAccessConfig) PortForwardOptions() config.PortForwardOptions {
 func (t *testAccessConfig) Mode() config.RunMode {
 	return config.RunModes.Run
 }
+func (t *testAccessConfig) Tail() bool {
+	return true
+}
 func (t *testAccessConfig) PortForwardResources() []*latest.PortForwardResource {
 	return t.forwards
 }
@@ -232,6 +235,7 @@ func TestGcloudFoundServicesForwarder(t *testing.T) {
 			var b bytes.Buffer
 			writer := bufio.NewWriter(&b)
 			err := forwarder.Start(ctx, writer)
+			writer.Flush()
 			output := b.Bytes()
 			if test.expectStatus == proto.StatusCode_OK {
 				if err != nil {
diff --git a/pkg/skaffold/deploy/cloudrun/deploy_test.go b/pkg/skaffold/deploy/cloudrun/deploy_test.go
index 8b6f2d5bc..32fae0346 100644
--- a/pkg/skaffold/deploy/cloudrun/deploy_test.go
+++ b/pkg/skaffold/deploy/cloudrun/deploy_test.go
@@ -393,7 +393,7 @@ func TestDeployRewrites(tOuter *testing.T) {
 	}
 }
 
-func TestCleanup(tOuter *testing.T) {
+func TestCleanupService(tOuter *testing.T) {
 	tests := []struct {
 		description    string
 		toDelete       *run.Service
@@ -480,3 +480,150 @@ func TestCleanup(tOuter *testing.T) {
 		})
 	}
 }
+
+func TestCleanupJob(tOuter *testing.T) {
+	tests := []struct {
+		description    string
+		toDelete       *run.Job
+		defaultProject string
+		region         string
+		expectedPath   string
+		httpErr        int
+	}{
+		{
+			description:    "test cleanup",
+			defaultProject: "testProject",
+			region:         "us-central1",
+			expectedPath:   "/apis/run.googleapis.com/v1/namespaces/testProject/jobs/test-job",
+			toDelete: &run.Job{
+				ApiVersion: "run.googleapis.com/v1",
+				Kind:       "Job",
+				Metadata: &run.ObjectMeta{
+					Name: "test-job",
+				},
+			},
+		},
+		{
+			description:    "test cleanup fails",
+			defaultProject: "testProject",
+			region:         "us-central1",
+			expectedPath:   "/apis/run.googleapis.com/v1/namespaces/testProject/jobs/test-job",
+			toDelete: &run.Job{
+				ApiVersion: "run.googleapis.com/v1",
+				Kind:       "Job",
+				Metadata: &run.ObjectMeta{
+					Name: "test-job",
+				},
+			},
+			httpErr: http.StatusUnauthorized,
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(tOuter, test.description, func(t *testutil.T) {
+			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				if test.httpErr != 0 {
+					http.Error(w, "Expected http error", test.httpErr)
+					return
+				}
+				if r.URL.Path != test.expectedPath {
+					http.Error(w, "unexpected path: "+r.URL.Path, http.StatusNotFound)
+					return
+				}
+				response := &run.Status{}
+				b, err := json.Marshal(response)
+				if err != nil {
+					http.Error(w, "unable to marshal response: "+err.Error(), http.StatusInternalServerError)
+					return
+				}
+				w.Write(b)
+			}))
+			defer ts.Close()
+			deployer, _ := NewDeployer(&runcontext.RunContext{}, &label.DefaultLabeller{}, &latest.CloudRunDeploy{ProjectID: test.defaultProject, Region: test.region}, configName)
+			deployer.clientOptions = append(deployer.clientOptions, option.WithEndpoint(ts.URL), option.WithoutAuthentication())
+			deployer.useGcpOptions = false
+			manifestListByConfig := manifest.NewManifestListByConfig()
+			manifest, _ := json.Marshal(test.toDelete)
+			manifests := [][]byte{manifest}
+			manifestListByConfig.Add(configName, manifests)
+			err := deployer.Cleanup(context.Background(), os.Stderr, false, manifestListByConfig)
+			if test.httpErr == 0 && err != nil {
+				t.Fatalf("Expected success but got err: %v", err)
+			} else if test.httpErr != 0 && err == nil {
+				t.Fatalf("Expected HTTP Error %s but got success", http.StatusText(test.httpErr))
+			}
+		})
+	}
+}
+
+func TestCleanupMultipleResources(tOuter *testing.T) {
+	tests := []struct {
+		description    string
+		toDelete       []interface{}
+		defaultProject string
+		region         string
+		expectedPath   map[string]int
+	}{
+		{
+			description:    "test cleanup",
+			defaultProject: "testProject",
+			region:         "us-central1",
+			expectedPath:   map[string]int{"/apis/run.googleapis.com/v1/namespaces/testProject/jobs/test-job": 1, "/v1/projects/testProject/locations/us-central1/services/test-service": 1},
+			toDelete: []interface{}{&run.Job{
+				ApiVersion: "run.googleapis.com/v1",
+				Kind:       "Job",
+				Metadata: &run.ObjectMeta{
+					Name: "test-job",
+				},
+			},
+				&run.Service{
+					ApiVersion: "serving.knative.dev/v1",
+					Kind:       "Service",
+					Metadata: &run.ObjectMeta{
+						Name: "test-service",
+					},
+				},
+			},
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(tOuter, test.description, func(t *testutil.T) {
+			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				if count, ok := test.expectedPath[r.URL.Path]; !ok || count <= 0 {
+					http.Error(w, "unexpected path: "+r.URL.Path, http.StatusNotFound)
+					return
+				}
+				test.expectedPath[r.URL.Path]--
+				response := &run.Status{}
+				b, err := json.Marshal(response)
+				if err != nil {
+					http.Error(w, "unable to marshal response: "+err.Error(), http.StatusInternalServerError)
+					return
+				}
+				w.Write(b)
+			}))
+			defer ts.Close()
+			deployer, _ := NewDeployer(&runcontext.RunContext{}, &label.DefaultLabeller{}, &latest.CloudRunDeploy{ProjectID: test.defaultProject, Region: test.region}, configName)
+			deployer.clientOptions = append(deployer.clientOptions, option.WithEndpoint(ts.URL), option.WithoutAuthentication())
+			deployer.useGcpOptions = false
+			manifestListByConfig := manifest.NewManifestListByConfig()
+			var manifests [][]byte
+			for _, res := range test.toDelete {
+				manifest, err := json.Marshal(res)
+				if err != nil {
+					t.Fatalf("error marshaling manifest: %v", err)
+				}
+				manifests = append(manifests, manifest)
+			}
+			manifestListByConfig.Add(configName, manifests)
+			err := deployer.Cleanup(context.Background(), os.Stderr, false, manifestListByConfig)
+			if err != nil {
+				t.Fatalf("Expected success but got err: %v", err)
+			}
+			for key, val := range test.expectedPath {
+				if val > 0 {
+					t.Fatalf("Missing expected call for path " + key)
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/skaffold/deploy/cloudrun/formatter_test.go b/pkg/skaffold/deploy/cloudrun/formatter_test.go
new file mode 100644
index 000000000..aea95dce9
--- /dev/null
+++ b/pkg/skaffold/deploy/cloudrun/formatter_test.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cloudrun
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"testing"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
+)
+
+func TestLogFormatter_Name(t *testing.T) {
+	serviceName := "test"
+	formatter := LogFormatter{prefix: serviceName, outputColor: output.Red}
+	if formatter.Name() != serviceName {
+		t.Fatalf("expected service name to be %s but got %s", serviceName, formatter.Name())
+	}
+}
+
+func TestLogFormatter_Printline(t *testing.T) {
+	testLog := "Testing log output"
+	serviceName := "test"
+	var b bytes.Buffer
+	writer := bufio.NewWriter(&b)
+	formatter := LogFormatter{prefix: serviceName, outputColor: output.Red}
+	formatter.PrintLine(writer, testLog)
+	writer.Flush()
+	result := b.Bytes()
+	expectedLogOutput := fmt.Sprintf("[%s] %s", serviceName, testLog)
+	if string(result) != expectedLogOutput {
+		t.Fatalf("expected log output to be %s but got %s", expectedLogOutput, result)
+	}
+}
diff --git a/pkg/skaffold/deploy/cloudrun/log_test.go b/pkg/skaffold/deploy/cloudrun/log_test.go
new file mode 100644
index 000000000..7afdebd9e
--- /dev/null
+++ b/pkg/skaffold/deploy/cloudrun/log_test.go
@@ -0,0 +1,249 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cloudrun
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"testing"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
+	sErrors "github.com/GoogleContainerTools/skaffold/pkg/skaffold/errors"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
+	"github.com/GoogleContainerTools/skaffold/proto/v1"
+)
+
+type testConfig struct {
+	tail bool
+}
+
+func (t *testConfig) Tail() bool {
+	return t.tail
+}
+
+func (t *testConfig) Mode() config.RunMode {
+	return config.RunModes.Run
+}
+
+func (t *testConfig) PortForwardOptions() config.PortForwardOptions {
+	return config.PortForwardOptions{}
+}
+
+func (t *testConfig) PortForwardResources() []*latest.PortForwardResource {
+	return nil
+}
+func newLogTestConfig(tail bool) *testConfig {
+	return &testConfig{tail: tail}
+}
+func TestNewLoggerAggregator(t *testing.T) {
+	tests := []struct {
+		name       string
+		tail       bool
+		numTailers int
+	}{
+		{
+			name:       "tailing is turned on",
+			tail:       true,
+			numTailers: 1,
+		},
+		{
+			name:       "tailing is turned off",
+			tail:       false,
+			numTailers: 0,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			cfg := newLogTestConfig(test.tail)
+			logAggregator := NewLoggerAggregator(cfg, "test")
+			if len(logAggregator.logTailers) != test.numTailers {
+				t.Fatalf("expected %d forwarders, but got %v", test.numTailers, logAggregator.logTailers)
+			}
+		})
+	}
+}
+
+func TestResourcesAddedLogTailers(t *testing.T) {
+	tests := []struct {
+		name      string
+		resources []RunResourceName
+		outputs   []logTailerResource
+	}{
+		{
+			name: "one service with one color",
+			resources: []RunResourceName{
+				{
+					Project: "test-proj",
+					Region:  "test-region",
+					Service: "test-service",
+				},
+			},
+			outputs: []logTailerResource{
+				{
+					name: RunResourceName{
+						Project: "test-proj",
+						Region:  "test-region",
+						Service: "test-service",
+					},
+					formatter: LogFormatter{
+						prefix:      "test-service",
+						outputColor: output.DefaultColorCodes[0],
+					},
+					isTailing: false,
+				},
+			},
+		},
+		{
+			name: "two services with same name",
+			resources: []RunResourceName{
+				{
+					Project: "test-proj",
+					Region:  "test-region",
+					Service: "test-service",
+				},
+				{
+					Project: "test-proj",
+					Region:  "test-region",
+					Service: "test-service",
+				},
+			},
+			outputs: []logTailerResource{
+				{
+					name: RunResourceName{
+						Project: "test-proj",
+						Region:  "test-region",
+						Service: "test-service",
+					},
+					formatter: LogFormatter{
+						prefix:      "test-service",
+						outputColor: output.DefaultColorCodes[0],
+					},
+					isTailing: true,
+				},
+			},
+		},
+		{
+			name: "two services with different names",
+			resources: []RunResourceName{
+				{
+					Project: "test-proj",
+					Region:  "test-region",
+					Service: "test-service",
+				},
+				{
+					Project: "test-proj",
+					Region:  "test-region",
+					Service: "test-service2",
+				},
+			},
+			outputs: []logTailerResource{
+				{
+					name: RunResourceName{
+						Project: "test-proj",
+						Region:  "test-region",
+						Service: "test-service",
+					},
+					formatter: LogFormatter{
+						prefix:      "test-service",
+						outputColor: output.DefaultColorCodes[0],
+					},
+					isTailing: false,
+				},
+				{
+					name: RunResourceName{
+						Project: "test-proj",
+						Region:  "test-region",
+						Service: "test-service2",
+					},
+					formatter: LogFormatter{
+						prefix:      "test-service2",
+						outputColor: output.DefaultColorCodes[1],
+					},
+					isTailing: false,
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			cfg := newLogTestConfig(true)
+			logAggregator := NewLoggerAggregator(cfg, "")
+			for _, resource := range test.resources {
+				logAggregator.AddResource(resource)
+			}
+			if len(test.outputs) != len(logAggregator.resources.resources) {
+				t.Fatalf("Mismatch in expected outputs. Expected %v, got %v", test.outputs, logAggregator.resources.resources)
+			}
+			for _, result := range test.outputs {
+				got, found := logAggregator.resources.resources[result.name]
+				if !found {
+					t.Fatalf("expected to find log resource %v but got nothing", result.name)
+				}
+				if result.name != got.name || result.formatter != got.formatter {
+					t.Fatalf("did not get expected result. Expected %v, got %v", result, got)
+				}
+			}
+		})
+	}
+}
+
+func TestGcloudFoundLogTailing(t *testing.T) {
+	tests := []struct {
+		name         string
+		gcloudFound  bool
+		expectStatus proto.StatusCode
+	}{
+		{
+			name:        "gcloud found",
+			gcloudFound: true,
+		},
+		{
+			name:         "gcloud not found",
+			gcloudFound:  false,
+			expectStatus: proto.StatusCode_LOG_STREAMING_RUN_GCLOUD_NOT_FOUND,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			logTailers := runLogTailer{resources: &loggerTracker{}}
+			gcloudInstalled = func() bool { return test.gcloudFound }
+			ctx := context.Background()
+			var b bytes.Buffer
+			writer := bufio.NewWriter(&b)
+			err := logTailers.Start(ctx, writer)
+			writer.Flush()
+			result := b.Bytes()
+			if test.expectStatus == proto.StatusCode_OK {
+				if err != nil {
+					t.Fatalf("expected success, got error: %v with output %s", err, string(result))
+				}
+			} else {
+				if err == nil {
+					t.Fatalf("expected failure with code %v, got success with output: %s", test.expectStatus, string(result))
+				}
+				sErr := err.(*sErrors.ErrDef)
+				if sErr.StatusCode() != test.expectStatus {
+					t.Fatalf("expected failure with code %v, got code %v with output: %s", test.expectStatus, sErr.StatusCode(), string(result))
+				}
+			}
+		})
+	}
+}