Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 268 additions & 0 deletions .github/workflows/cloud-run-deploy-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
name: Deploy existing GAR image to Cloud Run

on:
workflow_call:
inputs:
GAR_REPO_DOMAIN:
description: GAR Docker host, for example us-central1-docker.pkg.dev.
required: true
type: string
GAR_REPO_PATH:
description: GAR image path after the host, for example project/repository/image.
required: true
type: string
GITHUB_ENVIRONMENT_NAME:
description: GitHub environment containing target-specific variables and protections.
required: true
type: string
IMAGE_DIGEST:
description: Exact sha256 image digest. Mutually exclusive with IMAGE_TAG.
required: false
type: string
default: ''
IMAGE_TAG:
description: Existing 40-character Git commit tag in GAR. Mutually exclusive with IMAGE_DIGEST.
required: false
type: string
default: ''
ROLLBACK:
description: Skip migration and deploy only IMAGE_DIGEST.
required: false
type: boolean
default: false
RUNNER_LABELS:
description: JSON array of runner labels.
required: false
type: string
default: '["ubuntu-latest"]'
GCP_APPLICATION_PROJECT:
description: Optional override: GCP project to host Cloud Run Service
required: false
type: string
default: ''
CLOUD_RUN_REGION:
description: Optional override: GCP region to deploy Cloud Run Service
required: false
type: string
default: ''
CLOUD_RUN_SERVICE:
description: Optional override: Name of Cloud Run service
required: false
type: string
default: ''
CLOUD_RUN_MIGRATION_JOB:
description: Optional override: Empty means this application has no migration job.
required: false
type: string
default: ''
SERVICE_ACCOUNT:
description: Optional override: for GCP_DEPLOYER_SERVICE_ACCOUNT in the GitHub environment.
required: false
type: string
default: ''
WORKLOAD_IDENTITY_PROVIDER:
description: Optional override: for GCP_WORKLOAD_IDENTITY_PROVIDER in the GitHub environment.
required: false
type: string
default: ''
outputs:
image_uri:
description: Immutable GAR image URI deployed to Cloud Run.
value: ${{ jobs.deploy.outputs.image_uri }}
image_digest:
description: sha256 digest deployed to Cloud Run.
value: ${{ jobs.deploy.outputs.image_digest }}
migration_execution:
description: Cloud Run migration execution, empty when skipped.
value: ${{ jobs.deploy.outputs.migration_execution }}
revision:
description: Ready Cloud Run revision.
value: ${{ jobs.deploy.outputs.revision }}
service_url:
description: URL of the ready Cloud Run service.
value: ${{ jobs.deploy.outputs.service_url }}

permissions:
contents: read

