Google Cloud Build

You want every pull request built by Cloud Build to keep your mutation score. This page adds Gooze to a cloudbuild.yaml: one step first, then a baseline in Artifact Registry so pull requests test only what changed, then triggers and a gate. The setups mirror the GitHub Actions and GitLab CI guides.

1. Simplest: run

Install Gooze and run the whole suite in one step. No registry, no caching. A mutation run starts one go test per mutant, so give -p as many workers as the machine has vCPUs, and raise the build timeout (60 minutes by default) if a full run needs longer. For the calc package, the build log ends with the line you saw locally, and the build passes, because no threshold is set:

# cloudbuild.yamlsteps:  - id: mutation    name: golang:1.27        # official Go image: go is already installed    script: |      go install gooze.dev/pkg/gooze@latest      gooze run -p 8 ./...
timeout: 7200s               # the default is 60 minutesoptions:  machineType: E2_HIGHCPU_8  # 8 vCPUs for -p 8
Mutation score: 84.62% (11 killed, 2 survived; 0 cached, 13 tested)

The script field runs the lines as a shell script; it needs no entrypoint or args. Pin a release (gooze@vX.Y.Z) instead of @latest once the pipeline works, so a new Gooze release can't change results unannounced.

2. Create a registry for the reports

Gooze stores its reports as an OCI artifact (see Report artifacts), and Artifact Registry stores OCI artifacts in Docker-format repositories. Google Container Registry has been shut down; Artifact Registry replaces it and also serves gcr.io addresses, so a gcr.io/PROJECT/... reference in an Artifact Registry gcr.io repository works the same way. Create the repository once and let your build's service account write to it:

REGION=us-central1PROJECT_ID=$(gcloud config get-value project)
# A Docker-format repository stores OCI artifacts, including gooze reports.gcloud artifacts repositories create gooze \  --repository-format=docker \  --location="$REGION" \  --description="gooze mutation reports"
# Let the service account your trigger runs as read and write it.gcloud artifacts repositories add-iam-policy-binding gooze \  --location="$REGION" \  --member="serviceAccount:BUILD_SERVICE_ACCOUNT" \  --role=roles/artifactregistry.writer

BUILD_SERVICE_ACCOUNT is the service account shown on your trigger. Projects whose first build ran after mid-2024 use the Compute Engine default service account (PROJECT_NUMBER-compute@developer.gserviceaccount.com); earlier projects keep PROJECT_NUMBER@cloudbuild.gserviceaccount.com.

3. Cached baseline in Artifact Registry

Every build pulls the :main baseline for the cache (incremental runs), so files that didn't change, such as an untouched calc.go, are not re-tested. Only builds of main push the new baseline back; pull request builds only read it.

# cloudbuild.yamlsteps:  # A short-lived Artifact Registry token, written where the gooze steps read it.  - id: token    name: gcr.io/cloud-builders/gcloud    script: gcloud auth print-access-token > /registry/token
  - id: mutation    name: golang:1.27    script: |      set -e      go install gooze.dev/pkg/gooze@latest      export GOOZE_REGISTRY_USERNAME=oauth2accesstoken      export GOOZE_REGISTRY_PASSWORD="$(cat /registry/token)"      REPORTS_REF="${_REGION}-docker.pkg.dev/${PROJECT_ID}/gooze/reports:main"
      # Reuse the main baseline so unchanged files are skipped (the cache).      gooze report pull "$REPORTS_REF" || echo "no baseline yet"      gooze run -p 8 ./...      gooze report view
  # The token lasts about an hour, so fetch a fresh one for the push.  - id: push-token    name: gcr.io/cloud-builders/gcloud    script: gcloud auth print-access-token > /registry/token
  - id: push    name: golang:1.27    script: |      set -e      # Only builds of main publish the baseline; pull requests only read it.      if [ -n "${_PR_NUMBER:-}" ] || [ "$BRANCH_NAME" != main ]; then        echo "not main: baseline not pushed"        exit 0      fi      export GOOZE_REGISTRY_USERNAME=oauth2accesstoken      export GOOZE_REGISTRY_PASSWORD="$(cat /registry/token)"      gooze report push "${_REGION}-docker.pkg.dev/${PROJECT_ID}/gooze/reports:main"
