# gcloud CLI Cheat Sheet — Artifact Registry & Cloud Build

> **Tool:** Google Cloud CLI (gcloud)
> **Category:** Cloud CLIs
> **Verified against:** Google Cloud SDK 553.0.0, flags verified via `gcloud artifacts repositories create
> --help`, `gcloud artifacts repositories set-cleanup-policies --help`, `gcloud artifacts repositories
> add-iam-policy-binding --help`, `gcloud artifacts docker tags list --help`, `gcloud builds submit --help`,
> `gcloud builds triggers create github --help`, `gcloud builds triggers list --help`, and `gcloud builds
> worker-pools create --help` run locally, 2026-09-17
> **Official docs:** https://cloud.google.com/sdk/gcloud/reference/artifacts and https://cloud.google.com/sdk/gcloud/reference/builds

## What it is and where it fits 🎯

Artifact Registry is GCP's managed registry for container images and language packages (npm, Maven, Python,
Go, apt/yum); Cloud Build is GCP's managed CI build/test/push pipeline runner. They're covered together here
because they're almost always used as a pair, Cloud Build produces an artifact, Artifact Registry stores it,
and every deploy on pages `02`, `05`, and `10` pulls from exactly this registry. Page `03` already showed
`gcloud builds submit` in passing to build from a `Dockerfile`; this page covers both services properly,
repositories, IAM, cleanup policies, and the trigger-based pipelines Cloud Build is actually built around.

## Core concepts: how an image gets from source to a running workload

```mermaid
sequenceDiagram
    participant Dev as Developer / CI push
    participant Repo as Source repo (GitHub)
    participant Build as Cloud Build
    participant AR as Artifact Registry
    participant Run as Cloud Run / GKE

    Dev->>Repo: git push
    Repo->>Build: trigger fires
    Build->>Build: run cloudbuild.yaml steps (build, test)
    Build->>AR: docker push image:sha
    AR-->>Build: digest confirmed
    Build->>Run: gcloud run deploy --image=...:sha
    Run-->>Dev: new revision serving traffic
```

