ci: name Cloud Run revisions so a failed one cannot wedge a service #31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |