All Posts
5 Sep 202415 min readAdvanced

Designing GitOps With Jenkins, GitHub Actions, and Argo CD

Part of Platform Engineering

  • GitOps
  • GitHub Actions
  • Jenkins
  • Kubernetes

Every platform team eventually faces the same question: should we consolidate on a single CI/CD platform, or accept a split stack? I have operated all three - Jenkins for long-lived automation, GitHub Actions for developer-facing CI, and Argo CD for cluster reconciliation - and the answer is rarely "pick one."

The mistake I see most often is letting tool sprawl happen without boundaries. Jenkins runs everything because it was there first. GitHub Actions duplicates deploy logic because a team wanted faster PR feedback. Argo CD gets added later and nobody agrees on who owns the source of truth. GitOps only works when each tool has a lane.

The Golden Rule: Three Lanes, One Git Source of Truth

My operating model is straightforward. GitHub Actions owns CI: lint, test, build, scan, and publish artifacts on every pull request and merge. Jenkins owns specialized automation that does not fit the PR lifecycle - nightly data migrations, cross-repo orchestration, legacy VM deployments, or jobs that need credentials and plugins GHA should not carry. Argo CD owns Kubernetes CD: it watches Git (or Helm/Kustomize in Git) and reconciles cluster state continuously.

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   GitHub    │     │   Jenkins    │     │  Argo CD    │
│   Actions   │     │  (specialized│     │  (K8s CD)   │
│   (CI)      │     │   automation)│     │             │
└──────┬──────┘     └──────┬───────┘     └──────┬──────┘
       │ build/test         │ batch/cron          │ reconcile
       ▼                    ▼                     ▼
   Container            Legacy /              Kubernetes
   Registry             cross-repo            cluster state
                        workflows

Git remains the contract. Application manifests, Helm values, and Kustomize overlays live in repos Argo CD watches. CI pipelines never kubectl apply directly to production - they update image tags or open PRs against the deployment repo. That separation is what makes rollbacks auditable.

Why GitHub Actions Wins Developer-Facing CI

Developers live in GitHub. Putting CI next to the code reduces context switching and makes required checks visible on every PR. In teams I have supported, moving unit tests and container builds from Jenkins to GHA cut median feedback time from twelve minutes to under four - mostly because runners started on demand and caching was simpler to configure per repo.

GHA is also the right place for supply-chain gates: SAST, dependency scanning, SBOM generation, and signing. These checks should block merges, not run as optional nightly jobs nobody watches.

# .github/workflows/ci.yml (simplified)
name: CI
on:
  pull_request:
  push:
    branches: [main]

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci && npm test
      - run: docker build -t $REGISTRY/app:$GITHUB_SHA .
      - run: cosign sign --yes $REGISTRY/app:$GITHUB_SHA

Where Jenkins Still Earns Its Keep

Jenkins is not dead - it is misassigned. I still run Jenkins for workloads that need long execution windows, complex credential matrices, or plugin ecosystems GHA cannot replicate cleanly. Examples from production: triggering a multi-hour analytics backfill across three AWS accounts, coordinating a blue/green deploy to EC2 Auto Scaling groups that predate Kubernetes, and running scheduled compliance exports that write to an SFTP endpoint owned by a vendor.

The anti-pattern is using Jenkins as a generic PR gate. When Jenkins runs the same lint-and-test pipeline GHA already runs, you pay twice in maintenance and drift. Keep Jenkins jobs declarative (Jenkinsfile in Git), versioned, and scoped to automation GHA should not own.

WorkloadGHAJenkinsArgo CD
PR lint/test/buildPrimaryAvoidN/A
Container image publishPrimaryFallbackN/A
K8s deploy to prodTrigger onlyAvoidPrimary
Cross-account batch jobAvoidPrimaryN/A
Legacy VM deployAvoidPrimaryN/A
Manifest reconciliationN/AN/APrimary

Argo CD as the Kubernetes Control Plane

Argo CD turns Git into the runtime contract for Kubernetes. I configure ApplicationSets for multi-environment, multi-cluster patterns - one overlay per environment, one Application per cluster. Sync policies differ by environment: staging auto-syncs on merge; production requires manual promotion or an approved PR merge to the prod overlay branch.

Health checks and sync waves matter. Database migrations run in wave 1, application Deployments in wave 2, ingress and HPA in wave 3. Without ordering, you deploy code that expects a schema change that has not landed yet.

