When deploying services to Google Cloud Run via CI/CD pipelines, dry-run validations are essential for catching configuration drift, schema errors, and quota bottlenecks before modifying live traffic. Cloud Run Admin API v2 supports this via the validateOnly=true query parameter on mutation endpoints like projects.locations.services.patch.

However, developers often encounter a confusing response pattern: the request returns an HTTP 200 OK containing an Operation object without a done: true field, yet subsequent polling calls to operations.get return 404 Not Found or 403 Forbidden. This article clarifies the canonical way to confirm validation success and explains the lifecycle of Cloud Run validation operations.

The Short Answer: HTTP 200 OK Is Terminal Success

In the Cloud Run Admin API v2, an HTTP 200 OK response to a request with validateOnly=true represents a complete, successful validation. You should not attempt to poll the returned Operation ID.

Cloud Run validates resource definitions synchronously. Because no backend workload is scheduled or persistent resource altered, the returned Operation resource is never registered in Cloud Run's operation persistence layer. Attempting to retrieve it via operations.get will result in a 404 Not Found.

Understanding AIP-151 and Cloud Run API Design

Google's API Improvement Proposal (AIP-151) defines standard guidelines for requests with validate_only:

  • Validation must inspect syntax, resource validity, and necessary permissions.
  • No state changes must be committed.
  • If the normal method returns a Long-Running Operation (LRO), the method may return an LRO or an immediate result.

Because the Cloud Run v2 services.patch endpoint is defined via gRPC to return google.longrunning.Operation, the API gateway must return a schema-conforming Operation object even when validateOnly=true is set.

{
  "name": "projects/PROJECT/locations/REGION/operations/OPERATION_ID",
  "metadata": {
    "@type": "type.googleapis.com/google.cloud.run.v2.Service",
    "name": "projects/PROJECT/locations/REGION/services/SERVICE"
  }
}

Notice that done: true is missing and the response map is omitted. While AIP-151 allows long-running asynchronous validations, Cloud Run executes all validations synchronously. The generated OPERATION_ID is an ephemeral identifier; no backend worker is spawned, and the operation record is not stored in the operations database.

Why Did operations.get Return 403 and 404?

The behavior observed when querying the operation ID reflects Google Cloud's security and persistence tiers:

  • HTTP 403 Forbidden: Often occurs if the caller lacks the run.operations.get permission, or if the API gateway denies access to an unindexed operation before reaching the resource handler.
  • HTTP 404 Not Found: Once authenticated with proper IAM roles (e.g., Cloud Run Viewer / Admin), the operations service checks its database for the specified operation ID. Because the dry-run never wrote an operation record to storage, it returns 404 Not Found.

Does forceNewRevision=true Impact Validation?

Setting forceNewRevision=true tells Cloud Run to generate a new revision even if the revision template is identical to the current active revision. When combined with validateOnly=true:

  • Cloud Run verifies that a new revision can be provisioned (e.g., checking revision naming constraints, service account permissions, and image metadata).
  • It does not change the lifecycle of the operation. The check remains purely synchronous and does not trigger an asynchronous queue.

How to Implement Cloud Run Dry-Run in CI/CD

To safely validate Cloud Run deployments, treat HTTP response status codes as the single source of truth. Do not parse the Operation ID for subsequent polling.

Example: Validating a Service with cURL

RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/validate_out.json -X PATCH \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -d @service.json \
  "https://run.googleapis.com/v2/projects/MY_PROJECT/locations/us-central1/services/my-service?validateOnly=true&forceNewRevision=true")

if [ "$RESPONSE" -eq 200 ]; then
  echo "Validation successful! Service definition is valid."
else
  echo "Validation failed with HTTP status $RESPONSE:"
  cat /tmp/validate_out.json
  exit 1
fi

Example: Python Google Cloud Client Handling

When using client libraries or the REST interface in Python, catch GoogleAPICallError exceptions to detect validation failures:

from google.api_core.exceptions import GoogleAPICallError
from google.cloud import run_v2

def validate_service_patch(service_path, service_payload):
    client = run_v2.ServicesClient()
    request = run_v2.UpdateServiceRequest(
        service=service_payload,
        validate_only=True,
        allow_missing=False
    )
    
    try:
        # Synchronous validation check
        operation = client.update_service(request=request)
        print("Validation succeeded. Manifest is valid for deployment.")
        # Do NOT call operation.result() here!
        return True
    except GoogleAPICallError as error:
        print(f"Validation failed: {error.message}")
        if error.details:
            for detail in error.details:
                print(f" - {detail}")
        return False

Key Takeaways

  • Do not poll validation operations: Cloud Run v2 operations returned when validateOnly=true are non-persistent stubs created to satisfy the API signature.
  • HTTP 200 means pass: If the request returns 200 OK, the parameters, template fields, IAM checks, and resource constraints have passed validation.
  • HTTP 4xx means fail: Syntax errors, nonexistent container images, missing environment variables, or schema mismatches will return a 400 Bad Request or 403 Forbidden immediately.