substitutions:  _REGION: us-central1       # where the gooze repository lives
timeout: 7200soptions:  machineType: E2_HIGHCPU_8  # Substitutions such as PROJECT_ID and BRANCH_NAME become environment  # variables in script: steps.  automapSubstitutions: true  # Shared by every step: /go keeps the installed gooze binary and the Go  # module cache, /registry holds the token.  volumes:    - name: go      path: /go    - name: registry      path: /registry
  • Login. Artifact Registry accepts the username oauth2accesstoken with an access token as the password. The gcloud builder prints one for the build's service account, and Gooze reads it from GOOZE_REGISTRY_USERNAME and GOOZE_REGISTRY_PASSWORD.
  • Volumes. Steps share only /workspace by default. The go volume keeps /go, where go install puts gooze, so the push step can call it without reinstalling. The token lives in its own volume, outside your source.
  • Pull requests. PR builds set _PR_NUMBER; the push step skips them and any branch other than main.

4. Pull requests: only the changed lines

A pull request that changes three lines of a large file still re-tests that whole file. With diff mode, pull request builds mutate only the lines they change against the target branch, while builds of main keep a full run and push the baseline. These are the changes to the cached build:

steps:  # ...token step as above
  # Builds clone one commit. Pull requests fetch history and the target  # branch, so gooze finds the merge base.  - id: history    name: gcr.io/cloud-builders/git    script: |      if [ -n "${_PR_NUMBER:-}" ]; then        git fetch --unshallow origin "+refs/heads/${_BASE_BRANCH}:refs/remotes/origin/${_BASE_BRANCH}"      fi
  - id: mutation    name: golang:1.27    script: |      set -e      # ...install and login, pull the baseline as above      # Pull requests test only their changed lines; main keeps a full run.      # A diff run writes its reports to .gooze-reports/run.      BASE=""      REPORTS=.gooze-reports      if [ -n "${_PR_NUMBER:-}" ]; then        BASE="origin/${_BASE_BRANCH}"        REPORTS=.gooze-reports/run      fi      # On a pull request, the build fails (exit code 3) when the changed      # lines score below 80%. On main, BASE is empty and this threshold      # doesn't apply.      gooze run -p 8 --diff="$BASE" --threshold-diff-total 80 ./...      gooze report view -o "$REPORTS"
  # ...push-token and push steps as above
  • Cloud Build clones only the commit that started the build, so there is no merge base and no origin/main. The history step fetches them, the way Cloud Build's docs unshallow a clone. For a private repository, git needs credentials for that fetch (Cloud Build's docs keep them in Secret Manager).
  • Pull request triggers set _PR_NUMBER and _BASE_BRANCH, the target branch. On main, BASE stays empty and --diff="" runs everything.
  • Both gcr.io/cloud-builders/git and the golang image include git, which diff mode needs; a minimal image may not.
  • A diff run writes its reports to .gooze-reports/run/ and leaves the pulled baseline as it was. In a pull request, gooze report view -o .gooze-reports/run prints the score of the changed lines, and --threshold-diff-total gates the pull request on that score, its own code (see diff thresholds).
  • The build runs in one step, without shards, which suits a diff run: a shard doesn't check thresholds.

Triggers

Connect the repository to Cloud Build, then create one trigger for pull requests and one for pushes to main, both using the same cloudbuild.yaml. You can do this in the console or with gcloud:

# Pull requests into main: read the baseline, test only what changed.gcloud builds triggers create github \  --name=gooze-pr \  --repo-owner=OWNER --repo-name=REPO \  --pull-request-pattern='^main$' \  --build-config=cloudbuild.yaml
# Pushes to main: refresh the baseline.gcloud builds triggers create github \  --name=gooze-main \  --repo-owner=OWNER --repo-name=REPO \  --branch-pattern='^main$' \  --build-config=cloudbuild.yaml