# argocd Application (conceptual)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-prod
spec:
  project: production
  source:
    repoURL: https://github.com/org/k8s-manifests
    path: overlays/production/api
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc
    namespace: api
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

When something breaks, Argo CD gives you a diff between desired and live state. That is the GitOps superpower - not faster deploys, but faster diagnosis.

Wiring the Three Together Without Chaos

The handoff pattern I use: GHA builds and pushes an immutable image tag (git SHA or semver). A bot or workflow opens a PR updating the image tag in the manifests repo. Argo CD picks up the change after merge. Jenkins enters only when the target is not Kubernetes - for example, updating an ASG launch template and triggering a rolling refresh.

Secrets never live in Git. CI systems fetch short-lived credentials from AWS Secrets Manager or Vault. Argo CD uses sealed secrets, External Secrets Operator, or SOPS-encrypted files. Each tool reads secrets at runtime; none commit them.

Observability ties the loop: GHA workflow runs link to commit SHAs, Jenkins jobs emit structured logs to CloudWatch, and Argo CD sync events feed into Slack and PagerDuty. When a deploy fails, you should trace from alert → Argo sync status → manifest diff → CI run that produced the image.

Engineering Takeaways

  • Assign each tool a lane: GHA for CI, Jenkins for specialized automation, Argo CD for Kubernetes CD.
  • Git (or a manifests repo) is the single source of truth for desired runtime state - CI updates tags, CD reconciles clusters.
  • Never kubectl apply from CI to production; open a PR against the deployment repo instead.
  • Use Argo CD sync waves to order migrations, app deploys, and ingress changes.
  • Retire duplicate Jenkins PR pipelines once GHA coverage is complete - drift is the hidden cost.
  • Traceability requires linking image SHA → manifest PR → Argo sync event in your alerting pipeline.

Trade-offs I Would Consider

Option: Monorepo manifests with Kustomize overlays per environment

  • Pros: One repo to audit, ApplicationSets scale cleanly, diffs are visible in one place.
  • Cons: Blast radius is larger; a bad merge can affect all environments if branch protections are weak.

Option: Separate repos per environment (dev/staging/prod manifests)

  • Pros: Stronger isolation, prod repo can have tighter access controls and required reviewers.
  • Cons: More repos to maintain, promoting changes requires cherry-pick or automation across repos.

What I Would Do in Production

Start by moving PR checks to GitHub Actions before touching deploy paths. Teams feel the win immediately and you reduce Jenkins load.

Introduce Argo CD on staging first with auto-sync enabled. Run production with manual sync or a promotion PR workflow until the team trusts the diff-and-reconcile loop.

Document the golden rule in your platform runbook and enforce it in architecture reviews. New services default to GHA + Argo CD; Jenkins requires a written exception.

Set up Argo CD notifications for sync failed, sync degraded, and health degraded. These fire before users notice.

Keep Jenkins on LTS, pin plugins, and treat Jenkinsfiles like application code - reviewed, tested, and owned by the platform team or a designated guild.

FAQ

Q: Can I replace Jenkins entirely with GitHub Actions?

For many greenfield teams, yes. If you have no legacy VM deploys, no multi-hour batch orchestration, and no vendor-specific plugins, GHA plus Argo CD covers most needs. Keep Jenkins only where GHA's runner limits or credential model genuinely block you.

Q: Should Argo CD auto-sync production?

I auto-sync staging always. For production, I prefer merge-to-deploy with optional manual sync for the first few months. Once rollback drills are routine and sync waves are stable, auto-sync with strong branch protection is reasonable.

Q: How do I handle secrets in a GitOps repo?

Use External Secrets Operator pulling from AWS Secrets Manager, or SOPS-encrypted files with keys in KMS. Never plaintext. CI systems write secrets to the manager; Git stores references only.

Q: What about Helm versus Kustomize?

Both work with Argo CD. I pick Kustomize when teams own plain YAML and want minimal templating. Helm when you publish reusable charts across many services. The GitOps pattern is the same either way.

Q: How do I prevent GHA and Jenkins from deploying the same service?

Declare ownership in a service catalog or CODEOWNERS file. Kubernetes services go through Argo CD only. Add CI checks that fail if a Jenkinsfile or GHA workflow contains kubectl apply to protected clusters.