Repository: GoogleContainerTools/skaffold
Pull request #8054 (2022-11-08 00:12:03)

docs: Linked to the Google Cloud Solutions Template
Included a link to the Google Cloud Solutions Template on the Quickstart Standalone tab.

<!-- Thank you for your contribution! -->

<!-- Include if applicable: -->
Fixes: #nnn <!-- tracking issues that this PR will close -->
**Related**: _Relevant tracking issues, for context_
**Merge before/after**: _Dependent or prerequisite PRs_

**Description**
<!-- Describe your changes here. The more detail, the easier the review! -->

**User facing changes (remove if N/A)**
<!-- Describe any user facing changes this PR introduces. -->
<!-- "Before" and "After" sections work great - bonus points for screenshots! -->
<!-- Be sure all docs have been updated as well! -->

**Follow-up Work (remove if N/A)**
<!-- Mention any related follow up work to this PR. -->


<!--
Please be sure your PR includes unit tests - we don't merge code that brings down test coverage! 
Integration tests are sometimes an appropriate substitute. 
-->


container-tools-bot: Please visit [http://104.154.69.201:1313](http://104.154.69.201:1313) to view changes to the docs.
codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8054?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#8054](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8054?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (32a2062) 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.00%`.
> The diff coverage is `53.46%`.

```diff
@@            Coverage Diff             @@
##             main    #8054      +/-   ##
==========================================
- Coverage   70.48%   66.47%   -4.01%     
==========================================
  Files         515      597      +82     
  Lines       23150    29030    +5880     
==========================================
+ Hits        16317    19298    +2981     
- Misses       5776     8295    +2519     
- Partials     1057     1437     +380     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8054?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/credits/export.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8054/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/8054/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/8054/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/8054/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/8054/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/8054/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/8054/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/8054/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/8054/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: |
| [cmd/skaffold/app/cmd/fix.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8054/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZml4Lmdv) | `56.41% <37.50%> (-20.07%)` | :arrow_down: |
| ... and [392 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8054/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/.github/workflows/performance-comparison.yml b/.github/workflows/performance-comparison.yml
index b9ca3aeff..2fe1344b4 100644
--- a/.github/workflows/performance-comparison.yml
+++ b/.github/workflows/performance-comparison.yml
@@ -92,7 +92,7 @@ jobs:
         sudo install minikube /usr/local/bin/minikube
         minikube start --profile=minikube --driver=docker
 
-    - uses: xt0rted/pull-request-comment-branch@f2f07162618551540507cbd00ddf16ffcaa0c72f
+    - uses: xt0rted/pull-request-comment-branch@653a7d5ca8bd91d3c5cb83286063314d0b063b8e
       id: comment-branch
 
     - name: Make and install Skaffold binary from current PR
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 69fcf24e3..d741c844f 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -27,7 +27,7 @@ jobs:
           persist-credentials: false
 
       - name: "Run analysis"
-        uses: ossf/scorecard-action@e363bfca00e752f91de7b7d2a77340e2e523cb18 # v2.0.0
+        uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.0
         with:
           results_file: results.sarif
           results_format: sarif
diff --git a/docs-v2/content/en/docs/quickstart/_index.md b/docs-v2/content/en/docs/quickstart/_index.md
index ef2c4e808..dfa108ab0 100644
--- a/docs-v2/content/en/docs/quickstart/_index.md
+++ b/docs-v2/content/en/docs/quickstart/_index.md
@@ -8,7 +8,7 @@ weight: 20
 {{% tab "STANDALONE" %}}
 
 Follow this tutorial if you're using the Skaffold [standalone binary]({{< relref "../install/#standalone-binary" >}}). It walks through running Skaffold on a small Kubernetes app built with [Docker](https://www.docker.com/) inside [minikube](https://minikube.sigs.k8s.io)
-and deployed with [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)! 
+and deployed with [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)! To learn how to generate a new project with previously built-in structure and features, see the [Google Cloud Solutions Template](https://github.com/GoogleCloudPlatform/solutions-template).
 
 This tutorial uses minikube as Skaffold knows how to build the app using the Docker daemon hosted
 inside minikube and thus avoiding any need for a registry to host the app's container images.
diff --git a/pkg/skaffold/deploy/cloudrun/cloudrun.go b/pkg/skaffold/deploy/cloudrun/cloudrun.go
index ac44f61d2..b59e42890 100644
--- a/pkg/skaffold/deploy/cloudrun/cloudrun.go
+++ b/pkg/skaffold/deploy/cloudrun/cloudrun.go
@@ -18,14 +18,39 @@ package cloudrun
 
 import "fmt"
 
+const (
+	typeService = "Service"
+	typeJob     = "Job"
+)
+
+type ResourceType string
+
 // RunResourceName represents a Cloud Run Service
 type RunResourceName struct {
 	Project string
 	Region  string
 	Service string
+	Job     string
 }
 
 // String returns the path representation of a Cloud Run Service.
 func (n RunResourceName) String() string {
-	return fmt.Sprintf("projects/%s/locations/%s/services/%s", n.Project, n.Region, n.Service)
+	// only one of Job or Service should be specified
+	if n.Service != "" {
+		return fmt.Sprintf("projects/%s/locations/%s/services/%s", n.Project, n.Region, n.Service)
+	}
+	return fmt.Sprintf("namespaces/%s/jobs/%s", n.Project, n.Job)
+}
+func (n RunResourceName) Name() string {
+	if n.Service != "" {
+		return n.Service
+	}
+	return n.Job
+}
+
+func (n RunResourceName) Type() ResourceType {
+	if n.Service != "" {
+		return typeService
+	}
+	return typeJob
 }
diff --git a/pkg/skaffold/deploy/cloudrun/deploy.go b/pkg/skaffold/deploy/cloudrun/deploy.go
index c69f5008b..f1fb48e8c 100644
--- a/pkg/skaffold/deploy/cloudrun/deploy.go
+++ b/pkg/skaffold/deploy/cloudrun/deploy.go
@@ -25,6 +25,7 @@ import (
 	"google.golang.org/api/googleapi"
 	"google.golang.org/api/option"
 	"google.golang.org/api/run/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	k8syaml "sigs.k8s.io/yaml"
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/access"
@@ -150,6 +151,7 @@ func (d *Deployer) getMonitor() *Monitor {
 func (d *Deployer) deployToCloudRun(ctx context.Context, out io.Writer, manifest []byte) 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...)
@@ -159,22 +161,56 @@ func (d *Deployer) deployToCloudRun(ctx context.Context, out io.Writer, manifest
 			ErrCode: proto.StatusCode_DEPLOY_GET_CLOUD_RUN_CLIENT_ERR,
 		})
 	}
-	service := &run.Service{}
-	if err = k8syaml.Unmarshal(manifest, service); err != nil {
+	// figure out which type we have:
+	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,
 		})
 	}
+	var resName *RunResourceName
+	switch {
+	case resource.GetAPIVersion() == "serving.knative.dev/v1" && resource.GetKind() == "Service":
+		resName, err = d.deployService(crclient, manifest, out)
+		// the accessor only supports services. Jobs don't run by themselves so port forwarding doesn't make sense.
+		if resName != nil {
+			d.accessor.AddResource(*resName)
+		}
+	case resource.GetAPIVersion() == "run.googleapis.com/v1" && resource.GetKind() == "Job":
+		resName, err = d.deployJob(crclient, manifest, out)
+	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,
+			})
+	}
+
+	if err != nil {
+		return err
+	}
+
+	d.getMonitor().Resources = append(d.getMonitor().Resources, *resName)
+	return nil
+}
+
+func (d *Deployer) deployService(crclient *run.APIService, manifest []byte, out io.Writer) (*RunResourceName, error) {
+	service := &run.Service{}
+	if err := k8syaml.Unmarshal(manifest, service); err != nil {
+		return nil, sErrors.NewError(fmt.Errorf("unable to unmarshal Cloud Run Service config"), &proto.ActionableErr{
+			Message: err.Error(),
+			ErrCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
+		})
+	}
 	if d.Project != "" {
 		service.Metadata.Namespace = d.Project
 	} else if service.Metadata.Namespace == "" {
-		return sErrors.NewError(fmt.Errorf("unable to detect project for Cloud Run"), &proto.ActionableErr{
+		return nil, sErrors.NewError(fmt.Errorf("unable to detect project for Cloud Run"), &proto.ActionableErr{
 			Message: "No Google Cloud project found in Cloud Run Manifest or Skaffold Config",
 			ErrCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
 		})
 	}
-
 	// we need to strip "skaffold.dev" from the run-id label because gcp labels don't support domains
 	runID, foundID := service.Metadata.Labels["skaffold.dev/run-id"]
 	if foundID {
@@ -188,7 +224,6 @@ func (d *Deployer) deployToCloudRun(ctx context.Context, out io.Writer, manifest
 			service.Spec.Template.Metadata.Labels["run-id"] = runID
 		}
 	}
-
 	resName := RunResourceName{
 		Project: service.Metadata.Namespace,
 		Region:  d.Region,
@@ -198,16 +233,13 @@ func (d *Deployer) deployToCloudRun(ctx context.Context, out io.Writer, manifest
 	parent := fmt.Sprintf("projects/%s/locations/%s", service.Metadata.Namespace, d.Region)
 
 	sName := resName.String()
-
-	d.getMonitor().Resources = append(d.getMonitor().Resources, ResourceName{path: sName, name: service.Metadata.Name})
-	d.accessor.AddResource(resName)
 	getCall := crclient.Projects.Locations.Services.Get(sName)
-	_, err = getCall.Do()
+	_, err := getCall.Do()
 
 	if err != nil {
 		gErr, ok := err.(*googleapi.Error)
 		if !ok || gErr.Code != http.StatusNotFound {
-			return sErrors.NewError(fmt.Errorf("error checking Cloud Run State: %w", err), &proto.ActionableErr{
+			return nil, sErrors.NewError(fmt.Errorf("error checking Cloud Run State: %w", err), &proto.ActionableErr{
 				Message: err.Error(),
 				ErrCode: proto.StatusCode_DEPLOY_CLOUD_RUN_GET_SERVICE_ERR,
 			})
@@ -220,13 +252,77 @@ func (d *Deployer) deployToCloudRun(ctx context.Context, out io.Writer, manifest
 		_, err = replaceCall.Do()
 	}
 	if err != nil {
-		return sErrors.NewError(fmt.Errorf("error deploying Cloud Run Service: %s", err), &proto.ActionableErr{
+		return nil, sErrors.NewError(fmt.Errorf("error deploying Cloud Run Service: %s", err), &proto.ActionableErr{
 			Message: err.Error(),
 			ErrCode: proto.StatusCode_DEPLOY_CLOUD_RUN_UPDATE_SERVICE_ERR,
 		})
 	}
-	// register status monitor
-	return nil
+	return &resName, nil
+}
+
+func (d *Deployer) deployJob(crclient *run.APIService, manifest []byte, out io.Writer) (*RunResourceName, error) {
+	job := &run.Job{}
+	if err := k8syaml.Unmarshal(manifest, job); err != nil {
+		return nil, sErrors.NewError(fmt.Errorf("unable to unmarshal Cloud Run Service config"), &proto.ActionableErr{
+			Message: err.Error(),
+			ErrCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
+		})
+	}
+	if d.Project != "" {
+		job.Metadata.Namespace = d.Project
+	} else if job.Metadata.Namespace == "" {
+		return nil, sErrors.NewError(fmt.Errorf("unable to detect project for Cloud Run"), &proto.ActionableErr{
+			Message: "No Google Cloud project found in Cloud Run Manifest or Skaffold Config",
+			ErrCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
+		})
+	}
+	// we need to strip "skaffold.dev" from the run-id label because gcp labels don't support domains
+	runID, foundID := job.Metadata.Labels["skaffold.dev/run-id"]
+	if foundID {
+		delete(job.Metadata.Labels, "skaffold.dev/run-id")
+		job.Metadata.Labels["run-id"] = runID
+	}
+	if job.Spec != nil && job.Spec.Template != nil && job.Spec.Template.Metadata != nil {
+		runID, foundID = job.Spec.Template.Metadata.Labels["skaffold.dev/run-id"]
+		if foundID {
+			delete(job.Spec.Template.Metadata.Labels, "skaffold.dev/run-id")
+			job.Spec.Template.Metadata.Labels["run-id"] = runID
+		}
+	}
+	resName := RunResourceName{
+		Project: job.Metadata.Namespace,
+		Region:  d.Region,
+		Job:     job.Metadata.Name,
+	}
+	output.Default.Fprintln(out, "Deploying Cloud Run service:\n\t", job.Metadata.Name)
+	parent := fmt.Sprintf("namespaces/%s", job.Metadata.Namespace)
+
+	sName := resName.String()
+	getCall := crclient.Namespaces.Jobs.Get(sName)
+	_, err := getCall.Do()
+
+	if err != nil {
+		gErr, ok := err.(*googleapi.Error)
+		if !ok || gErr.Code != http.StatusNotFound {
+			return nil, sErrors.NewError(fmt.Errorf("error checking Cloud Run State: %w", err), &proto.ActionableErr{
+				Message: err.Error(),
+				ErrCode: proto.StatusCode_DEPLOY_CLOUD_RUN_GET_SERVICE_ERR,
+			})
+		}
+		// This is a new service, we need to create it
+		createCall := crclient.Namespaces.Jobs.Create(parent, job)
+		_, err = createCall.Do()
+	} else {
+		replaceCall := crclient.Namespaces.Jobs.ReplaceJob(sName, job)
+		_, err = replaceCall.Do()
+	}
+	if err != nil {
+		return nil, sErrors.NewError(fmt.Errorf("error deploying Cloud Run Job: %s", err), &proto.ActionableErr{
+			Message: err.Error(),
+			ErrCode: proto.StatusCode_DEPLOY_CLOUD_RUN_UPDATE_SERVICE_ERR,
+		})
+	}
+	return &resName, nil
 }
 
 func (d *Deployer) deleteRunService(ctx context.Context, out io.Writer, dryRun bool, manifests manifest.ManifestList) error {
diff --git a/pkg/skaffold/deploy/cloudrun/status.go b/pkg/skaffold/deploy/cloudrun/status.go
index a420724cd..57d2d129a 100644
--- a/pkg/skaffold/deploy/cloudrun/status.go
+++ b/pkg/skaffold/deploy/cloudrun/status.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -42,7 +43,7 @@ var (
 )
 
 type Monitor struct {
-	Resources     []ResourceName
+	Resources     []RunResourceName
 	clientOptions []option.ClientOption
 	singleRun     singleflight.Group
 	labeller      *label.DefaultLabeller
@@ -52,11 +53,6 @@ type Monitor struct {
 	reportStatusTime    time.Duration
 }
 
-type ResourceName struct {
-	path string
-	name string
-}
-
 func NewMonitor(labeller *label.DefaultLabeller, clientOptions []option.ClientOption) *Monitor {
 	return &Monitor{
 		labeller:            labeller,
@@ -80,7 +76,16 @@ func (s *Monitor) Check(ctx context.Context, out io.Writer) error {
 func (s *Monitor) check(ctx context.Context, out io.Writer) error {
 	resources := make([]*runResource, len(s.Resources))
 	for i, resource := range s.Resources {
-		resources[i] = &runResource{path: resource.path, name: resource.name}
+		var sub runSubresource
+		switch resource.Type() {
+		case typeService:
+			sub = &runServiceResource{path: resource.String()}
+		case typeJob:
+			sub = &runJobResource{path: resource.String()}
+		default:
+			return fmt.Errorf("unable to monitor resource. Unknown type %s", resource.Type())
+		}
+		resources[i] = &runResource{resource: resource, sub: sub}
 	}
 	c := newCounter(len(resources))
 	cctx, cancel := context.WithCancel(ctx)
@@ -139,12 +144,10 @@ func (c *counter) remaining() string {
 }
 
 type runResource struct {
-	path      string
-	name      string
+	resource  RunResourceName
 	completed bool
 	status    Status
-	url       string
-	revision  string
+	sub       runSubresource
 }
 
 type Status struct {
@@ -152,6 +155,11 @@ type Status struct {
 	reported bool
 }
 
+type runSubresource interface {
+	getTerminalStatus(*run.APIService) (*run.GoogleCloudRunV1Condition, *proto.ActionableErr)
+	reportSuccess()
+}
+
 func (r *runResource) pollResourceStatus(ctx context.Context, deadline time.Duration, pollPeriod time.Duration, clientOptions []option.ClientOption, useGcpOptions bool) {
 	ticker := time.NewTicker(pollPeriod)
 	defer ticker.Stop()
@@ -159,6 +167,7 @@ func (r *runResource) pollResourceStatus(ctx context.Context, deadline time.Dura
 	defer cancel()
 	options := clientOptions
 	if useGcpOptions {
+		options = append(options, option.WithEndpoint(fmt.Sprintf("%s-run.googleapis.com", r.resource.Region)))
 		options = append(gcp.ClientOptions(ctx), options...)
 	}
 	crClient, err := run.NewService(ctx, options...)
@@ -211,37 +220,21 @@ func (r *runResource) ReportSinceLastUpdated() string {
 	if curStatus.ae == nil {
 		return ""
 	}
-	return fmt.Sprintf("%s: %s", r.name, curStatus.ae.Message)
+	return fmt.Sprintf("%s: %s", r.resource.Name(), curStatus.ae.Message)
 }
 
 func (r *runResource) checkStatus(crClient *run.APIService) {
-	call := crClient.Projects.Locations.Services.Get(r.path)
-	res, err := call.Do()
+	ready, err := r.sub.getTerminalStatus(crClient)
 	if err != nil {
-		r.updateStatus(&proto.ActionableErr{
-			ErrCode: proto.StatusCode_STATUSCHECK_KUBECTL_CLIENT_FETCH_ERR,
-			Message: fmt.Sprintf("Unable to check Cloud Run status: %v", err),
-		})
+		r.updateStatus(err)
 		return
 	}
-	// find the ready condition
-	var ready *run.GoogleCloudRunV1Condition
 
-	// If the status is still showing the old generation, treat it the
-	// same as no status being set.
-	if res.Status.ObservedGeneration == res.Metadata.Generation {
-		for _, cond := range res.Status.Conditions {
-			if cond.Type == "Ready" {
-				ready = cond
-				break
-			}
-		}
-	}
 	if ready == nil {
 		// No ready condition found, must not have started reconciliation yet
 		r.updateStatus(&proto.ActionableErr{
 			ErrCode: proto.StatusCode_STATUSCHECK_CONTAINER_WAITING_UNKNOWN,
-			Message: "Waiting for service to start",
+			Message: fmt.Sprintf("Waiting for %s to start", strings.ToLower(string(r.resource.Type()))),
 		})
 		return
 	}
@@ -250,21 +243,20 @@ func (r *runResource) checkStatus(crClient *run.APIService) {
 		r.completed = true
 		r.updateStatus(&proto.ActionableErr{
 			ErrCode: proto.StatusCode_STATUSCHECK_SUCCESS,
-			Message: "Service started",
+			Message: fmt.Sprintf("%s started", r.resource.Type()),
 		})
-		r.url = res.Status.Url
-		r.revision = res.Status.LatestReadyRevisionName
+
 	case "False":
 		r.completed = true
 		r.updateStatus(&proto.ActionableErr{
 			ErrCode: proto.StatusCode_STATUSCHECK_UNHEALTHY,
-			Message: fmt.Sprintf("Service failed to start: %v", ready.Message),
+			Message: fmt.Sprintf("%s failed to start: %v", r.resource.Type(), ready.Message),
 		})
 	default:
 		// status is unknown
 		r.updateStatus(&proto.ActionableErr{
 			ErrCode: proto.StatusCode_STATUSCHECK_UNKNOWN,
-			Message: fmt.Sprintf("Service starting: %v", ready.Message),
+			Message: fmt.Sprintf("%s starting: %v", r.resource.Type(), ready.Message),
 		})
 	}
 }
@@ -295,7 +287,7 @@ func (s *Monitor) printStatus(resources []*runResource, out io.Writer) bool {
 		}
 		allDone = false
 		if status := res.ReportSinceLastUpdated(); status != "" {
-			eventV2.ResourceStatusCheckEventUpdated(res.path, res.status.ae)
+			eventV2.ResourceStatusCheckEventUpdated(res.resource.String(), res.status.ae)
 			fmt.Fprintln(out, status)
 		}
 	}
@@ -309,13 +301,86 @@ func (s *Monitor) printStatusCheckSummary(out io.Writer, c *counter, r *runResou
 		// another deployment failed
 		return
 	}
-	eventV2.ResourceStatusCheckEventCompleted(r.path, curStatus.ae)
-	if r.url != "" {
-		eventV2.CloudRunServiceReady(r.path, r.url, r.revision)
-	}
+	eventV2.ResourceStatusCheckEventCompleted(r.resource.String(), curStatus.ae)
+	r.sub.reportSuccess()
 	if curStatus.ae.ErrCode != proto.StatusCode_STATUSCHECK_SUCCESS {
-		output.Default.Fprintln(out, fmt.Sprintf("Cloud Run Service %s failed with error: %s", r.name, curStatus.ae.Message))
+		output.Default.Fprintln(out, fmt.Sprintf("Cloud Run %s %s failed with error: %s", r.resource.Type(), r.resource.Name(), curStatus.ae.Message))
 	} else {
-		output.Default.Fprintln(out, fmt.Sprintf("Cloud Run Service %s finished: %s. %s", r.name, curStatus.ae.Message, c.remaining()))
+		output.Default.Fprintln(out, fmt.Sprintf("Cloud Run %s %s finished: %s. %s", r.resource.Type(), r.resource.Name(), curStatus.ae.Message, c.remaining()))
+	}
+}
+
+type runServiceResource struct {
+	path string
+
+	url            string
+	latestRevision string
+}
+
+func (r *runServiceResource) getTerminalStatus(crClient *run.APIService) (*run.GoogleCloudRunV1Condition, *proto.ActionableErr) {
+	call := crClient.Projects.Locations.Services.Get(r.path)
+	res, err := call.Do()
+	if err != nil {
+		return nil, &proto.ActionableErr{
+			ErrCode: proto.StatusCode_STATUSCHECK_KUBECTL_CLIENT_FETCH_ERR,
+			Message: fmt.Sprintf("Unable to check Cloud Run status: %v", err),
+		}
+	}
+	// find the ready condition
+	var ready *run.GoogleCloudRunV1Condition
+
+	// If the status is still showing the old generation, treat it the
+	// same as no status being set.
+	if res.Status.ObservedGeneration == res.Metadata.Generation {
+		for _, cond := range res.Status.Conditions {
+			if cond.Type == "Ready" {
+				ready = cond
+				break
+			}
+		}
 	}
+	r.latestRevision = res.Status.LatestCreatedRevisionName
+	r.url = res.Status.Url
+	return ready, nil
+}
+func (r *runServiceResource) reportSuccess() {
+	if r.url != "" {
+		eventV2.CloudRunServiceReady(r.path, r.url, r.latestRevision)
+	}
+}
+
+type runJobResource struct {
+	path            string
+	latestExecution string
+}
+
+func (r *runJobResource) getTerminalStatus(crClient *run.APIService) (*run.GoogleCloudRunV1Condition, *proto.ActionableErr) {
+	call := crClient.Namespaces.Jobs.Get(r.path)
+	res, err := call.Do()
+	if err != nil {
+		return nil, &proto.ActionableErr{
+			ErrCode: proto.StatusCode_STATUSCHECK_KUBECTL_CLIENT_FETCH_ERR,
+			Message: fmt.Sprintf("Unable to check Cloud Run status: %v", err),
+		}
+	}
+	// find the ready condition
+	var ready *run.GoogleCloudRunV1Condition
+
+	// If the status is still showing the old generation, treat it the
+	// same as no status being set.
+	if res.Status.ObservedGeneration == res.Metadata.Generation {
+		for _, cond := range res.Status.Conditions {
+			if cond.Type == "Ready" {
+				ready = cond
+				break
+			}
+		}
+	}
+	if res.Status.LatestCreatedExecution != nil {
+		r.latestExecution = res.Status.LatestCreatedExecution.Name
+	}
+	return ready, nil
+}
+
+func (r *runJobResource) reportSuccess() {
 }
diff --git a/pkg/skaffold/kubernetes/manifest/visitor.go b/pkg/skaffold/kubernetes/manifest/visitor.go
index 3ba7229c1..4b5a24297 100644
--- a/pkg/skaffold/kubernetes/manifest/visitor.go
+++ b/pkg/skaffold/kubernetes/manifest/visitor.go
@@ -171,6 +171,11 @@ var TransformAllowlist = map[apimachinery.GroupKind]latest.ResourceFilter{
 		Image:     []string{".spec.image.application", ".spec.image.init"},
 		Labels:    []string{".*"},
 	},
+	{Group: "run.googleapis.com", Kind: "Job"}: {
+		GroupKind: "Job.run.googleapis.com",
+		Image:     []string{".*"},
+		Labels:    []string{".*"},
+	},
 }
 
 // TransformDenylist is the default denylist on the set of kinds that can be transformed by Skaffold.


Test patch:
diff --git a/pkg/skaffold/deploy/cloudrun/deploy_test.go b/pkg/skaffold/deploy/cloudrun/deploy_test.go
index d1713c96c..8b6f2d5bc 100644
--- a/pkg/skaffold/deploy/cloudrun/deploy_test.go
+++ b/pkg/skaffold/deploy/cloudrun/deploy_test.go
@@ -40,7 +40,11 @@ import (
 	"github.com/GoogleContainerTools/skaffold/testutil"
 )
 
-func TestDeploy(tOuter *testing.T) {
+const (
+	configName = "default"
+)
+
+func TestDeployService(tOuter *testing.T) {
 	tests := []struct {
 		description    string
 		toDeploy       *run.Service
@@ -56,6 +60,8 @@ func TestDeploy(tOuter *testing.T) {
 			region:         "us-central1",
 			expectedPath:   "/v1/projects/testProject/locations/us-central1/services",
 			toDeploy: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
 				Metadata: &run.ObjectMeta{
 					Name: "test-service",
 				},
@@ -67,6 +73,8 @@ func TestDeploy(tOuter *testing.T) {
 			region:         "us-central1",
 			expectedPath:   "/v1/projects/testProject/locations/us-central1/services",
 			toDeploy: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
 				Metadata: &run.ObjectMeta{
 					Name:      "test-service",
 					Namespace: "my-project",
@@ -79,6 +87,8 @@ func TestDeploy(tOuter *testing.T) {
 			region:         "us-central1",
 			httpErr:        http.StatusUnauthorized,
 			toDeploy: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
 				Metadata: &run.ObjectMeta{
 					Name:      "test-service",
 					Namespace: "my-project",
@@ -90,6 +100,123 @@ func TestDeploy(tOuter *testing.T) {
 			description: "test no project specified",
 			region:      "us-central1",
 			toDeploy: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
+				Metadata: &run.ObjectMeta{
+					Name: "test-service",
+				},
+			},
+			errCode: proto.StatusCode_DEPLOY_READ_MANIFEST_ERR,
+		},
+	}
+	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, "test expecting error", test.httpErr)
+					return
+				}
+				if r.URL.Path != test.expectedPath {
+					http.Error(w, "unexpected path: "+r.URL.Path, http.StatusNotFound)
+				}
+				var service run.Service
+				body, err := io.ReadAll(r.Body)
+				if err != nil {
+					http.Error(w, "Unable to read body: "+err.Error(), http.StatusInternalServerError)
+					return
+				}
+				if err = json.Unmarshal(body, &service); err != nil {
+					http.Error(w, "Unable to parse service: "+err.Error(), http.StatusBadRequest)
+					return
+				}
+				b, err := json.Marshal(service)
+				if err != nil {
+					http.Error(w, "unable to marshal response: "+err.Error(), http.StatusInternalServerError)
+					return
+				}
+				w.Write(b)
+			}))
+
+			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
+			manifestList, _ := json.Marshal(test.toDeploy)
+			manifestsByConfig := manifest.NewManifestListByConfig()
+			manifestsByConfig.Add(configName, manifest.ManifestList{manifestList})
+			err := deployer.Deploy(context.Background(), os.Stderr, []graph.Artifact{}, manifestsByConfig)
+			if test.errCode == proto.StatusCode_OK && err != nil {
+				t.Fatalf("Expected success but got err: %v", err)
+			} else if test.errCode != proto.StatusCode_OK {
+				if err == nil {
+					t.Fatalf("Expected status code %s but got success", test.errCode)
+				}
+				sErr := err.(sErrors.Error)
+				if sErr.StatusCode() != test.errCode {
+					t.Fatalf("Expected status code %v but got %v", test.errCode, sErr.StatusCode())
+				}
+			}
+		})
+	}
+}
+
+func TestDeployJob(tOuter *testing.T) {
+	tests := []struct {
+		description    string
+		toDeploy       *run.Job
+		defaultProject string
+		region         string
+		expectedPath   string
+		httpErr        int
+		errCode        proto.StatusCode
+	}{
+		{
+			description:    "test deploy",
+			defaultProject: "testProject",
+			region:         "us-central1",
+			expectedPath:   "/apis/run.googleapis.com/v1/namespaces/testProject/jobs",
+			toDeploy: &run.Job{
+				ApiVersion: "run.googleapis.com/v1",
+				Kind:       "Job",
+				Metadata: &run.ObjectMeta{
+					Name: "test-service",
+				},
+			},
+		},
+		{
+			description:    "test deploy with specified project",
+			defaultProject: "testProject",
+			region:         "us-central1",
+			expectedPath:   "/apis/run.googleapis.com/v1/namespaces/testProject/jobs",
+			toDeploy: &run.Job{
+				ApiVersion: "run.googleapis.com/v1",
+				Kind:       "Job",
+				Metadata: &run.ObjectMeta{
+					Name:      "test-service",
+					Namespace: "my-project",
+				},
+			},
+		},
+		{
+			description:    "test permission denied on deploy errors",
+			defaultProject: "testProject",
+			region:         "us-central1",
+			httpErr:        http.StatusUnauthorized,
+			toDeploy: &run.Job{
+				ApiVersion: "run.googleapis.com/v1",
+				Kind:       "Job",
+				Metadata: &run.ObjectMeta{
+					Name:      "test-service",
+					Namespace: "my-project",
+				},
+			},
+			errCode: proto.StatusCode_DEPLOY_CLOUD_RUN_GET_SERVICE_ERR,
+		},
+		{
+			description: "test no project specified",
+			region:      "us-central1",
+			toDeploy: &run.Job{
+				ApiVersion: "run.googleapis.com/v1",
+				Kind:       "Job",
 				Metadata: &run.ObjectMeta{
 					Name: "test-service",
 				},
@@ -125,7 +252,6 @@ func TestDeploy(tOuter *testing.T) {
 				w.Write(b)
 			}))
 
-			configName := "default"
 			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
@@ -159,6 +285,8 @@ func TestDeployRewrites(tOuter *testing.T) {
 		{
 			description: "override run-id in service and template",
 			toDeploy: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
 				Metadata: &run.ObjectMeta{
 					Labels: map[string]string{
 						"skaffold.dev/run-id": "abc123",
@@ -178,6 +306,8 @@ func TestDeployRewrites(tOuter *testing.T) {
 			defaultProject: "test-project",
 			region:         "us-central1",
 			expected: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
 				Metadata: &run.ObjectMeta{
 					Labels: map[string]string{
 						"run-id": "abc123",
@@ -199,6 +329,8 @@ func TestDeployRewrites(tOuter *testing.T) {
 		{
 			description: "test deploy with overridden project",
 			toDeploy: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
 				Metadata: &run.ObjectMeta{
 					Name:      "test-service",
 					Namespace: "my-project",
@@ -207,6 +339,8 @@ func TestDeployRewrites(tOuter *testing.T) {
 			defaultProject: "test-project",
 			region:         "us-central1",
 			expected: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
 				Metadata: &run.ObjectMeta{
 					Name:      "test-service",
 					Namespace: "test-project",
@@ -274,6 +408,8 @@ func TestCleanup(tOuter *testing.T) {
 			region:         "us-central1",
 			expectedPath:   "/v1/projects/testProject/locations/us-central1/services/test-service",
 			toDelete: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
 				Metadata: &run.ObjectMeta{
 					Name: "test-service",
 				},
@@ -285,6 +421,8 @@ func TestCleanup(tOuter *testing.T) {
 			region:         "us-central1",
 			expectedPath:   "/v1/projects/testProject/locations/us-central1/services/test-service",
 			toDelete: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
 				Metadata: &run.ObjectMeta{
 					Name:      "test-service",
 					Namespace: "my-project",
@@ -297,6 +435,8 @@ func TestCleanup(tOuter *testing.T) {
 			region:         "us-central1",
 			expectedPath:   "/v1/projects/testProject/locations/us-central1/services/test-service",
 			toDelete: &run.Service{
+				ApiVersion: "serving.knative.dev/v1",
+				Kind:       "Service",
 				Metadata: &run.ObjectMeta{
 					Name: "test-service",
 				},
@@ -324,7 +464,6 @@ func TestCleanup(tOuter *testing.T) {
 				w.Write(b)
 			}))
 			defer ts.Close()
-			configName := "default"
 			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
diff --git a/pkg/skaffold/deploy/cloudrun/status_test.go b/pkg/skaffold/deploy/cloudrun/status_test.go
index 552317d51..d12ffafb4 100644
--- a/pkg/skaffold/deploy/cloudrun/status_test.go
+++ b/pkg/skaffold/deploy/cloudrun/status_test.go
@@ -42,27 +42,34 @@ func TestPrintSummaryStatus(t *testing.T) {
 	tests := []struct {
 		description string
 		pending     int32
-		path        string
-		name        string
+		resource    RunResourceName
 		ae          *proto.ActionableErr
 		expected    string
 	}{
 		{
 			description: "single resource running",
 			pending:     int32(1),
-			path:        "/projects/test/locations/region/services/test-service",
-			name:        "test-service",
+			resource:    RunResourceName{Project: "test", Region: "region", Service: "test-service"},
 			ae: &proto.ActionableErr{
 				ErrCode: proto.StatusCode_STATUSCHECK_SUCCESS,
 				Message: "Service started",
 			},
 			expected: "Cloud Run Service test-service finished: Service started. 1/10 deployment(s) still pending\n",
 		},
+		{
+			description: "single job running",
+			pending:     int32(1),
+			resource:    RunResourceName{Project: "test", Region: "region", Job: "test-job"},
+			ae: &proto.ActionableErr{
+				ErrCode: proto.StatusCode_STATUSCHECK_SUCCESS,
+				Message: "Job started",
+			},
+			expected: "Cloud Run Job test-job finished: Job started. 1/10 deployment(s) still pending\n",
+		},
 		{
 			description: "nothing prints if cancelled",
 			pending:     int32(3),
-			path:        "/projects/test/locations/region/services/test-service",
-			name:        "test-service",
+			resource:    RunResourceName{Project: "test", Region: " region", Service: "test-service"},
 			ae: &proto.ActionableErr{
 				ErrCode: proto.StatusCode_STATUSCHECK_USER_CANCELLED,
 				Message: "Deploy cancelled",
@@ -73,9 +80,9 @@ func TestPrintSummaryStatus(t *testing.T) {
 	for _, test := range tests {
 		testutil.Run(t, test.description, func(t *testutil.T) {
 			res := &runResource{
-				path:   test.path,
-				name:   test.name,
-				status: Status{ae: test.ae},
+				resource: test.resource,
+				status:   Status{ae: test.ae},
+				sub:      &runServiceResource{path: test.resource.String()},
 			}
 			s := NewMonitor(labeller, []option.ClientOption{})
 			out := new(bytes.Buffer)
@@ -87,17 +94,17 @@ func TestPrintSummaryStatus(t *testing.T) {
 		})
 	}
 }
-func TestPollResourceStatus(t *testing.T) {
+func TestPollServiceStatus(t *testing.T) {
 	tests := []struct {
 		description string
-		resource    ResourceName
+		resource    RunResourceName
 		responses   []run.Service
 		expected    *proto.ActionableErr
 		fail        bool
 	}{
 		{
 			description: "test basic check with one resource ready",
-			resource:    ResourceName{name: "test-service", path: "projects/tp/locations/tr/services/test-service"},
+			resource:    RunResourceName{Project: "tp", Region: "tr", Service: "test-service"},
 			responses: []run.Service{
 				{
 					ApiVersion: "serving.knative.dev/v1",
@@ -120,7 +127,7 @@ func TestPollResourceStatus(t *testing.T) {
 		},
 		{
 			description: "test basic check with one resource going ready after 1 non-ready",
-			resource:    ResourceName{name: "test-service", path: "projects/tp/locations/tr/services/test-service"},
+			resource:    RunResourceName{Project: " tp", Region: "tr", Service: "test-service"},
 			responses: []run.Service{
 				{
 					ApiVersion: "serving.knative.dev/v1",
@@ -159,7 +166,7 @@ func TestPollResourceStatus(t *testing.T) {
 		},
 		{
 			description: "test previous deploy failed reports correctly",
-			resource:    ResourceName{name: "test-service", path: "projects/tp/locations/tr/services/test-service"},
+			resource:    RunResourceName{Project: "tp", Region: "tr", Service: "test-service"},
 			responses: []run.Service{
 				{
 					ApiVersion: "serving.knative.dev/v1",
@@ -232,7 +239,118 @@ func TestPollResourceStatus(t *testing.T) {
 			defer ts.Close()
 			testEvent.InitializeState([]latest.Pipeline{{}})
 
-			resource := &runResource{path: test.resource.path, name: test.resource.name}
+			resource := &runResource{resource: test.resource, sub: &runServiceResource{path: test.resource.String()}}
+			ctx := context.Background()
+			resource.pollResourceStatus(ctx, 5*time.Second, 1*time.Second, []option.ClientOption{option.WithEndpoint(ts.URL), option.WithoutAuthentication()}, false)
+			t.CheckDeepEqual(test.expected, resource.status.ae, protocmp.Transform())
+		})
+	}
+}
+
+func TestPollJobStatus(t *testing.T) {
+	tests := []struct {
+		description string
+		resource    RunResourceName
+		responses   []run.Job
+		expected    *proto.ActionableErr
+		fail        bool
+	}{
+		{
+			description: "test basic check with one resource ready",
+			resource:    RunResourceName{Project: "tp", Region: "tr", Job: "test-job"},
+			responses: []run.Job{
+				{
+					ApiVersion: "run.googleapis.com/v1",
+					Metadata: &run.ObjectMeta{
+						Generation: 1,
+					},
+					Status: &run.JobStatus{
+						ObservedGeneration: 1,
+						Conditions: []*run.GoogleCloudRunV1Condition{
+							{
+								Type:   "Ready",
+								Status: "True",
+							},
+						},
+					},
+				},
+			},
+			expected: &proto.ActionableErr{Message: "Job started", ErrCode: proto.StatusCode_STATUSCHECK_SUCCESS},
+		},
+		{
+			description: "test previous deploy failed reports correctly",
+			resource:    RunResourceName{Project: "tp", Region: "tr", Job: "test-job"},
+			responses: []run.Job{
+				{
+					ApiVersion: "run.googleapis.com/v1",
+					Metadata: &run.ObjectMeta{
+						Generation: 2,
+					},
+					Status: &run.JobStatus{
+						ObservedGeneration: 1,
+						Conditions: []*run.GoogleCloudRunV1Condition{
+							{
+								Type:    "Ready",
+								Status:  "False",
+								Message: "Pre-existing failure",
+							},
+						},
+					},
+				},
+				{
+					ApiVersion: "run.googleapis.com/v1",
+					Metadata: &run.ObjectMeta{
+						Generation: 2,
+					},
+					Status: &run.JobStatus{
+						ObservedGeneration: 2,
+						Conditions: []*run.GoogleCloudRunV1Condition{
+							{
+								Type:   "Ready",
+								Status: "Unknown",
+							},
+						},
+					},
+				},
+				{
+					ApiVersion: "run.googleapis.com/v1",
+					Metadata: &run.ObjectMeta{
+						Generation: 2,
+					},
+					Status: &run.JobStatus{
+						ObservedGeneration: 2,
+						Conditions: []*run.GoogleCloudRunV1Condition{
+							{
+								Type:   "Ready",
+								Status: "True",
+							},
+						},
+					},
+				},
+			},
+			expected: &proto.ActionableErr{Message: "Job started", ErrCode: proto.StatusCode_STATUSCHECK_SUCCESS},
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			checkTimes := 0
+			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				if checkTimes >= len(test.responses) {
+					checkTimes = len(test.responses) - 1
+				}
+				resp := test.responses[checkTimes]
+				checkTimes++
+				b, err := json.Marshal(resp)
+				if err != nil {
+					http.Error(w, "unable to marshal response: "+err.Error(), http.StatusInternalServerError)
+					return
+				}
+				w.Write(b)
+			}))
+			defer ts.Close()
+			testEvent.InitializeState([]latest.Pipeline{{}})
+
+			resource := &runResource{resource: test.resource, sub: &runJobResource{path: test.resource.String()}}
 			ctx := context.Background()
 			resource.pollResourceStatus(ctx, 5*time.Second, 1*time.Second, []option.ClientOption{option.WithEndpoint(ts.URL), option.WithoutAuthentication()}, false)
 			t.CheckDeepEqual(test.expected, resource.status.ae, protocmp.Transform())
@@ -252,8 +370,7 @@ func TestMonitorPrintStatus(t *testing.T) {
 			description: "test basic print with one resource getting ready",
 			resources: []*runResource{
 				{
-					path:      "projects/tp/locations/tr/services/test-service",
-					name:      "test-service",
+					resource:  RunResourceName{Project: "tp", Region: "tr", Service: "test-service"},
 					completed: false,
 					status: Status{
 						reported: false,
@@ -271,8 +388,7 @@ func TestMonitorPrintStatus(t *testing.T) {
 			description: "test basic print with one resource ready and reported, one not ready",
 			resources: []*runResource{
 				{
-					path:      "projects/tp/locations/tr/services/test-service1",
-					name:      "test-service1",
+					resource:  RunResourceName{Project: "tp", Region: "tr", Service: "test-service1"},
 					completed: true,
 					status: Status{
 						reported: true,
@@ -283,8 +399,7 @@ func TestMonitorPrintStatus(t *testing.T) {
 					},
 				},
 				{
-					path:      "projects/tp/locations/tr/services/test-service2",
-					name:      "test-service2",
+					resource:  RunResourceName{Project: "tp", Region: "tr", Service: "test-service2"},
 					completed: false,
 					status: Status{
 						reported: false,
@@ -302,8 +417,7 @@ func TestMonitorPrintStatus(t *testing.T) {
 			description: "test resources completed",
 			resources: []*runResource{
 				{
-					path:      "projects/tp/locations/tr/services/test-service",
-					name:      "test-service",
+					resource:  RunResourceName{Project: "tp", Region: "tr", Service: "test-service"},
 					completed: true,
 				},
 			},