jobs:
deploy:
runs-on: ${{ fromJSON(inputs.RUNNER_LABELS) }}
permissions:
contents: read
id-token: write
environment:
name: ${{ inputs.GITHUB_ENVIRONMENT_NAME }}
concurrency:
group: ${{ github.repository }}-cloud-run-${{ inputs.GITHUB_ENVIRONMENT_NAME }}
cancel-in-progress: false
outputs:
image_uri: ${{ steps.image.outputs.image_uri }}
image_digest: ${{ steps.image.outputs.image_digest }}
migration_execution: ${{ steps.migrate.outputs.migration_execution }}
revision: ${{ steps.service.outputs.revision }}
service_url: ${{ steps.service.outputs.service_url }}
steps:
- name: Resolve and validate target configuration
id: config
env:
APPLICATION_PROJECT: ${{ inputs.GCP_APPLICATION_PROJECT || vars.GCP_APPLICATION_PROJECT }}
CLOUD_RUN_REGION: ${{ inputs.CLOUD_RUN_REGION || vars.CLOUD_RUN_REGION }}
CLOUD_RUN_SERVICE: ${{ inputs.CLOUD_RUN_SERVICE || vars.CLOUD_RUN_SERVICE }}
MIGRATION_JOB_NAME: ${{ inputs.CLOUD_RUN_MIGRATION_JOB || vars.CLOUD_RUN_MIGRATION_JOB }}
SERVICE_ACCOUNT: ${{ inputs.SERVICE_ACCOUNT || vars.GCP_DEPLOYER_SERVICE_ACCOUNT }}
WORKLOAD_IDENTITY_PROVIDER: ${{ inputs.WORKLOAD_IDENTITY_PROVIDER || vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
run: |
set -euo pipefail
[[ "$APPLICATION_PROJECT" =~ ^[a-z][a-z0-9-]{4,28}[a-z0-9]$ ]]
[[ "$CLOUD_RUN_REGION" =~ ^[a-z]+[a-z0-9-]*[a-z0-9]$ ]]
[[ "$CLOUD_RUN_SERVICE" =~ ^[a-z]([a-z0-9-]*[a-z0-9])?$ ]]
if [[ -n "$MIGRATION_JOB_NAME" ]]; then
[[ "$MIGRATION_JOB_NAME" =~ ^[a-z]([a-z0-9-]*[a-z0-9])?$ ]]
fi
[[ "$SERVICE_ACCOUNT" =~ ^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$ ]]
[[ "$WORKLOAD_IDENTITY_PROVIDER" =~ ^projects/[0-9]+/locations/global/workloadIdentityPools/[a-z0-9-]+/providers/[a-z0-9-]+$ ]]
{
printf 'application_project=%s\n' "$APPLICATION_PROJECT"
printf 'cloud_run_region=%s\n' "$CLOUD_RUN_REGION"
printf 'cloud_run_service=%s\n' "$CLOUD_RUN_SERVICE"
printf 'migration_job_name=%s\n' "$MIGRATION_JOB_NAME"
printf 'service_account=%s\n' "$SERVICE_ACCOUNT"
printf 'workload_identity_provider=%s\n' "$WORKLOAD_IDENTITY_PROVIDER"
} >> "$GITHUB_OUTPUT"

- name: Validate artifact selection
env:
GAR_REPO_DOMAIN: ${{ inputs.GAR_REPO_DOMAIN }}
GAR_REPO_PATH: ${{ inputs.GAR_REPO_PATH }}
IMAGE_DIGEST: ${{ inputs.IMAGE_DIGEST }}
IMAGE_TAG: ${{ inputs.IMAGE_TAG }}
ROLLBACK: ${{ inputs.ROLLBACK }}
run: |
set -euo pipefail
[[ "$GAR_REPO_DOMAIN" =~ ^[a-z0-9-]+-docker\.pkg\.dev$ ]]
[[ "$GAR_REPO_PATH" =~ ^[a-z0-9][a-z0-9._/-]*$ ]]
if [[ -n "$IMAGE_DIGEST" && -n "$IMAGE_TAG" ]] || [[ -z "$IMAGE_DIGEST" && -z "$IMAGE_TAG" ]]; then
printf 'supply exactly one of IMAGE_DIGEST or IMAGE_TAG\n' >&2
exit 1
fi
if [[ -n "$IMAGE_DIGEST" ]]; then
[[ "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
else
[[ "$IMAGE_TAG" =~ ^[0-9a-f]{40}$ ]]
fi
if [[ "$ROLLBACK" == 'true' && -z "$IMAGE_DIGEST" ]]; then
printf 'rollback requires IMAGE_DIGEST and does not accept IMAGE_TAG\n' >&2
exit 1
fi

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7
with:
workload_identity_provider: ${{ steps.config.outputs.workload_identity_provider }}
service_account: ${{ steps.config.outputs.service_account }}

- name: Set up gcloud
uses: google-github-actions/setup-gcloud@6189d56e4096ee891640bb02ac264be376592d6a # v2.1.2

- name: Resolve immutable image
id: image
env:
GAR_REPO_DOMAIN: ${{ inputs.GAR_REPO_DOMAIN }}
GAR_REPO_PATH: ${{ inputs.GAR_REPO_PATH }}
IMAGE_DIGEST: ${{ inputs.IMAGE_DIGEST }}
IMAGE_TAG: ${{ inputs.IMAGE_TAG }}
run: |
set -euo pipefail
IMAGE_REPOSITORY="${GAR_REPO_DOMAIN}/${GAR_REPO_PATH}"
if [[ -n "$IMAGE_TAG" ]]; then
IMAGE_TAG_URI="${IMAGE_REPOSITORY}:${IMAGE_TAG}"
IMAGE_DIGEST="$(gcloud artifacts docker images describe "$IMAGE_TAG_URI" --format='value(image_summary.digest)')"
fi
[[ "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
IMAGE_URI="${IMAGE_REPOSITORY}@${IMAGE_DIGEST}"
{
printf 'image_uri=%s\n' "$IMAGE_URI"
printf 'image_digest=%s\n' "$IMAGE_DIGEST"
} >> "$GITHUB_OUTPUT"

- name: Update and execute optional migration job
id: migrate
if: ${{ inputs.ROLLBACK == false && steps.config.outputs.migration_job_name != '' }}
env:
APPLICATION_PROJECT: ${{ steps.config.outputs.application_project }}
CLOUD_RUN_REGION: ${{ steps.config.outputs.cloud_run_region }}
IMAGE_URI: ${{ steps.image.outputs.image_uri }}
MIGRATION_JOB_NAME: ${{ steps.config.outputs.migration_job_name }}
run: |
set -euo pipefail
[[ -n "$MIGRATION_JOB_NAME" ]]
gcloud run jobs update "$MIGRATION_JOB_NAME" \
--project "$APPLICATION_PROJECT" \
--region "$CLOUD_RUN_REGION" \
--image "$IMAGE_URI" \
--quiet
MIGRATION_EXECUTION="$(gcloud run jobs execute "$MIGRATION_JOB_NAME" \
--project "$APPLICATION_PROJECT" \
--region "$CLOUD_RUN_REGION" \
--wait \
--format='value(metadata.name)')"
[[ -n "$MIGRATION_EXECUTION" ]]
printf 'migration_execution=%s\n' "$MIGRATION_EXECUTION" >> "$GITHUB_OUTPUT"

- name: Deploy service and verify readiness
id: service
env:
APPLICATION_PROJECT: ${{ steps.config.outputs.application_project }}
CLOUD_RUN_REGION: ${{ steps.config.outputs.cloud_run_region }}
CLOUD_RUN_SERVICE: ${{ steps.config.outputs.cloud_run_service }}
IMAGE_URI: ${{ steps.image.outputs.image_uri }}
run: |
set -euo pipefail
gcloud run services update "$CLOUD_RUN_SERVICE" \
--project "$APPLICATION_PROJECT" \
--region "$CLOUD_RUN_REGION" \
--image "$IMAGE_URI" \
--quiet
SERVICE_JSON="$(gcloud run services describe "$CLOUD_RUN_SERVICE" \
--project "$APPLICATION_PROJECT" \
--region "$CLOUD_RUN_REGION" \
--format=json)"
printf '%s' "$SERVICE_JSON" | jq -e '
any(.status.conditions[]?; .type == "Ready" and .status == "True") and
(.status.latestReadyRevisionName | type == "string" and length > 0) and
(.status.url | type == "string" and length > 0)
' >/dev/null
REVISION="$(printf '%s' "$SERVICE_JSON" | jq -er '.status.latestReadyRevisionName')"
SERVICE_URL="$(printf '%s' "$SERVICE_JSON" | jq -er '.status.url')"
READY_IMAGE_URI="$(gcloud run revisions describe "$REVISION" \
--project "$APPLICATION_PROJECT" \
--region "$CLOUD_RUN_REGION" \
--format='value(spec.containers[0].image)')"
[[ "$READY_IMAGE_URI" == "$IMAGE_URI" ]]
{
printf 'revision=%s\n' "$REVISION"
printf 'service_url=%s\n' "$SERVICE_URL"
} >> "$GITHUB_OUTPUT"

- name: Write deployment summary
env:
GITHUB_ENVIRONMENT_NAME: ${{ inputs.GITHUB_ENVIRONMENT_NAME }}
IMAGE_DIGEST: ${{ steps.image.outputs.image_digest }}
IMAGE_URI: ${{ steps.image.outputs.image_uri }}
MIGRATION_EXECUTION: ${{ steps.migrate.outputs.migration_execution }}
REVISION: ${{ steps.service.outputs.revision }}
ROLLBACK: ${{ inputs.ROLLBACK }}
SERVICE_URL: ${{ steps.service.outputs.service_url }}
run: |
set -euo pipefail
{
printf '## Cloud Run deployment\n\n'
printf -- '- Environment: `%s`\n' "$GITHUB_ENVIRONMENT_NAME"
printf -- '- Rollback: `%s`\n' "$ROLLBACK"
printf -- '- Image: `%s`\n' "$IMAGE_URI"
printf -- '- Digest: `%s`\n' "$IMAGE_DIGEST"
printf -- '- Migration execution: `%s`\n' "${MIGRATION_EXECUTION:-not run}"
printf -- '- Ready revision: `%s`\n' "$REVISION"
printf -- '- Service URL: %s\n' "$SERVICE_URL"
} >> "$GITHUB_STEP_SUMMARY"
Loading