These commands are for a GitHub repository connected through the Cloud Build GitHub app. For a 2nd gen repository connection, pass --repository=projects/PROJECT/locations/REGION/connections/CONNECTION/repositories/REPO and --region=REGION instead of --repo-owner and --repo-name. A trigger that runs as a user-managed service account also needs options.logging set, for example to CLOUD_LOGGING_ONLY.

Fail the build below a score (quality gate)

Add a last step with a threshold. Below the mutation score you set, Gooze exits with code 3 and the build fails:

steps:  # ...token, mutation, push-token and push steps as above
  # Last: on main, fail the build (exit code 3) below _MIN_SCORE. The  # baseline is already pushed. Pull requests are gated on their changed  # lines in the mutation step.  - id: gate    name: golang:1.27    script: |      set -e      if [ -n "${_PR_NUMBER:-}" ]; then exit 0; fi      gooze report view --threshold-total "${_MIN_SCORE}"
substitutions:  _REGION: us-central1  _MIN_SCORE: "60"           # fail below this percent

The calc run at 84.62% passes a minimum of 60 and fails a minimum of 90, until the two boundary tests bring it to 100.00%.

A failed step stops the build, so the gate comes after the push step: a main build below the minimum still pushes its baseline. To record a low score without failing the build, add allowExitCodes: [3] to the step; every other code still fails it. More in exit codes.

Exit codes

Every Gooze exit code but 0 fails the step, and a failed step stops the build. That is right for errors. A low score (code 3) is different: the run finished and wrote its reports, so you choose what it does.

Put the gate in the last step, after the push step, so a low score never costs the baseline. To record a low score without failing the build, list the code in allowExitCodes:

  - id: gate    name: golang:1.27    script: |      set -e      gooze report view --threshold-total "${_MIN_SCORE}"    # A low score doesn't fail the build; any other exit code does.    allowExitCodes: [3]

The usual causes of the other codes in Cloud Build: 4, a build without the history step, since builds clone one commit; 5, an expired token or a missing roles/artifactregistry.writer; 6, a step that runs before Gooze wrote reports to /workspace. Every code: CLI reference.

Bigger runs

  • More CPUs, not shards. All steps of a build run on one machine, so splitting the run into parallel steps with -s INDEX/TOTAL only makes them share its CPUs. Raise machineType (for example E2_HIGHCPU_32) and -p together instead. Sharding pays off on CI systems that spread jobs across machines.
  • Scope and exclude. Run on the packages that matter and skip generated code with -x, for example gooze run -p 8 -x mocks/ ./internal/....
  • Coverage first. With go test -coverprofile=coverage.out ./... and gooze run --coverage-profile=coverage.out, mutants on lines no test runs are reported as not covered without running any tests.

FAQ

How do I run Gooze mutation tests in Google Cloud Build?

Add a cloudbuild.yaml step on the golang:1.27 image that runs go install gooze.dev/pkg/gooze@latest and then gooze run ./.... Raise the build timeout if a full run takes longer than the 60 minute default.

Can Gooze store its reports in Google Container Registry?

Container Registry has been shut down; its successor is Artifact Registry, which also serves gcr.io addresses. Create a Docker-format repository and use gooze report push and gooze report pull with a REGION-docker.pkg.dev/PROJECT/REPOSITORY/NAME:TAG reference.

How does Gooze authenticate to Artifact Registry in Cloud Build?

Set GOOZE_REGISTRY_USERNAME=oauth2accesstoken and GOOZE_REGISTRY_PASSWORD to a token from gcloud auth print-access-token, fetched in a gcr.io/cloud-builders/gcloud step. The build's service account needs roles/artifactregistry.writer on the repository.

Should I shard Gooze in Cloud Build?

Not within one build: every step of a build runs on the same machine, so parallel shards compete for the same CPUs. Use -p N with a larger machineType instead.

How do I fail a Cloud Build when the mutation score is too low?

Add a last step that runs gooze report view --threshold-total "${_MIN_SCORE}". Below it, Gooze exits with code 3 and the build fails, after the baseline is pushed. To record a low score without failing the build, add allowExitCodes: [3] to that step.