HomeBlogHow to Build a Production CI/CD Pipeline with GitHub Actions
CI/CDGitHub ActionsArgoCDKubernetes

How to Build a Production CI/CD Pipeline with GitHub Actions

May 18, 2026·12 min read·Omphora Engineering

Why GitHub Actions + ArgoCD is the standard

GitHub Actions has become the default CI platform for most engineering teams — it's deeply integrated with Git workflows, has a rich marketplace, and scales from a startup to thousands of engineers. Paired with ArgoCD for continuous delivery, you get a fully GitOps-driven pipeline that's both powerful and maintainable.

This guide walks through building a production-grade pipeline that covers:

  • Build, test, and security scan in CI
  • OIDC authentication (no long-lived AWS credentials)
  • Docker image build and push to ECR
  • ArgoCD for GitOps-based deployment to EKS
  • Argo Rollouts for canary deployments

Repository structure

.github/
  workflows/
    ci.yml          # Build, test, scan
    deploy.yml      # Update image tag in GitOps repo
k8s/
  apps/
    my-app/
      base/         # Kubernetes manifests
      overlays/
        staging/    # Kustomize overlays
        production/

OIDC Authentication — no more long-lived credentials

The first thing to get right is authentication to AWS. Never store AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY as GitHub secrets — they're long-lived credentials that can be leaked or rotated incorrectly.

Use OIDC federation instead:

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
    role-session-name: GitHubActions
    aws-region: us-east-1

The IAM role trust policy allows GitHub's OIDC provider to assume it:

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:environment:production"
    }
  }
}

The CI workflow

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test -- --coverage

      - name: Lint
        run: npm run lint

  security-scan:
    runs-on: ubuntu-latest
    needs: build-test
    steps:
      - uses: actions/checkout@v4

      - name: Run Trivy vulnerability scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

  build-push:
    runs-on: ubuntu-latest
    needs: [build-test, security-scan]
    if: github.ref == 'refs/heads/main'
    outputs:
      image-tag: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.AWS_ROLE_ARN }}
          aws-region: us-east-1

      - name: Login to ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ vars.ECR_REGISTRY }}/my-app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

ArgoCD for continuous delivery

With the image pushed to ECR, the deployment step updates the image tag in your GitOps repository. ArgoCD watches that repository and syncs the change to your cluster:

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build-push
    environment: staging
    steps:
      - name: Update image tag in GitOps repo
        uses: actions/checkout@v4
        with:
          repository: your-org/k8s-configs
          token: ${{ secrets.GITOPS_TOKEN }}

      - name: Update staging overlay
        run: |
          cd apps/my-app/overlays/staging
          kustomize edit set image my-app=${{ vars.ECR_REGISTRY }}/my-app:${{ github.sha }}
          git commit -am "chore: deploy my-app ${{ github.sha }} to staging"
          git push

ArgoCD detects the change and syncs automatically to staging. Production requires a manual promotion or tag-based trigger.

Progressive delivery with Argo Rollouts

For production deployments, use Argo Rollouts for canary analysis:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
spec:
  strategy:
    canary:
      canaryService: my-app-canary
      stableService: my-app-stable
      steps:
        - setWeight: 10
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 50
        - pause: { duration: 10m }
        - setWeight: 100

If the AnalysisTemplate detects elevated error rates, Argo Rollouts automatically rolls back to the stable version — no manual intervention needed.

Key takeaways

  1. OIDC over static credentials — always, for every environment
  2. Security scanning in CI — Trivy, Checkov, or Semgrep before images are pushed
  3. GitOps for CD — ArgoCD owns what's deployed, not the CI pipeline
  4. Progressive delivery — canary + automatic rollback for production
  5. Environment gates — staging is automatic, production requires approval or a tag trigger

Not sure where to start?
Let's talk.

One conversation, no commitment. We listen to what your team is struggling with and give you an honest picture of what needs to change — and what doesn't.

  • What's slowing down your team's deployment process
  • Where your cloud spend is going — and what's being wasted
  • Security vulnerabilities in your current setup
  • Reliability gaps that could cause downtime
  • Blind spots in your monitoring and alerting
Available for new projectsResponse within 1 business dayNo long-term commitment required
your-infra ~ after-omphora
$ terraform apply
✓ 23 resources. Apply complete in 4m 12s
$ kubectl get nodes
NAME STATUS ROLES AGE
ip-10-0-1 Ready worker 2d
ip-10-0-2 Ready worker 2d
ip-10-0-3 Ready worker 2d
$ argocd app list
production Synced Healthy
staging Synced Healthy
$ # Commit → production: 3m 42s
✓ Zero downtime · p99: 82ms · cost ↓ 38%
$ # Example output — results vary by workload.
3m 42s
Deploy time
38%
Cost saved
99.9%
Uptime