Push vs Pull: Two CI/CD Philosophies for AI Services
The architectural difference between push-based "reactor" pipelines for Cloud Run and pull-based GitOps sync for GKE — and how Jenkins orchestrates both patterns.
Build pipelines — whether targeting GKE or Cloud Run — act as strict gatekeepers. A Git tag push (e.g., 1.2.3-uat.1) triggers a webhook to Jenkins. The GenericTrigger plugin uses a regex filter to validate the tag format and extract the environment suffix before the pipeline even starts. A UAT pipeline only activates for -uat tags; a -dev tag is silently rejected. Think of it as a bouncer checking your ID at the door. This protects upstream environments from unverified, ad-hoc changes.
// Jenkins Generic Webhook Trigger — the "bouncer" pattern
// The Expression regex validates tag format AND environment match.
// Text is assembled from webhook payload variables:
// $appEnvironment = extracted env suffix (e.g. "uat")
// $changesType = webhook action type ("ADD" or "UPDATE")
// $ref = full git ref ("refs/tags/1.2.3-uat.1")
//
// Match: "uat-ADD-refs/tags/1.2.3-uat.1" → pipeline runs
// Reject: "dev-ADD-refs/tags/1.2.3-dev.1" → UAT pipeline blocked
regexpFilterExpression: "^uat-(ADD|UPDATE)-refs/tags/\\d+\\.\\d+\\.\\d(-uat)?(\\.\\d+)?$"
regexpFilterText: "$appEnvironment-$changesType-$ref"
Cloud Run deploy pipelines follow the opposite philosophy. When a PR is merged into the config repo, the pipeline accepts the event unconditionally — like a mailroom accepting all packages. The intelligence lives inside the pipeline stages: it runs git diff to determine which service YAML files changed, then deploys only those services. A single merge can update multiple services simultaneously; unchanged services are never touched. This provides maximum speed and efficiency.
For GKE, no deploy webhook is needed at all. The build pipeline creates a PR to the GKE config repository with the updated image tag. Once merged, ArgoCD detects the drift automatically and syncs the manifests to the cluster. With selfHeal: true, even manual kubectl changes are automatically reverted. The entire deployment state is always exactly the state of Git. This pull-based model is incredibly resilient and self-healing.
# GKE Config Repo values.yaml — Jenkins only updates the tag
image:
repository: asia-southeast2-docker.pkg.dev/my-project/my-repo/my-service
tag: "1.2.3-dev.1" # ← CI updates this single line via PR
# ArgoCD auto-syncs on commit — no webhook needed
syncPolicy:
automated:
prune: true
selfHeal: true
A major gotcha is coordinating branching strategies between the two models. In our push-based Cloud Run setup, developers were tempted to deploy directly from feature branches, bypassing UAT governance. In our pull-based GKE cluster, direct commits to the main config repo caused sync conflicts when multiple automated PRs landed at once. The lesson: establish strict trunk-based development with auto-squashing PRs, and enforce repository branch protections so that only certified pipelines can write to the environment directories.