Repository: GoogleContainerTools/skaffold
Pull request #8173 (2022-11-29 21:42:57)

feat: add life cycle hooks to Cloud Run deployer
<!-- Include if applicable: -->
Fixes: #7901

**Description**
This PR adds host hooks to Cloud Run deployer. It adds a new configuration property to setup the hooks and the logic to trigger those.

**Follow-up Work (remove if N/A)**
- Document Cloud Run hooks

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#8173](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (9725f22) into [main](https://codecov.io/gh/GoogleContainerTools/skaffold/commit/290280e8c6c19231e9ed5b5c027f2c01d22f75be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (290280e) will **decrease** coverage by `4.27%`.
> The diff coverage is `53.26%`.

```diff
@@            Coverage Diff             @@
##             main    #8173      +/-   ##
==========================================
- Coverage   70.48%   66.21%   -4.28%     
==========================================
  Files         515      599      +84     
  Lines       23150    29383    +6233     
==========================================
+ Hits        16317    19455    +3138     
- Misses       5776     8473    +2697     
- Partials     1057     1455     +398     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | Coverage Δ | |
|---|---|---|
| [cmd/skaffold/app/cmd/completion.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY29tcGxldGlvbi5nbw==) | `13.04% <0.00%> (-1.25%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/credits/export.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY3JlZGl0cy9leHBvcnQuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/deploy.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGVwbG95Lmdv) | `40.90% <0.00%> (-12.94%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/test.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdGVzdC5nbw==) | `44.44% <0.00%> (ø)` | |
| [cmd/skaffold/app/exitcode.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9leGl0Y29kZS5nbw==) | `100.00% <ø> (+6.66%)` | :arrow_up: |
| [cmd/skaffold/skaffold.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL3NrYWZmb2xkLmdv) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/inspect\_tests.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvaW5zcGVjdF90ZXN0cy5nbw==) | `62.50% <14.28%> (-1.14%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/render.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcmVuZGVyLmdv) | `35.48% <18.18%> (-5.90%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/lsp.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvbHNwLmdv) | `28.12% <28.12%> (ø)` | |
| [cmd/skaffold/app/cmd/run.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcnVuLmdv) | `64.28% <33.33%> (-9.63%)` | :arrow_down: |
| ... and [393 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8173/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)


Patch:
diff --git a/cmd/skaffold/app/cmd/flags.go b/cmd/skaffold/app/cmd/flags.go
index 4685242d8..0590cb029 100644
--- a/cmd/skaffold/app/cmd/flags.go
+++ b/cmd/skaffold/app/cmd/flags.go
@@ -402,7 +402,7 @@ var flagRegistry = []Flag{
 		Value:         &opts.CustomTag,
 		DefValue:      "",
 		FlagAddMethod: "StringVar",
-		DefinedOn:     []string{"build", "debug", "dev", "run", "deploy"},
+		DefinedOn:     []string{"build", "debug", "dev", "run", "deploy", "render"},
 	},
 	{
 		Name:          "platform",
diff --git a/deploy/skaffold/Dockerfile.deps.slim b/deploy/skaffold/Dockerfile.deps.slim
index f5ecc3d00..e63114f25 100644
--- a/deploy/skaffold/Dockerfile.deps.slim
+++ b/deploy/skaffold/Dockerfile.deps.slim
@@ -97,7 +97,6 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
     build-essential \
     python-setuptools \
     lsb-release \
-    openjdk-17-jdk \
     software-properties-common \
     jq \
     apt-transport-https && \
diff --git a/docs-v2/config.toml b/docs-v2/config.toml
index b12062e2e..6a7756a21 100644
--- a/docs-v2/config.toml
+++ b/docs-v2/config.toml
@@ -82,7 +82,7 @@ weight = 1
 copyright = "Skaffold Authors"
 privacy_policy = "https://policies.google.com/privacy"
 github_repo = "https://github.com/GoogleContainerTools/skaffold"
-skaffold_version = "skaffold/v3"
+skaffold_version = "skaffold/v4beta2"
 
 # Google Custom Search Engine ID. Remove or comment out to disable search.
 # gcs_engine_id = "013756393218025596041:3nojel67sum"
diff --git a/docs-v2/content/en/docs/environment/templating.md b/docs-v2/content/en/docs/environment/templating.md
index dad639cf1..ac1168a21 100644
--- a/docs-v2/content/en/docs/environment/templating.md
+++ b/docs-v2/content/en/docs/environment/templating.md
@@ -37,6 +37,7 @@ List of variables that are available for templating:
 * For the `envTemplate` tagger:
   * `IMAGE_NAME` - the artifact's image name - the [image name rewriting]({{< relref "/docs/environment/image-registries.md" >}}) acts after the template is calculated
 * For Helm deployments:
-  * `IMAGE_NAME`, `IMAGE_TAG`, `IMAGE_DIGEST` - the first (by order of declaration in `build.artifacts`) artifact's image name, tag, and sha256 digest. Note: the [image name rewriting]({{< relref "/docs/environment/image-registries.md" >}}) acts after the template is calculated.
-  * `IMAGE_NAME2`, `IMAGE_TAG2`, `IMAGE_DIGEST2` - the 2nd artifact's image name, tag, and sha256 digest
-  * `IMAGE_NAMEN`, `IMAGE_TAGN`, `IMAGE_DIGESTN` - the Nth artifact's image name, tag, and sha256 digest
+  * `IMAGE_NAME`, `IMAGE_TAG`, `IMAGE_DIGEST, IMAGE_DOMAIN, IMAGE_REPO_NO_DOMAIN` - the first (by order of declaration in `build.artifacts`) artifact's image name, repo, tag, sha256 digest, registry/domain and repository w/o the registry/domain prefixed . Note: the [image name rewriting]({{< relref "/docs/environment/image-registries.md" >}}) acts after the template is calculated.
+  * `IMAGE_NAME_<artifact-name>`, `IMAGE_REPO_<artifact-name>`, `IMAGE_TAG_<artifact-name>`, `IMAGE_DIGEST_<artifact-name>` - the named artifact's image name, repo, tag, and sha256 digest. NOTE: When used in for templating all `/` and `-` chars must be changed to `_` characters as go templates do not accept `/` and `-`.
+  * `IMAGE_NAME2`, `IMAGE_REPO2`, `IMAGE_TAG2`, `IMAGE_DIGEST2` - the 2nd artifact's image name, tag, and sha256 digest
+  * `IMAGE_NAME2`, `IMAGE_REPON`, `IMAGE_TAGN`, `IMAGE_DIGESTN` - the Nth artifact's image name, tag, and sha256 digest
diff --git a/docs-v2/content/en/docs/references/cli/_index.md b/docs-v2/content/en/docs/references/cli/_index.md
index 1a74cb012..9b219e970 100644
--- a/docs-v2/content/en/docs/references/cli/_index.md
+++ b/docs-v2/content/en/docs/references/cli/_index.md
@@ -1010,6 +1010,7 @@ Options:
       --remote-cache-dir='': Specify the location of the git repositories cache (default $HOME/.skaffold/repos)
       --resource-selector-rules-file='': Path to JSON file specifying the deny list of yaml objects for skaffold to NOT transform with 'image' and 'label' field replacements.  NOTE: this list is additive to skaffold's default denylist and denylist has priority over allowlist
       --sync-remote-cache='always': Controls how Skaffold manages the remote config cache (see `remote-cache-dir`). One of `always` (default), `missing`, or `never`. `always` syncs remote repositories to latest on access. `missing` only clones remote repositories if they do not exist locally. `never` means the user takes responsibility for updating remote repositories.
+  -t, --tag='': The optional custom tag to use for images which overrides the current Tagger configuration
       --wait-for-connection=false: Blocks ending execution of skaffold until the /v2/events gRPC/HTTP endpoint is hit
 
 Usage:
@@ -1043,6 +1044,7 @@ Env vars:
 * `SKAFFOLD_REMOTE_CACHE_DIR` (same as `--remote-cache-dir`)
 * `SKAFFOLD_RESOURCE_SELECTOR_RULES_FILE` (same as `--resource-selector-rules-file`)
 * `SKAFFOLD_SYNC_REMOTE_CACHE` (same as `--sync-remote-cache`)
+* `SKAFFOLD_TAG` (same as `--tag`)
 * `SKAFFOLD_WAIT_FOR_CONNECTION` (same as `--wait-for-connection`)
 
 ### skaffold run
diff --git a/docs-v2/content/en/schemas/v4beta2.json b/docs-v2/content/en/schemas/v4beta2.json
index d4458277d..b126c529e 100755
--- a/docs-v2/content/en/schemas/v4beta2.json
+++ b/docs-v2/content/en/schemas/v4beta2.json
@@ -967,6 +967,11 @@
     },
     "CloudRunDeploy": {
       "properties": {
+        "hooks": {
+          "$ref": "#/definitions/CloudRunDeployHooks",
+          "description": "describes a set of lifecycle host hooks that are executed before and after the Cloud Run deployer.",
+          "x-intellij-html-description": "describes a set of lifecycle host hooks that are executed before and after the Cloud Run deployer."
+        },
         "projectid": {
           "type": "string",
           "description": "the GCP Project to use for Cloud Run. If specified, all Services will be deployed to this project. If not specified, each Service will be deployed to the project specified in `metadata.namespace` of the Cloud Run manifest.",
@@ -980,13 +985,42 @@
       },
       "preferredOrder": [
         "projectid",
-        "region"
+        "region",
+        "hooks"
       ],
       "additionalProperties": false,
       "type": "object",
       "description": "*alpha* deploys the container to Google Cloud Run.",
       "x-intellij-html-description": "<em>alpha</em> deploys the container to Google Cloud Run."
     },
+    "CloudRunDeployHooks": {
+      "properties": {
+        "after": {
+          "items": {
+            "$ref": "#/definitions/HostHook"
+          },
+          "type": "array",
+          "description": "describes the list of lifecycle hooks to execute *after* the Cloud Run deployer.",
+          "x-intellij-html-description": "describes the list of lifecycle hooks to execute <em>after</em> the Cloud Run deployer."
+        },
+        "before": {
+          "items": {
+            "$ref": "#/definitions/HostHook"
+          },
+          "type": "array",
+          "description": "describes the list of lifecycle hooks to execute *before* the Cloud Run deployer.",
+          "x-intellij-html-description": "describes the list of lifecycle hooks to execute <em>before</em> the Cloud Run deployer."
+        }
+      },
+      "preferredOrder": [
+        "before",
+        "after"
+      ],
+      "additionalProperties": false,
+      "type": "object",
+      "description": "describes the list of lifecycle hooks to execute in the host before and after the Cloud Run deployer.",
+      "x-intellij-html-description": "describes the list of lifecycle hooks to execute in the host before and after the Cloud Run deployer."
+    },
     "ClusterDetails": {
       "properties": {
         "HTTPS_PROXY": {
diff --git a/docs/content/en/docs/references/cli/_index.md b/docs/content/en/docs/references/cli/_index.md
index 1a74cb012..9b219e970 100644
--- a/docs/content/en/docs/references/cli/_index.md
+++ b/docs/content/en/docs/references/cli/_index.md
@@ -1010,6 +1010,7 @@ Options:
       --remote-cache-dir='': Specify the location of the git repositories cache (default $HOME/.skaffold/repos)
       --resource-selector-rules-file='': Path to JSON file specifying the deny list of yaml objects for skaffold to NOT transform with 'image' and 'label' field replacements.  NOTE: this list is additive to skaffold's default denylist and denylist has priority over allowlist
       --sync-remote-cache='always': Controls how Skaffold manages the remote config cache (see `remote-cache-dir`). One of `always` (default), `missing`, or `never`. `always` syncs remote repositories to latest on access. `missing` only clones remote repositories if they do not exist locally. `never` means the user takes responsibility for updating remote repositories.
+  -t, --tag='': The optional custom tag to use for images which overrides the current Tagger configuration
       --wait-for-connection=false: Blocks ending execution of skaffold until the /v2/events gRPC/HTTP endpoint is hit
 
 Usage:
@@ -1043,6 +1044,7 @@ Env vars:
 * `SKAFFOLD_REMOTE_CACHE_DIR` (same as `--remote-cache-dir`)
 * `SKAFFOLD_RESOURCE_SELECTOR_RULES_FILE` (same as `--resource-selector-rules-file`)
 * `SKAFFOLD_SYNC_REMOTE_CACHE` (same as `--sync-remote-cache`)
+* `SKAFFOLD_TAG` (same as `--tag`)
 * `SKAFFOLD_WAIT_FOR_CONNECTION` (same as `--wait-for-connection`)
 
 ### skaffold run
diff --git a/examples/lifecycle-hooks/k8s-pod.yaml b/examples/lifecycle-hooks/k8s-pod.yaml
index 27fdd6799..304716904 100644
--- a/examples/lifecycle-hooks/k8s-pod.yaml
+++ b/examples/lifecycle-hooks/k8s-pod.yaml
@@ -5,4 +5,4 @@ metadata:
 spec:
   containers:
   - name: getting-started
-    image: skaffold-example
+    image: hooks-example
diff --git a/examples/lifecycle-hooks/skaffold.yaml b/examples/lifecycle-hooks/skaffold.yaml
index 8826595a9..21c4c4ef4 100644
--- a/examples/lifecycle-hooks/skaffold.yaml
+++ b/examples/lifecycle-hooks/skaffold.yaml
@@ -2,7 +2,7 @@ apiVersion: skaffold/v4beta1
 kind: Config
 manifests:
   rawYaml:
-  - k8s-pod.yaml
+  - deployment.yaml
   hooks:
     before:
       - host:
diff --git a/hack/generate-man.sh b/hack/generate-man.sh
index 9302b8a83..69ba01560 100755
--- a/hack/generate-man.sh
+++ b/hack/generate-man.sh
@@ -15,6 +15,6 @@
 # limitations under the License.
 
 cat \
-  docs/content/en/docs/references/cli/index_header \
+  docs-v2/content/en/docs/references/cli/index_header \
   <(go run hack/man/man.go) \
-  > docs/content/en/docs/references/cli/_index.md
+  > docs-v2/content/en/docs/references/cli/_index.md
diff --git a/integration/examples/kaniko/skaffold.yaml b/integration/examples/kaniko/skaffold.yaml
index 16197973a..d104be00b 100644
--- a/integration/examples/kaniko/skaffold.yaml
+++ b/integration/examples/kaniko/skaffold.yaml
@@ -6,9 +6,7 @@ build:
       kaniko:
         cache: {}
   cluster:
-    pullSecretName: e2esecret-with-path
-    pullSecretPath:     kaniko-secret.json
-    namespace: default
+    pullSecretName: e2esecret
 manifests:
   rawYaml:
   - k8s-*
diff --git a/pkg/skaffold/build/build_problems.go b/pkg/skaffold/build/build_problems.go
index 1aeffa356..b5fca1619 100644
--- a/pkg/skaffold/build/build_problems.go
+++ b/pkg/skaffold/build/build_problems.go
@@ -134,10 +134,15 @@ func suggestBuildPushAccessDeniedAction(cfg interface{}) []*proto.Suggestion {
 			return append(suggestions, makeAuthSuggestionsForRepo(defaultRepo))
 		}
 	}
+	action := "Try running with `--default-repo` flag"
+	if buildCfg.Mode() != config.RunModes.Build {
+		// for `interactive` modes, also suggest user to use a local kubernetes cluster
+		action += ". Otherwise start a local kubernetes cluster like `minikube`"
+	}
 
 	return []*proto.Suggestion{{
 		SuggestionCode: proto.SuggestionCode_ADD_DEFAULT_REPO,
-		Action:         "Try running with `--default-repo` flag",
+		Action:         action,
 	}}
 }
 
diff --git a/pkg/skaffold/build/builder_mux.go b/pkg/skaffold/build/builder_mux.go
index 588b1c748..2b7a1d7c8 100644
--- a/pkg/skaffold/build/builder_mux.go
+++ b/pkg/skaffold/build/builder_mux.go
@@ -22,6 +22,7 @@ import (
 	"io"
 	"reflect"
 
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/hooks"
@@ -48,6 +49,7 @@ type Cache interface {
 type Config interface {
 	GetPipelines() []latest.Pipeline
 	DefaultRepo() *string
+	Mode() config.RunMode
 	MultiLevelRepo() *bool
 	GlobalConfig() string
 	BuildConcurrency() int
diff --git a/pkg/skaffold/build/cache/retrieve.go b/pkg/skaffold/build/cache/retrieve.go
index df1221343..82ba05057 100644
--- a/pkg/skaffold/build/cache/retrieve.go
+++ b/pkg/skaffold/build/cache/retrieve.go
@@ -43,7 +43,10 @@ func (c *cache) Build(ctx context.Context, out io.Writer, tags tag.ImageTags, ar
 
 	start := time.Now()
 
-	output.Default.Fprintln(out, "Checking cache...")
+	if len(artifacts) > 0 {
+		output.Default.Fprintln(out, "Checking cache...")
+	}
+
 	ctx, endTrace := instrumentation.StartTrace(ctx, "Build_CheckBuildCache")
 	defer endTrace()
 
diff --git a/pkg/skaffold/deploy/cloudrun/deploy.go b/pkg/skaffold/deploy/cloudrun/deploy.go
index 6411a0fc8..411f14a3b 100644
--- a/pkg/skaffold/deploy/cloudrun/deploy.go
+++ b/pkg/skaffold/deploy/cloudrun/deploy.go
@@ -35,6 +35,8 @@ import (
 	sErrors "github.com/GoogleContainerTools/skaffold/pkg/skaffold/errors"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/gcp"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/hooks"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/log"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
@@ -55,10 +57,14 @@ type Config interface {
 // Deployer deploys code to Google Cloud Run.
 type Deployer struct {
 	configName string
+
+	*latest.CloudRunDeploy
+
 	logger     *LogAggregator
 	accessor   *RunAccessor
 	monitor    *Monitor
 	labeller   *label.DefaultLabeller
+	hookRunner hooks.Runner
 
 	Project string
 	Region  string
@@ -71,13 +77,15 @@ type Deployer struct {
 // NewDeployer creates a new Deployer for Cloud Run from the Skaffold deploy config.
 func NewDeployer(cfg Config, labeller *label.DefaultLabeller, crDeploy *latest.CloudRunDeploy, configName string) (*Deployer, error) {
 	return &Deployer{
-		configName: configName,
-		Project:    crDeploy.ProjectID,
-		Region:     crDeploy.Region,
+		configName:     configName,
+		CloudRunDeploy: crDeploy,
+		Project:        crDeploy.ProjectID,
+		Region:         crDeploy.Region,
 		// TODO: implement logger for Cloud Run.
 		logger:        NewLoggerAggregator(cfg, labeller.GetRunID()),
 		accessor:      NewAccessor(cfg, labeller.GetRunID()),
 		labeller:      labeller,
+		hookRunner:    hooks.NewCloudRunDeployRunner(crDeploy.LifecycleHooks, hooks.NewDeployEnvOpts(labeller.GetRunID(), "", []string{})),
 		useGcpOptions: true,
 	}, nil
 }
@@ -143,6 +151,30 @@ func (d *Deployer) GetStatusMonitor() status.Monitor {
 	return d.getMonitor()
 }
 
+func (d *Deployer) HasRunnableHooks() bool {
+	return len(d.CloudRunDeploy.LifecycleHooks.PreHooks) > 0 || len(d.CloudRunDeploy.LifecycleHooks.PostHooks) > 0
+}
+
+func (d *Deployer) PreDeployHooks(ctx context.Context, out io.Writer) error {
+	childCtx, endTrace := instrumentation.StartTrace(ctx, "Deploy_PreHooks")
+	if err := d.hookRunner.RunPreHooks(childCtx, out); err != nil {
+		endTrace(instrumentation.TraceEndError(err))
+		return err
+	}
+	endTrace()
+	return nil
+}
+
+func (d *Deployer) PostDeployHooks(ctx context.Context, out io.Writer) error {
+	childCtx, endTrace := instrumentation.StartTrace(ctx, "Deploy_PostHooks")
+	if err := d.hookRunner.RunPostHooks(childCtx, out); err != nil {
+		endTrace(instrumentation.TraceEndError(err))
+		return err
+	}
+	endTrace()
+	return nil
+}
+
 func (d *Deployer) getMonitor() *Monitor {
 	if d.monitor == nil {
 		d.monitor = NewMonitor(d.labeller, d.clientOptions)
diff --git a/pkg/skaffold/hooks/deploy.go b/pkg/skaffold/hooks/deploy.go
index 0aabbafed..159897ba2 100644
--- a/pkg/skaffold/hooks/deploy.go
+++ b/pkg/skaffold/hooks/deploy.go
@@ -33,13 +33,38 @@ import (
 
 // for testing
 var (
-	NewDeployRunner = newDeployRunner
+	NewDeployRunner         = newDeployRunner
+	NewCloudRunDeployRunner = newCloudRunDeployRunner
 )
 
 func newDeployRunner(cli *kubectl.CLI, d latest.DeployHooks, namespaces *[]string, formatter logger.Formatter, opts DeployEnvOpts) Runner {
 	return deployRunner{d, cli, namespaces, formatter, opts, new(sync.Map)}
 }
 
+func newCloudRunDeployRunner(d latest.CloudRunDeployHooks, opts DeployEnvOpts) Runner {
+	deployHooks := latest.DeployHooks{}
+	deployHooks.PreHooks = createDeployHostHooksFromCloudRunHooks(d.PreHooks)
+	deployHooks.PostHooks = createDeployHostHooksFromCloudRunHooks(d.PostHooks)
+
+	return deployRunner{
+		DeployHooks: deployHooks,
+		opts:        opts,
+	}
+}
+
+func createDeployHostHooksFromCloudRunHooks(cloudRunHook []latest.HostHook) []latest.DeployHookItem {
+	deployHooks := []latest.DeployHookItem{}
+
+	for i := range cloudRunHook {
+		hookItem := latest.DeployHookItem{
+			HostHook: &cloudRunHook[i],
+		}
+		deployHooks = append(deployHooks, hookItem)
+	}
+
+	return deployHooks
+}
+
 func NewDeployEnvOpts(runID string, kubeContext string, namespaces []string) DeployEnvOpts {
 	return DeployEnvOpts{
 		RunID:       runID,
diff --git a/pkg/skaffold/runner/build.go b/pkg/skaffold/runner/build.go
index e5b42dc76..85c2ad33f 100644
--- a/pkg/skaffold/runner/build.go
+++ b/pkg/skaffold/runner/build.go
@@ -161,7 +161,12 @@ func (r *Builder) ApplyDefaultRepo(tag string) (string, error) {
 func (r *Builder) imageTags(ctx context.Context, out io.Writer, artifacts []*latest.Artifact) (tag.ImageTags, error) {
 	start := time.Now()
 	maxWorkers := runtime.GOMAXPROCS(0)
-	output.Default.Fprintln(out, "Generating tags...")
+
+	if len(artifacts) > 0 {
+		output.Default.Fprintln(out, "Generating tags...")
+	} else {
+		output.Default.Fprintln(out, "No tags generated")
+	}
 
 	tagErrs := make([]chan tagErr, len(artifacts))
 
diff --git a/pkg/skaffold/runner/deploy.go b/pkg/skaffold/runner/deploy.go
index 0a71d44fe..4edd03875 100644
--- a/pkg/skaffold/runner/deploy.go
+++ b/pkg/skaffold/runner/deploy.go
@@ -68,7 +68,9 @@ func (r *SkaffoldRunner) Deploy(ctx context.Context, out io.Writer, artifacts []
 
 	out, ctx = output.WithEventContext(ctx, out, constants.Deploy, constants.SubtaskIDNone)
 
-	output.Default.Fprintln(out, "Tags used in deployment:")
+	if len(artifacts) > 0 {
+		output.Default.Fprintln(out, "Tags used in deployment:")
+	}
 
 	for _, artifact := range artifacts {
 		output.Default.Fprintf(out, " - %s -> ", artifact.ImageName)
diff --git a/pkg/skaffold/runner/deployer.go b/pkg/skaffold/runner/deployer.go
index 90a75f28e..efddb0ef1 100644
--- a/pkg/skaffold/runner/deployer.go
+++ b/pkg/skaffold/runner/deployer.go
@@ -335,5 +335,12 @@ func getCloudRunDeployer(runCtx *runcontext.RunContext, labeller *label.DefaultL
 			}
 		}
 	}
-	return cloudrun.NewDeployer(runCtx, labeller, &latest.CloudRunDeploy{Region: region, ProjectID: defaultProject}, configName)
+
+	lifecycleHooks := latest.CloudRunDeployHooks{}
+	if configName != "" {
+		currentPipeline := runCtx.Pipelines.GetForConfigName(configName)
+		lifecycleHooks = currentPipeline.Deploy.CloudRunDeploy.LifecycleHooks
+	}
+
+	return cloudrun.NewDeployer(runCtx, labeller, &latest.CloudRunDeploy{Region: region, ProjectID: defaultProject, LifecycleHooks: lifecycleHooks}, configName)
 }
diff --git a/pkg/skaffold/runner/dev.go b/pkg/skaffold/runner/dev.go
index 9bca721c0..221fccf51 100644
--- a/pkg/skaffold/runner/dev.go
+++ b/pkg/skaffold/runner/dev.go
@@ -227,7 +227,12 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la
 	g := getTransposeGraph(artifacts)
 	// Watch artifacts
 	start := time.Now()
-	output.Default.Fprintln(out, "Listing files to watch...")
+
+	if len(artifacts) > 0 {
+		output.Default.Fprintln(out, "Listing files to watch...")
+	} else {
+		output.Default.Fprintln(out, "No artifacts found to watch")
+	}
 
 	for i := range artifacts {
 		artifact := artifacts[i]


Test patch:
diff --git a/integration/deploy_cloudrun_test.go b/integration/deploy_cloudrun_test.go
index 1aa3a03dd..0e2123278 100644
--- a/integration/deploy_cloudrun_test.go
+++ b/integration/deploy_cloudrun_test.go
@@ -19,12 +19,14 @@ package integration
 import (
 	"context"
 	"fmt"
+	"strings"
 	"testing"
 
 	"google.golang.org/api/run/v1"
 
 	"github.com/GoogleContainerTools/skaffold/integration/skaffold"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/gcp"
+	"github.com/GoogleContainerTools/skaffold/testutil"
 )
 
 func TestDeployCloudRun(t *testing.T) {
@@ -42,6 +44,31 @@ func TestDeployCloudRun(t *testing.T) {
 	}
 }
 
+func TestDeployCloudRunWithHooks(t *testing.T) {
+	MarkIntegrationTest(t, NeedsGcp)
+
+	testutil.Run(t, "cloud run deploy with hooks", func(t *testutil.T) {
+		expectedOutput := []string{
+			"PRE-DEPLOY Cloud Run MODULE1 host hook",
+			"POST-DEPLOY Cloud Run MODULE1 host hook",
+			"PRE-DEPLOY Cloud Run MODULE2 host hook",
+			"POST-DEPLOY Cloud Run MODULE2 host hook",
+		}
+
+		out := skaffold.Run().InDir("testdata/deploy-cloudrun-with-hooks").RunOrFailOutput(t.T)
+
+		commandOutput := string(out)
+		previousFoundIndex := -1
+
+		for _, expectedOutput := range expectedOutput {
+			expectedOutputFoundIndex := strings.Index(commandOutput, expectedOutput)
+			isPreviousOutputBeforeThanCurrent := previousFoundIndex < expectedOutputFoundIndex
+			t.CheckTrue(isPreviousOutputBeforeThanCurrent)
+			previousFoundIndex = expectedOutputFoundIndex
+		}
+	})
+}
+
 // TODO: remove nolint when test is unskipped
 //
 //nolint:unused
diff --git a/integration/render_test.go b/integration/render_test.go
index 1394bfa07..8a345ca83 100644
--- a/integration/render_test.go
+++ b/integration/render_test.go
@@ -837,3 +837,59 @@ spec:
 		})
 	}
 }
+
+func TestRenderWithTagFlag(t *testing.T) {
+	MarkIntegrationTest(t, CanRunWithoutGcp)
+
+	tests := []struct {
+		description    string
+		projectDir     string
+		expectedOutput string
+	}{
+		{
+			description: "tag flag in a single module project",
+			projectDir:  "testdata/getting-started",
+			expectedOutput: `apiVersion: v1
+kind: Pod
+metadata:
+  name: getting-started
+  namespace: default
+spec:
+  containers:
+  - image: gcr.io/k8s-skaffold/skaffold-example:customtag
+    name: getting-started
+`,
+		},
+		{
+			description: "tag flag in a multi module project",
+			projectDir:  "testdata/multi-config-pods",
+			expectedOutput: `apiVersion: v1
+kind: Pod
+metadata:
+  name: module1
+  namespace: default
+spec:
+  containers:
+  - image: gcr.io/k8s-skaffold/multi-config-module1:customtag
+    name: module1
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: module2
+  namespace: default
+spec:
+  containers:
+  - image: gcr.io/k8s-skaffold/multi-config-module2:customtag
+    name: module2
+`,
+		},
+	}
+
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			output := skaffold.Render("--tag", "customtag", "--default-repo", "gcr.io/k8s-skaffold").InDir(test.projectDir).RunOrFailOutput(t.T)
+			t.CheckDeepEqual(string(output), test.expectedOutput)
+		})
+	}
+}
diff --git a/integration/run_test.go b/integration/run_test.go
index 2af36151b..99086785f 100644
--- a/integration/run_test.go
+++ b/integration/run_test.go
@@ -318,17 +318,18 @@ func TestRunRenderOnly(t *testing.T) {
 
 func TestRunGCPOnly(t *testing.T) {
 	tests := []struct {
-		description string
-		dir         string
-		args        []string
-		deployments []string
-		pods        []string
+		description       string
+		dir               string
+		args              []string
+		deployments       []string
+		pods              []string
+		skipCrossPlatform bool
 	}{
-		// {
-		//	description: "Google Cloud Build",
-		//	dir:         "examples/google-cloud-build",
-		//	pods:        []string{"getting-started"},
-		// },
+		{
+			description: "Google Cloud Build",
+			dir:         "examples/google-cloud-build",
+			pods:        []string{"getting-started"},
+		},
 		{
 			description: "Google Cloud Build with sub folder",
 			dir:         "testdata/gcb-sub-folder",
@@ -345,31 +346,33 @@ func TestRunGCPOnly(t *testing.T) {
 			args:        []string{"-p", "gcb"},
 			pods:        []string{"module1", "module2"},
 		},
-		// {
-		//	description: "Google Cloud Build with Kaniko",
-		//	dir:         "examples/gcb-kaniko",
-		//	pods:        []string{"getting-started-kaniko"},
-		// },
-		// {
-		//	description: "kaniko",
-		//	dir:         "examples/kaniko",
-		//	pods:        []string{"getting-started-kaniko"},
-		// },
-		// {
-		//	description: "kaniko with target",
-		//	dir:         "testdata/kaniko-target",
-		//	pods:        []string{"getting-started-kaniko"},
-		// },
+		{
+			description: "Google Cloud Build with Kaniko",
+			dir:         "examples/gcb-kaniko",
+			pods:        []string{"getting-started-kaniko"},
+			// building machines on gcb are linux/amd64, kaniko doesn't support cross-platform builds.
+			skipCrossPlatform: true,
+		},
+		{
+			description: "kaniko",
+			dir:         "examples/kaniko",
+			pods:        []string{"getting-started-kaniko"},
+		},
+		{
+			description: "kaniko with target",
+			dir:         "testdata/kaniko-target",
+			pods:        []string{"getting-started-kaniko"},
+		},
 		{
 			description: "kaniko with sub folder",
 			dir:         "testdata/kaniko-sub-folder",
 			pods:        []string{"getting-started-kaniko"},
 		},
-		// {
-		//	description: "kaniko microservices",
-		//	dir:         "testdata/kaniko-microservices",
-		//	deployments: []string{"leeroy-app", "leeroy-web"},
-		// },
+		{
+			description: "kaniko microservices",
+			dir:         "testdata/kaniko-microservices",
+			deployments: []string{"leeroy-app", "leeroy-web"},
+		},
 		{
 			description: "jib in googlecloudbuild",
 			dir:         "testdata/jib",
@@ -387,11 +390,13 @@ func TestRunGCPOnly(t *testing.T) {
 			dir:         "examples/buildpacks",
 			args:        []string{"-p", "gcb"},
 			deployments: []string{"web"},
+			// buildpacks doesn't support arm64 builds.
+			skipCrossPlatform: true,
 		},
 	}
 	for _, test := range tests {
-		if (os.Getenv("GKE_CLUSTER_NAME") == "integration-tests-arm" || os.Getenv("GKE_CLUSTER_NAME") == "integration-tests-hybrid") && strings.Contains(test.description, "buildpacks") {
-			continue // buildpacks doesn't support arm64 builds, so skip run on these clusters
+		if (os.Getenv("GKE_CLUSTER_NAME") == "integration-tests-arm" || os.Getenv("GKE_CLUSTER_NAME") == "integration-tests-hybrid") && test.skipCrossPlatform {
+			continue
 		}
 		t.Run(test.description, func(t *testing.T) {
 			MarkIntegrationTest(t, NeedsGcp)
diff --git a/integration/testdata/deploy-cloudrun-with-hooks/module1/service.yaml b/integration/testdata/deploy-cloudrun-with-hooks/module1/service.yaml
new file mode 100644
index 000000000..fab10ff8b
--- /dev/null
+++ b/integration/testdata/deploy-cloudrun-with-hooks/module1/service.yaml
@@ -0,0 +1,9 @@
+apiVersion: "serving.knative.dev/v1"
+kind: Service
+metadata:
+  name: cloud-run-hooks-module1
+spec:
+  template:
+    spec:
+      containers:
+      - image: gcr.io/cloudrun/hello
\ No newline at end of file
diff --git a/integration/testdata/deploy-cloudrun-with-hooks/module1/skaffold.yaml b/integration/testdata/deploy-cloudrun-with-hooks/module1/skaffold.yaml
new file mode 100644
index 000000000..ef7e6d378
--- /dev/null
+++ b/integration/testdata/deploy-cloudrun-with-hooks/module1/skaffold.yaml
@@ -0,0 +1,21 @@
+apiVersion: skaffold/v4beta2
+kind: Config
+metadata:
+   name: cloud-run-hooks-module1
+
+manifests:
+  rawYaml:
+    - service.yaml
+
+deploy:
+  cloudrun:
+    projectid: k8s-skaffold
+    region: us-central1
+    hooks:
+      before:
+        - command: ["sh", "-c", "echo PRE-DEPLOY Cloud Run MODULE1 host hook"]
+          os: [darwin, linux]
+      
+      after:
+        - command: ["sh", "-c", "echo POST-DEPLOY Cloud Run MODULE1 host hook!"]
+          os: [darwin, linux]
\ No newline at end of file
diff --git a/integration/testdata/deploy-cloudrun-with-hooks/module2/service.yaml b/integration/testdata/deploy-cloudrun-with-hooks/module2/service.yaml
new file mode 100644
index 000000000..4d95b4c08
--- /dev/null
+++ b/integration/testdata/deploy-cloudrun-with-hooks/module2/service.yaml
@@ -0,0 +1,9 @@
+apiVersion: "serving.knative.dev/v1"
+kind: Service
+metadata:
+  name: cloud-run-hooks-module2
+spec:
+  template:
+    spec:
+      containers:
+      - image: gcr.io/cloudrun/hello
\ No newline at end of file
diff --git a/integration/testdata/deploy-cloudrun-with-hooks/module2/skaffold.yaml b/integration/testdata/deploy-cloudrun-with-hooks/module2/skaffold.yaml
new file mode 100644
index 000000000..23116c54a
--- /dev/null
+++ b/integration/testdata/deploy-cloudrun-with-hooks/module2/skaffold.yaml
@@ -0,0 +1,21 @@
+apiVersion: skaffold/v4beta2
+kind: Config
+metadata:
+   name: cloud-run-hooks-module2
+
+manifests:
+  rawYaml:
+    - service.yaml
+
+deploy:
+  cloudrun:
+    projectid: k8s-skaffold
+    region: us-central1
+    hooks:
+      before:
+        - command: ["sh", "-c", "echo PRE-DEPLOY Cloud Run MODULE2 host hook!"]
+          os: [darwin, linux]
+      
+      after:
+        - command: ["sh", "-c", "echo POST-DEPLOY Cloud Run MODULE2 host hook!"]
+          os: [darwin, linux]
\ No newline at end of file
diff --git a/integration/testdata/deploy-cloudrun-with-hooks/skaffold.yaml b/integration/testdata/deploy-cloudrun-with-hooks/skaffold.yaml
new file mode 100644
index 000000000..971d42d43
--- /dev/null
+++ b/integration/testdata/deploy-cloudrun-with-hooks/skaffold.yaml
@@ -0,0 +1,5 @@
+apiVersion: skaffold/v4beta2
+kind: Config
+requires:
+- path: ./module1
+- path: ./module2
\ No newline at end of file
diff --git a/pkg/skaffold/build/build_problems_test.go b/pkg/skaffold/build/build_problems_test.go
index 256d563c6..0f0804663 100644
--- a/pkg/skaffold/build/build_problems_test.go
+++ b/pkg/skaffold/build/build_problems_test.go
@@ -52,13 +52,15 @@ func TestBuildProblems(t *testing.T) {
 	tests := []struct {
 		description string
 		context     config.ContextConfig
+		mode        config.RunMode
 		optRepo     string
 		err         error
 		expected    string
 		expectedAE  *proto.ActionableErr
 	}{
 		{
-			description: "Push access denied when neither default repo or global config is defined",
+			description: "Push access denied when neither default repo or global config is defined in `build` command",
+			mode:        config.RunModes.Build,
 			err:         fmt.Errorf("skaffold build failed: could not push image: denied: push access to resource"),
 			expected:    "Build Failed. No push access to specified image repository. Try running with `--default-repo` flag.",
 			expectedAE: &proto.ActionableErr{
@@ -70,6 +72,20 @@ func TestBuildProblems(t *testing.T) {
 				},
 				}},
 		},
+		{
+			description: "Push access denied when neither default repo or global config is defined in `dev` command",
+			mode:        config.RunModes.Dev,
+			err:         fmt.Errorf("skaffold build failed: could not push image: denied: push access to resource"),
+			expected:    "Build Failed. No push access to specified image repository. Try running with `--default-repo` flag. Otherwise start a local kubernetes cluster like `minikube`.",
+			expectedAE: &proto.ActionableErr{
+				ErrCode: proto.StatusCode_BUILD_PUSH_ACCESS_DENIED,
+				Message: "skaffold build failed: could not push image: denied: push access to resource",
+				Suggestions: []*proto.Suggestion{{
+					SuggestionCode: proto.SuggestionCode_ADD_DEFAULT_REPO,
+					Action:         "Try running with `--default-repo` flag. Otherwise start a local kubernetes cluster like `minikube`",
+				},
+				}},
+		},
 		{
 			description: "Push access denied when default repo is defined",
 			optRepo:     "gcr.io/test",
@@ -188,7 +204,7 @@ func TestBuildProblems(t *testing.T) {
 				pc.AddPhaseProblems(constants.Build, problems)
 				return pc
 			})
-			cfg := mockConfig{optRepo: test.optRepo}
+			cfg := mockConfig{optRepo: test.optRepo, mode: test.mode}
 			actual := sErrors.ShowAIError(&cfg, test.err)
 			t.CheckDeepEqual(test.expected, actual.Error())
 			actualAE := sErrors.ActionableErr(&cfg, constants.Build, test.err)
diff --git a/pkg/skaffold/build/builder_mux_test.go b/pkg/skaffold/build/builder_mux_test.go
index 05077afe7..fb454c71f 100644
--- a/pkg/skaffold/build/builder_mux_test.go
+++ b/pkg/skaffold/build/builder_mux_test.go
@@ -22,6 +22,7 @@ import (
 	"io"
 	"testing"
 
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
@@ -191,11 +192,13 @@ func TestGetConcurrency(t *testing.T) {
 
 type mockConfig struct {
 	pipelines []latest.Pipeline
+	mode      config.RunMode
 	optRepo   string
 }
 
 func (m *mockConfig) GetPipelines() []latest.Pipeline { return m.pipelines }
 func (m *mockConfig) GlobalConfig() string            { return "" }
+func (m *mockConfig) Mode() config.RunMode            { return m.mode }
 func (m *mockConfig) DefaultRepo() *string {
 	if m.optRepo != "" {
 		return &m.optRepo
diff --git a/pkg/skaffold/hooks/deploy_test.go b/pkg/skaffold/hooks/deploy_test.go
index 71d758bd7..3ffb25345 100644
--- a/pkg/skaffold/hooks/deploy_test.go
+++ b/pkg/skaffold/hooks/deploy_test.go
@@ -112,3 +112,151 @@ func TestDeployHooks(t *testing.T) {
 		t.CheckContains(postContainerHookOut, postOut.String())
 	})
 }
+
+func TestNewCloudRunDeployRunnerHooksMapping(t *testing.T) {
+	tests := []struct {
+		description   string
+		expectedHooks latest.DeployHooks
+		inputConfig   latest.CloudRunDeployHooks
+	}{
+		{
+			description: "no hooks specified",
+			expectedHooks: latest.DeployHooks{
+				PreHooks:  []latest.DeployHookItem{},
+				PostHooks: []latest.DeployHookItem{},
+			},
+			inputConfig: latest.CloudRunDeployHooks{},
+		},
+		{
+			description: "pre hooks specified",
+			expectedHooks: latest.DeployHooks{
+				PreHooks: []latest.DeployHookItem{
+					{
+						HostHook: &latest.HostHook{
+							Command: []string{"no-real-command-1 arg1 arg2"},
+							OS:      []string{"darwin", "linux"},
+							Dir:     ".",
+						},
+					},
+					{
+						HostHook: &latest.HostHook{
+							Command: []string{"no-real-command-2 arg1 arg2"},
+							OS:      []string{"linux", "darwin"},
+							Dir:     "~/",
+						},
+					},
+				},
+				PostHooks: []latest.DeployHookItem{},
+			},
+			inputConfig: latest.CloudRunDeployHooks{
+				PreHooks: []latest.HostHook{
+					{
+						Command: []string{"no-real-command-1 arg1 arg2"},
+						OS:      []string{"darwin", "linux"},
+						Dir:     ".",
+					},
+					{
+						Command: []string{"no-real-command-2 arg1 arg2"},
+						OS:      []string{"linux", "darwin"},
+						Dir:     "~/",
+					},
+				},
+			},
+		},
+		{
+			description: "post hooks specified",
+			expectedHooks: latest.DeployHooks{
+				PreHooks: []latest.DeployHookItem{},
+				PostHooks: []latest.DeployHookItem{
+					{
+						HostHook: &latest.HostHook{
+							Command: []string{"no-real-command-1 arg1 arg2"},
+							OS:      []string{"darwin", "linux"},
+							Dir:     ".",
+						},
+					},
+					{
+						HostHook: &latest.HostHook{
+							Command: []string{"no-real-command-2 arg1 arg2"},
+							OS:      []string{"linux", "darwin"},
+							Dir:     "~/",
+						},
+					},
+				},
+			},
+			inputConfig: latest.CloudRunDeployHooks{
+				PostHooks: []latest.HostHook{
+					{
+						Command: []string{"no-real-command-1 arg1 arg2"},
+						OS:      []string{"darwin", "linux"},
+						Dir:     ".",
+					},
+					{
+						Command: []string{"no-real-command-2 arg1 arg2"},
+						OS:      []string{"linux", "darwin"},
+						Dir:     "~/",
+					},
+				},
+			},
+		},
+		{
+			description: "post and pre hooks specified",
+			expectedHooks: latest.DeployHooks{
+				PreHooks: []latest.DeployHookItem{
+					{
+						HostHook: &latest.HostHook{
+							Command: []string{"pre-no-real-command-1 arg1 arg2"},
+							OS:      []string{"darwin", "linux"},
+							Dir:     ".",
+						},
+					},
+				},
+				PostHooks: []latest.DeployHookItem{
+					{
+						HostHook: &latest.HostHook{
+							Command: []string{"post-no-real-command-1 arg1 arg2"},
+							OS:      []string{"darwin", "linux"},
+							Dir:     ".",
+						},
+					},
+					{
+						HostHook: &latest.HostHook{
+							Command: []string{"post-no-real-command-2 arg1 arg2"},
+							OS:      []string{"linux", "darwin"},
+							Dir:     "~/",
+						},
+					},
+				},
+			},
+			inputConfig: latest.CloudRunDeployHooks{
+				PreHooks: []latest.HostHook{
+					{
+						Command: []string{"pre-no-real-command-1 arg1 arg2"},
+						OS:      []string{"darwin", "linux"},
+						Dir:     ".",
+					},
+				},
+				PostHooks: []latest.HostHook{
+					{
+						Command: []string{"post-no-real-command-1 arg1 arg2"},
+						OS:      []string{"darwin", "linux"},
+						Dir:     ".",
+					},
+					{
+						Command: []string{"post-no-real-command-2 arg1 arg2"},
+						OS:      []string{"linux", "darwin"},
+						Dir:     "~/",
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			runner, ok := (NewCloudRunDeployRunner(test.inputConfig, DeployEnvOpts{})).(deployRunner)
+			t.CheckTrue(ok)
+			t.CheckDeepEqual(runner.DeployHooks, test.expectedHooks)
+		})
+	}
+}
diff --git a/pkg/skaffold/runner/deployer_test.go b/pkg/skaffold/runner/deployer_test.go
index 72a7312d6..e1f3a95f9 100644
--- a/pkg/skaffold/runner/deployer_test.go
+++ b/pkg/skaffold/runner/deployer_test.go
@@ -484,7 +484,7 @@ func TestGetCloudRunDeployer(tOuter *testing.T) {
 			cfgs: map[string]latest.DeployType{"": {
 				CloudRunDeploy: &latest.CloudRunDeploy{ProjectID: "test-project", Region: "test-region"},
 			}},
-			expected: &cloudrun.Deployer{Project: "test-project", Region: "test-region"},
+			expected: &cloudrun.Deployer{Project: "test-project", Region: "test-region", CloudRunDeploy: &latest.CloudRunDeploy{ProjectID: "test-project", Region: "test-region"}},
 		},
 		{
 			name: "deploy with two configs and conflicting processes",
@@ -502,7 +502,7 @@ func TestGetCloudRunDeployer(tOuter *testing.T) {
 			cfgs: map[string]latest.DeployType{"": {
 				CloudRunDeploy: &latest.CloudRunDeploy{ProjectID: "test-project", Region: "test-region"},
 			}},
-			expected: &cloudrun.Deployer{Project: "overridden-project", Region: "test-region"},
+			expected: &cloudrun.Deployer{Project: "overridden-project", Region: "test-region", CloudRunDeploy: &latest.CloudRunDeploy{ProjectID: "overridden-project", Region: "test-region"}},
 		},
 	}
 	for _, test := range tests {
diff --git a/pkg/skaffold/schema/latest/config.go b/pkg/skaffold/schema/latest/config.go
index 29f744117..8ab80f04b 100644
--- a/pkg/skaffold/schema/latest/config.go
+++ b/pkg/skaffold/schema/latest/config.go
@@ -751,6 +751,9 @@ type CloudRunDeploy struct {
 	// Region GCP location to use for the Cloud Run Deploy.
 	// Must be one of the regions listed in https://cloud.google.com/run/docs/locations.
 	Region string `yaml:"region,omitempty"`
+
+	// LifecycleHooks describes a set of lifecycle host hooks that are executed before and after the Cloud Run deployer.
+	LifecycleHooks CloudRunDeployHooks `yaml:"hooks,omitempty"`
 }
 
 // DockerDeploy uses the `docker` CLI to create application containers in Docker.
@@ -1567,6 +1570,14 @@ type RenderHooks struct {
 	PostHooks []RenderHookItem `yaml:"after,omitempty"`
 }
 
+// CloudRunDeployHooks describes the list of lifecycle hooks to execute in the host before and after the Cloud Run deployer.
+type CloudRunDeployHooks struct {
+	// PreHooks describes the list of lifecycle hooks to execute *before* the Cloud Run deployer.
+	PreHooks []HostHook `yaml:"before,omitempty"`
+	// PostHooks describes the list of lifecycle hooks to execute *after* the Cloud Run deployer.
+	PostHooks []HostHook `yaml:"after,omitempty"`
+}
+
 // DeployHookItem describes a single lifecycle hook to execute before or after each deployer step.
 type DeployHookItem struct {
 	// HostHook describes a single lifecycle hook to run on the host machine.