Unifying CI and CD in one GitOps flow: building and testing code, creating the image, then handing deployment entirely over to ArgoCD. Real examples with GitHub Actions and best practices for separating responsibilities.

In episode 17 we discussed notifications & alerts — how ArgoCD informs the team via Slack, email, or webhook when sync or health status changes. But up to this point we've treated Git as a source that "suddenly changes" without understanding who changes it. In this episode we close that loop: integrating ArgoCD with CI/CD pipelines, so that from code commit to a live application in the cluster is one traceable flow.
Why does this matter? In traditional setups, CI and CD are often merged: one pipeline runs the build then immediately does kubectl apply to the cluster. The problem is that pipeline holds cluster credentials, and every deployment is a decision recorded only in pipeline logs that can disappear. GitOps offers a healthier division of labor: CI is responsible for building and testing artifacts, while CD — held by ArgoCD — is responsible for applying the approved configuration in Git. CI may hold registry credentials; CD only needs to read Git. After this episode, you'll understand this pattern down to the implementation level.
Before writing a pipeline, you must understand who does what. The GitOps model splits the flow into two domains with different interests:
| Domain | Responsibility | Output | Credentials held |
|---|---|---|---|
| CI (Continuous Integration) | Code checkout, lint, unit test, image build, push to registry, security scan | Immutable image with a unique tag (commit SHA or semver) | Registry token, Git credentials for manifest commits |
| CD (Continuous Delivery) | Read Git, reconcile, apply, monitor health | Cluster always equals Git | Git credentials only (pull), held by ArgoCD |
In essence: CI produces artifacts, CD applies artifacts. ArgoCD doesn't need to know how the image was built; it only needs to know that the image with a certain tag exists in the registry, then materializes it in the cluster. This separation lets CI be re-run without affecting the cluster, and makes CD rollback independent of the pipeline.
The build stage is the sequence of steps that produces an image. The industry standard uses five sequential steps:
name: ci
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: bun install --frozen-lockfile
- run: bun run lint && bun run test
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/org/api:${{ github.sha }}Notice the image tag pattern: using the commit SHA as the tag is the cheapest way to create immutable artifacts — every commit produces an image that is never overwritten, so you can always reconstruct the exact version running in production. Trivy's exit-code: "1" makes the pipeline fail if high-severity CVEs are found — this is the security gate before an image enters the cluster.
After the image is in the registry, CI's job isn't done. In the GitOps model, the next step is updating the manifests in Git so ArgoCD sees the new image. The easiest option is Kustomize images (from episode 8):
- uses: azure/setup-kubectl@v4
- uses: imranismail/setup-kustomize@v2
- run: |
cd manifests/overlays/staging
kustomize edit set image ghcr.io/org/api:${{ github.sha }}
git config user.name "ci-bot"
git config user.email "ci@org.dev"
git commit -am "chore(staging): bump api to ${{ github.sha }}"
git pushAfter the git push, two things can happen depending on the Application's syncPolicy:
| Sync policy | Behavior after the manifest commit | When to use |
|---|---|---|
| Manual | ArgoCD detects OutOfSync, waits for approval | Production, sensitive changes |
| Automated | ArgoCD syncs automatically right away | Dev/staging, routine changes |
| Automated + selfHeal | Automatically return cluster drift to Git | Environments that must always match Git |
If the Application uses automated sync, there's no more manual step — ArgoCD sees the Git change, syncs, and the health check runs. If manual, the team runs:
argocd app get api --show-operation
argocd app sync api --strategy apply
argocd app wait api --healthTip
The pattern pipeline writes manifests, ArgoCD applies them makes Git the only write point into the cluster. If you want to be really strict, add branch protection to the manifest repo so commits can only land via reviewed pull requests — this adds a human approval layer without giving anyone direct cluster access.
The same pattern — build, push, update manifest, ArgoCD sync — can be built on any platform. Only the syntax differs:
| CI platform | How to update manifests | How to trigger ArgoCD |
|---|---|---|
| GitHub Actions | kustomize edit set image step + commit | No direct trigger needed: ArgoCD reads Git |
| GitLab CI/CD | Kustomize script + git commit with an API token | GitLab webhook to ArgoCD (optional, for fast sync) |
| Jenkins | update manifest stage using kustomize | ArgoCD plugin or argocd app sync via CLI |
| Tekton | Kustomize task + git commit task in the pipeline | argocd-app task from the Tekton catalog |
The key to remember: never call argocd app sync from a pipeline to "deploy". ArgoCD should pull from Git by itself. If you call sync from the pipeline, you're back to the push model, and GitOps's main claim — all changes originate from Git — breaks. Let ArgoCD be the driver.
latest tag is the enemy of reproducibility.git revert or argocd app rollback. Document the rollback steps in a runbook.A good deployment is tested before and after being applied:
kubectl kustomize or helm template --validate) to catch broken YAML before it enters Git.apiVersion: batch/v1
kind: Job
metadata:
name: api-smoke-test
annotations:
argocd.argoproj.io/hook: PostSync
spec:
template:
spec:
restartPolicy: Never
containers:
- name: smoke
image: curlimages/curl:latest
command: ["sh", "-c", "curl -sf http://api:8080/healthz"]If curl fails (e.g. non-zero exit code), the hook job fails, and ArgoCD marks the sync as failed — a trigger that can fire notifications (episode 17) and open the door to automatic rollback.
This episode united the whole flow: CI builds and tests artifacts then updates the manifests in Git, and ArgoCD as CD pulls the changes and reconciles the cluster. We covered the five build-stage steps, the kustomize edit set image pattern as the CI-to-CD bridge, the sync policy comparison, examples on GitHub Actions, GitLab, Jenkins, and Tekton, and testing strategies with PostSync hooks.
The points you should take with you:
The "fast and traceable" flow is now ready. But fast isn't enough — in production you need a way to release a new version without cutting off users. In the next episode 19 we discuss progressive delivery with Argo Rollouts — canary, blue-green, metric analysis, and traffic splitting. See you in episode 19!