build-lib — the shared build engine

Every app deploys through Cloud Build. Instead of each repo carrying hundreds of lines of bash, they share u2i/build-lib — a reusable engine that turns each pipeline into a short list of named steps. An app's deploy/cloudbuild/*.yaml becomes ~70 lines of "call this command" instead of ~210 lines of scripting, and the actual logic lives (and gets fixed) in one place.

Two parts

1. A Docker image (the toolbox)

Published at europe-west1-docker.pkg.dev/shared-images-nonprod/docker-images/build-lib:<tag>. It bundles Nushell plus every CLI a pipeline step shells out to — gcloud, kubectl, helm, crane, jq, docker, buildx. It inherits from Cloud Build's docker builder so the privileged buildx contract still works. Every Cloud Build step runs inside this one image.

2. A Nushell library (the commands)

Inside the image, /lib/*.nu exposes a small set of typed commands — kube login, build image, release package-for-config-sync, helm apply-preview, and so on. Your pipeline steps call these instead of hand-writing shell. Per-pipeline state (project, commit SHA, namespace, target) is written once to /workspace/.build-state.json by the first 'boundary' step and read by every step after.

How a step is written

Each step runs the build-lib image with one of two tiny wrapper entrypoints: init-step (the boundary step that writes state) or build-step(everything after). The wrapper imports the library, loads state, and emits start/finished markers — so the step body in the app's YAML is just the operations.

steps:
- name: '${_BUILD_LIB}'
  id: 'init'
  entrypoint: 'init-step'
  args: ['state init-env --target dev']

- name: '${_BUILD_LIB}'
  id: 'cluster-setup'
  waitFor: ['init']
  entrypoint: 'build-step'
  args:
    - 'cluster-setup'
    - |
      kube login
      kube ensure-namespace
      kube bind-workload-identity

substitutions:
  _BUILD_LIB: 'europe-west1-docker.pkg.dev/shared-images-nonprod/docker-images/build-lib:0.5.10'

Command surface

ModuleCommandsWhat it does
stateinit-env, init-preview, init-cleanup, load, setWrites the shared state file once (target, project, SHA, namespace) so later steps read it instead of re-deriving it.
buildsetup-buildx, imageBuilds a container image and pushes it to the app's Artifact Registry (multi-arch via buildx).
releasepackage-for-config-sync, push-chart, tag-chart, prepare-reposync, create-cloud-deploy-releasePackages the Helm chart, pushes it to the config-sync OCI registry, tags it, renders the RepoSync manifest, and creates the Cloud Deploy release.
kubelogin, ensure-namespace, bind-workload-identity, create-database-secret, run-job, delete-namespaceCluster operations: authenticate, make sure the namespace exists, wire Workload Identity, manage secrets, run jobs, tear a namespace down.
helminstall, oci-login, apply-previewHelm operations — including apply-preview, the direct apply used for per-PR preview environments.
migrationrunRuns database migrations (invoked as a Cloud Deploy postdeploy action).
secret / neon / lint / logsecret fetch · neon fork-branch · lint run · log build-metadataSupporting ops: pull secrets, fork a Neon Postgres branch per preview, run static checks, emit structured build logs.

A real dev pipeline, end to end

The dev flow is a chain of build-lib steps: initialise state, build the image, package and publish the Helm chart, then hand off to Cloud Deploy — exactly the path described on the tutorial.

state init-env --target dev      # boundary step: write .build-state.json
build image                      # build + push the container image
release package-for-config-sync  # helm package -> OCI artifact
release push-chart               # push chart to the config-sync registry
release tag-chart                # tag the chart version
release prepare-reposync         # render RepoSync with that version
release create-cloud-deploy-release   # hand off to Cloud Deploy

Why it exists

  • One place to fix CI logic — change build-lib, and every app's pipeline gets the fix (apps pin a version tag, e.g. 0.5.x, and upgrade deliberately).
  • Short, readable pipelines — an app's cloudbuild YAML reads as a list of intentions, not a shell script.
  • Consistency — every app builds, packages, and releases the same way, so the tutorial and the example match reality across apps.