Skip to content

ci: name Cloud Run revisions so a failed one cannot wedge a service #31

ci: name Cloud Run revisions so a failed one cannot wedge a service

ci: name Cloud Run revisions so a failed one cannot wedge a service #31

Workflow file for this run

# Builds the Spring Boot 3 demos linked from springdoc.org/v2 and deploys them to Cloud Run.
name: Demos
on:
push:
branches: [spring-boot-3]
paths-ignore: ['**.md']
workflow_dispatch:
# One deploy at a time per branch; a superseded build is not worth finishing.
concurrency:
group: demos-${{ github.ref }}
cancel-in-progress: true
env:
MAVEN_ARGS: -B --no-transfer-progress -Dmaven.artifact.threads=16
MAVEN_OPTS: -Xmx3g
REGISTRY: docker.io/springdocdemos
REGION: europe-west1
# Only the demos the v2 page links live; the rest of the branch is not published.
# Most of these modules share their artifactId with a Boot 4 demo on master, so they
# would publish Boot 3 images into repositories those demos already own. They each
# name the repository they mean instead; see jib.image.name and jib.image.prefix
# in the poms.
MODULES: >-
demo-spring-boot-3-webmvc,demo-spring-boot-3-webflux,demo-spring-boot-3-webflux-functional,
demo-spring-boot-webmvc-scalar,demo-spring-boot-webflux-scalar,demo-spring-hateoas,
demo-spring-cloud-function/spring-cloud-function-webmvc,
demo-spring-cloud-function/spring-cloud-function-webflux,
demo-microservices/config-service,demo-microservices/discovery-service,
demo-microservices/employee-service,demo-microservices/department-service,
demo-microservices/organization-service,demo-microservices/gateway-service
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 1
- uses: actions/setup-java@v5
with:
java-version: '17'
distribution: temurin
cache: maven
# jib picks the credentials up from the docker config this action writes.
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# The extra sha tag is what the deploy job pulls, so a redeploy is immutable.
- name: Build and push the images
run: mvn $MAVEN_ARGS -Pjib -pl "${MODULES// /}" -am package jib:build -T1C -Djib.to.tags=${{ github.sha }}
deploy:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
include:
- service: demo-spring-boot-3-webmvc
# A demo with a "definition" needs more than one container, which only a whole
# service definition can express; the rest deploy from flags. See .github/cloudrun.
- service: demo-spring-boot-3-webflux
definition: mongo-sidecar.yaml
- service: demo-spring-boot-3-webflux-functional
- service: demo-spring-boot-3-webmvc-scalar
- service: demo-spring-boot-3-webflux-scalar
definition: mongo-sidecar.yaml
- service: demo-spring-boot-3-hateoas
- service: demo-spring-boot-3-cloud-function-webmvc
- service: demo-spring-boot-3-cloud-function-webflux
- service: demo-spring-boot-3-microservices
definition: microservices.yaml
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 1
# Keyless auth through workload identity federation; no service account key.
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
# Cloud Run fails to import a multi-platform manifest list, and jib publishes
# amd64 plus arm64. Pick the amd64 child digest rather than drop arm64 support.
- name: Resolve the amd64 digest
id: image
if: ${{ !matrix.definition }}
run: |
repo=springdocdemos/${{ matrix.service }}
token=$(curl -fsSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$repo:pull" | jq -r .token)
digest=$(curl -fsSL -H "Authorization: Bearer $token" \
-H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
"https://registry-1.docker.io/v2/$repo/manifests/${{ github.sha }}" \
| jq -r '.manifests[] | select(.platform.architecture == "amd64" and .platform.os == "linux") | .digest')
test -n "$digest"
echo "ref=${{ env.REGISTRY }}/${{ matrix.service }}@$digest" >> "$GITHUB_OUTPUT"
- name: Warm the Docker Hub mirror
if: ${{ !matrix.definition }}
run: .github/scripts/cloudrun/warm-mirror.sh '${{ steps.image.outputs.ref }}'
# SERVER_PORT overrides the port each demo hardcodes in application.yml.
# no-allow-unauthenticated keeps the run.app URL closed: only the Cloudflare
# worker holds a service account with run.invoker, so there is no way around it.
- uses: google-github-actions/deploy-cloudrun@v2
if: ${{ !matrix.definition }}
with:
service: ${{ matrix.service }}
image: ${{ steps.image.outputs.ref }}
region: ${{ env.REGION }}
env_vars: SERVER_PORT=8080
# revision-suffix for the same reason the definitions carry a name; see the
# comment on REVISION below.
flags: >-
--no-allow-unauthenticated --port=8080 --cpu=1 --memory=512Mi --cpu-boost
--min-instances=0 --max-instances=2 --concurrency=80 --timeout=60s
--revision-suffix=${{ github.run_number }}-${{ github.run_attempt }}
--service-account=cloudrun-runtime@springdoc.iam.gserviceaccount.com
# Sidecars cannot be expressed as deploy flags, so the demos that need one go
# through the service definition instead. Replacing a service carries no IAM,
# so they stay closed to anyone but the Cloudflare worker.
- uses: google-github-actions/setup-gcloud@v2
if: ${{ matrix.definition }}
- name: Deploy from a service definition
if: ${{ matrix.definition }}
# Left to itself Cloud Run names a revision after the service generation, which
# only moves when the spec changes. jib builds reproducibly, so a commit that
# leaves a demo's code alone renders a byte-identical definition and replace
# resolves to the revision the previous run already created: if that one failed
# to import, every later run re-reports its failure in two seconds without ever
# attempting a deploy, and nothing short of a code change can free the service.
# Naming the revision here makes every attempt a real one. The run number and
# attempt keep it unique across re-runs of the same commit.
env:
REVISION: ${{ matrix.service }}-${{ github.run_number }}-${{ github.run_attempt }}
run: |
.github/scripts/cloudrun/render-service.sh \
".github/cloudrun/${{ matrix.definition }}" '${{ matrix.service }}' '${{ github.sha }}' > /tmp/service.yaml
.github/scripts/cloudrun/warm-mirror.sh \
$(grep -oE 'docker\.io/[^ ]+@sha256:[0-9a-f]+' /tmp/service.yaml | sort -u)
# A revision imports every one of its containers before it can start, and the
# microservices stack has six, so one cold mirror entry loses the whole deploy
# to "Container import failed". Each retry needs a revision name of its own, or
# it would land on the one that just failed and be answered from its status
# rather than retried.
for attempt in 1 2 3; do
sed "s|__REVISION__|$REVISION-$attempt|" /tmp/service.yaml > /tmp/revision.yaml
if gcloud run services replace /tmp/revision.yaml --region="$REGION" --quiet; then
exit 0
fi
echo "::warning::replace attempt $attempt failed; retrying in $((attempt * 20))s"
sleep $((attempt * 20))
done
echo "::error::gcloud run services replace failed three times"
exit 1