Two products, one pipeline: Cloud Build never stores the artifact itself, it always pushes to a registry
(Artifact Registry, in every current example, Container Registry's `gcr.io` is deprecated), and a deploy
step then pulls from that same registry by digest or tag. Tagging by the Git commit SHA (shown in the
diagram) rather than `latest` is what makes a specific deployed revision traceable back to the exact source
commit that produced it, worth adopting as a default rather than an afterthought.

## Creating an Artifact Registry repository

```bash
gcloud artifacts repositories create my-app-images \
  --repository-format=docker --location=us-central1 \
  --description="Container images for my-app" \
  --immutable-tags

gcloud artifacts repositories create my-app-npm \
  --repository-format=npm --location=us-central1

gcloud artifacts repositories list --location=us-central1
gcloud artifacts repositories describe my-app-images --location=us-central1
```

`--repository-format` is fixed at creation, `docker`, `npm`, `python`, `maven`, `go`, `apt`, `yum`, one
repository holds one format, mixing package types means separate repositories, not one shared one.
`--immutable-tags` rejects re-pushing an existing tag entirely, a genuinely strong guardrail against the
classic "someone force-pushed over `:latest` and now nobody knows what's actually running" failure mode,
worth enabling on any repository backing a production deploy pipeline.

## Pushing and pulling images

```bash
gcloud auth configure-docker us-central1-docker.pkg.dev   # from page 00 — registers gcloud as the credential helper

docker build -t us-central1-docker.pkg.dev/my-project-id/my-app-images/api:$(git rev-parse --short HEAD) .
docker push us-central1-docker.pkg.dev/my-project-id/my-app-images/api:$(git rev-parse --short HEAD)

gcloud artifacts docker tags list us-central1-docker.pkg.dev/my-project-id/my-app-images/api
gcloud artifacts docker images list us-central1-docker.pkg.dev/my-project-id/my-app-images \
  --include-tags --filter="tags:*"
```

The registry hostname is `<region>-docker.pkg.dev`, distinct from the older, now-deprecated `gcr.io`
Container Registry hostname a lot of existing tutorials and Stack Overflow answers still reference, new
setups should target Artifact Registry's own hostname from the start. `docker` itself performs the actual
push/pull, `gcloud artifacts` is for repository administration and inspecting what's already there, not a
substitute for `docker push`/`docker pull`.

## Access control and cleanup policies

```bash
gcloud artifacts repositories add-iam-policy-binding my-app-images \
  --location=us-central1 \
  --member="serviceAccount:ci@my-project-id.iam.gserviceaccount.com" \
  --role="roles/artifactregistry.writer"

gcloud artifacts repositories add-iam-policy-binding my-app-images \
  --location=us-central1 \
  --member="serviceAccount:deployer@my-project-id.iam.gserviceaccount.com" \
  --role="roles/artifactregistry.reader"

cat > cleanup-policy.json <<'EOF'
[
  {
    "name": "delete-untagged-after-30d",
    "action": {"type": "Delete"},
    "condition": {"tagState": "UNTAGGED", "olderThan": "2592000s"}
  },
  {
    "name": "keep-last-20-tagged",
    "action": {"type": "Keep"},
    "mostRecentVersions": {"tagState": "TAGGED", "keepCount": 20}
  }
]
EOF
gcloud artifacts repositories set-cleanup-policies my-app-images \
  --location=us-central1 --policy=cleanup-policy.json
```

Splitting `writer` (CI, which pushes new images) from `reader` (a deploy identity, which only ever pulls) is
the same least-privilege split as Secret Manager's `secretAccessor` on page `08`, a deploy identity almost
never needs to push. Without a cleanup policy, a repository grows unbounded, every build's untagged
intermediate layers and every superseded tagged image stay forever, `set-cleanup-policies` is what actually
caps that cost automatically instead of relying on someone remembering to prune manually.

## Cloud Build: running a build from local source

```bash
gcloud builds submit --tag=us-central1-docker.pkg.dev/my-project-id/my-app-images/api:latest .
gcloud builds submit --config=cloudbuild.yaml --substitutions=_ENV=staging .

gcloud builds list --limit=5
gcloud builds log <build-id>
gcloud builds describe <build-id> --format="value(status)"
```

`--tag` is shorthand for an implicit single-step `docker build` + push; `--config` points at a full
`cloudbuild.yaml` defining a real multi-step pipeline (build, run tests, push, trigger a deploy), the path
that scales past "just build one image." `gcloud builds submit` uploads the current directory as the build
context the same way `docker build .` does, run it from the repository root, and add a `.gcloudignore`
(mirroring `.gitignore`'s syntax) to keep `node_modules`/`.git`/build artifacts out of the uploaded context.

## Config file format: `cloudbuild.yaml`

```yaml
steps:
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', 'us-central1-docker.pkg.dev/$PROJECT_ID/my-app-images/api:$SHORT_SHA', '.']
  - name: 'gcr.io/cloud-builders/docker'
    args: ['run', 'us-central1-docker.pkg.dev/$PROJECT_ID/my-app-images/api:$SHORT_SHA', 'npm', 'test']
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 'us-central1-docker.pkg.dev/$PROJECT_ID/my-app-images/api:$SHORT_SHA']
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: gcloud
    args: ['run', 'deploy', 'my-app', '--image=us-central1-docker.pkg.dev/$PROJECT_ID/my-app-images/api:$SHORT_SHA', '--region=us-central1']
images:
  - 'us-central1-docker.pkg.dev/$PROJECT_ID/my-app-images/api:$SHORT_SHA'
timeout: 1200s
```

Each step is its own container invocation, `$PROJECT_ID` and `$SHORT_SHA` are built-in substitution
variables (no need to pass them via `--substitutions`), a custom one like `_ENV` shown earlier must start
with an underscore. The final `images:` block is what Cloud Build actually treats as this build's tracked
output artifacts, distinct from just running a `docker push` step, it's what shows up in the build's own
provenance metadata.

## Build triggers: running automatically on push

```bash
gcloud builds triggers create github \
  --repo-owner=my-org --repo-name=my-app \
  --branch-pattern="^main$" --build-config=cloudbuild.yaml \
  --name=deploy-on-main --region=us-central1

gcloud builds triggers list --region=us-central1
gcloud builds triggers run deploy-on-main --branch=main --region=us-central1   # trigger manually, outside a push
```

A trigger is what turns Cloud Build from "something I invoke manually" into an actual CI/CD pipeline,
`--branch-pattern` is a regex, not a literal branch name, `^main$` matches only `main` exactly while a
pattern like `^release/.*$` would match every release branch. `triggers run` is genuinely useful for
re-running a pipeline against a specific ref without needing a real push to test it.

## Private/dedicated worker pools

```bash
gcloud builds worker-pools create my-private-pool \
  --region=us-central1 \
  --worker-machine-type=e2-standard-4 --worker-disk-size=100 \
  --peered-network=projects/my-project-id/global/networks/prod-network \
  --no-public-egress

gcloud builds submit --config=cloudbuild.yaml --worker-pool=my-private-pool .
```

A private pool runs builds inside your own VPC (via `--peered-network`) instead of Google's shared build
infrastructure, the pattern for a build that needs to reach a private resource (an internal artifact mirror,
a database for integration tests) that isn't reachable from the public internet at all. `--no-public-egress`
additionally denies the pool any outbound internet access, appropriate for a fully locked-down build
environment where even outbound calls should be explicitly allowlisted rather than open by default.

## Real-world scenario: image provenance from commit to production

A team needs to answer "what exact code is running in production" reliably during an incident, not
approximately:

```bash
# In cloudbuild.yaml, tag every image with the immutable commit SHA, never `latest`
# Image: us-central1-docker.pkg.dev/my-project/my-app-images/api:$SHORT_SHA

gcloud run services describe my-app --region=us-central1 --format="value(spec.template.spec.containers[0].image)"
# → us-central1-docker.pkg.dev/my-project/my-app-images/api:a1b2c3d

git show a1b2c3d --stat   # the exact commit that produced the running image
```

Because the deployed image tag *is* the short commit SHA, answering "what's running" is a direct
`describe` call away, with no separate deployment log or spreadsheet to cross-reference, the entire
mechanism relies on never deploying a `:latest`-tagged image, which `--immutable-tags` on the repository
(shown earlier) helps enforce structurally rather than by convention alone.

## Real-world scenario: pre-flight checklist for a new production build pipeline

- [ ] Repository created with `--immutable-tags` so a compromised or buggy pipeline can't silently
      overwrite a previously-deployed tag
- [ ] A cleanup policy is in place before the repository accumulates its first month of untagged layers,
      not added retroactively once storage costs are already a problem
- [ ] The CI service account has `artifactregistry.writer` only, never `artifactregistry.admin`, which
      would also allow deleting the repository itself
- [ ] The build trigger's `--branch-pattern` matches exactly the branches meant to auto-deploy, an overly
      broad pattern (`.*` catching every feature branch) is a real, easy mistake with real consequences

## CI/CD integration recipe: this page's own build, wired into GitHub Actions

```yaml
# .github/workflows/build-and-push.yml
name: Build and push
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/<project-number>/locations/global/workloadIdentityPools/github-pool/providers/github-provider
          service_account: ci@my-project-id.iam.gserviceaccount.com
      - uses: google-github-actions/setup-gcloud@v2
      - run: gcloud builds submit --config=cloudbuild.yaml --substitutions=_ENV=production .
```

Running the build through Cloud Build (rather than `docker build`/`docker push` directly inside the GitHub
Actions runner) means the exact same `cloudbuild.yaml` pipeline runs identically whether triggered by
GitHub Actions, a native Cloud Build trigger, or a manual `gcloud builds submit`, one pipeline definition,
not two parallel ones to keep in sync.

## Common pitfalls

- **Pushing to the deprecated `gcr.io` hostname out of habit.** New repositories and pipelines should target
  Artifact Registry's `<region>-docker.pkg.dev` hostname; `gcr.io` still works for existing setups but isn't
  the current recommended path.
- **Deploying `:latest` instead of an immutable, traceable tag.** Makes "what's actually running" a guess
  instead of a `describe` call, see the provenance scenario above.
- **No cleanup policy on a repository backing frequent CI builds.** Untagged layers accumulate silently
  and become a real, unnecessary storage cost within weeks.
- **Granting the CI service account `artifactregistry.admin` instead of `writer`.** Admin can delete the
  repository entirely; a pipeline identity almost never needs that.
- **An overly broad `--branch-pattern` on a trigger.** Confirm it matches only the branches meant to
  auto-build/deploy before enabling the trigger, not after a feature branch accidentally deploys to prod.

## Exit codes

`0` success, non-zero on any API/validation error or a failed build step, `gcloud builds submit` blocks and
streams logs by default, returning the build's own final exit code, a failing test step inside
`cloudbuild.yaml` fails the whole `gcloud builds submit` invocation with a non-zero exit, exactly the signal
a CI pipeline step needs to fail the job correctly.

## When to reach for something else

For a build pipeline with substantial existing investment in another CI system (GitHub Actions, GitLab CI,
Jenkins) that only needs to *push* to Artifact Registry rather than run the build itself, skip Cloud Build
entirely and authenticate that existing pipeline via Workload Identity Federation (page `01`) plus `docker
push`, standing up Cloud Build in parallel adds a second pipeline to maintain for no real benefit. For
declarative, reviewable repository and trigger provisioning across environments, prefer Terraform's
`google_artifact_registry_repository`/`google_cloudbuild_trigger` resources over a growing shell script of
the commands on this page, consistent with the IaC guidance on every earlier page.
