Why GitHub Actions?

GitHub Actions is the natural choice if your code is already on GitHub. It has native integration with your repository, a rich marketplace of reusable actions, and OIDC-based authentication to cloud providers — meaning you never store AWS access keys as secrets.

It's also free for public repositories and includes generous minutes for private repos. For most startups, it's the lowest-friction CI platform available.

The Pipeline We're Building

Our target pipeline has two parts:

This is the two-repo GitOps pattern: one repo for application code, one for Kubernetes manifests. The CI pipeline updates the manifest repo; ArgoCD deploys it.

Step 1: Set Up OIDC Authentication to AWS

Instead of storing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as GitHub secrets, we use OpenID Connect (OIDC). GitHub issues a short-lived JWT; AWS validates it and issues temporary credentials.

Create the IAM OIDC Provider

# Create the OIDC provider in your AWS account
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

Create the IAM Role

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringLike": {
        "token.actions.githubusercontent.com:sub":
          "repo:your-org/your-repo:*"
      },
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
      }
    }
  }]
}

Attach an inline policy granting ecr:GetAuthorizationToken, ecr:BatchCheckLayerAvailability, ecr:PutImage, and ecr:InitiateLayerUpload to this role.

Step 2: Write the CI Workflow

Create .github/workflows/ci.yml in your application repository:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  id-token: write   # needed for OIDC
  contents: read

env:
  AWS_REGION: ap-south-1
  ECR_REPOSITORY: my-app
  IMAGE_TAG: ${{ github.sha }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Run unit tests
        run: |
          # Replace with your test command
          npm ci && npm test

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecr
          aws-region: ${{ env.AWS_REGION }}

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

      - name: Build Docker image
        run: |
          docker build -t ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} .

      - name: Scan image with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: "${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}"
          exit-code: '1'
          severity: 'CRITICAL'

      - name: Push to ECR
        if: github.ref == 'refs/heads/main'
        run: |
          docker push ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}

      - name: Update image tag in GitOps repo
        if: github.ref == 'refs/heads/main'
        uses: actions/checkout@v4
        with:
          repository: your-org/gitops-repo
          token: ${{ secrets.GITOPS_PAT }}
          path: gitops

      - name: Patch Kustomize image tag
        if: github.ref == 'refs/heads/main'
        run: |
          cd gitops
          kustomize edit set image \
            my-app=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
          git config user.email "ci@omphoratech.com"
          git config user.name "CI Bot"
          git add .
          git commit -m "ci: update my-app image to ${{ env.IMAGE_TAG }}"
          git push

Key Points

Step 3: Write a Good Dockerfile

A multi-stage Dockerfile keeps your production image small and secure — no dev dependencies, no build tools:

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER app
EXPOSE 3000
CMD ["node", "dist/index.js"]

Running as a non-root user (USER app) is a security baseline requirement for most container security policies.

Step 4: Set Up ArgoCD for Continuous Deployment

Install ArgoCD on your EKS cluster using Helm — see our GitOps guide for detailed ArgoCD setup. Once installed, create an Application that points at your GitOps repository:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops-repo
    targetRevision: main
    path: apps/my-app/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

With automated.selfHeal: true, ArgoCD will revert any manual changes to the cluster. Git is the only source of truth.

Step 5: Environment Promotion with Kustomize

Structure your GitOps repo to support multiple environments:

gitops-repo/
  apps/
    my-app/
      base/
        deployment.yaml
        service.yaml
        kustomization.yaml
      overlays/
        staging/
          kustomization.yaml   # patches image, replicas
        production/
          kustomization.yaml   # patches image, replicas, resource limits

The CI pipeline updates the staging overlay first. A separate promotion step (manual approval or a time delay) updates production. This gives you a proper staging gate without a separate CD tool.

Handling Rollbacks

Rolling back with ArgoCD is a one-click operation in the UI: select the target revision (any previous Git commit), click Rollback. ArgoCD resyncs the cluster to that state. The image is still in ECR, so no rebuilding required.

You can also rollback from the CLI:

argocd app history my-app
argocd app rollback my-app <revision-id>

Adding Slack Notifications

Add deployment notifications to GitHub Actions using the Slack webhook action:

- name: Notify Slack on success
  if: success() && github.ref == 'refs/heads/main'
  uses: slackapi/slack-github-action@v1.26.0
  with:
    payload: |
      {
        "text": "✅ Deployed *my-app* `${{ env.IMAGE_TAG }}` to production"
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Summary

This pipeline gives you:

The goal of CI/CD isn't just speed — it's confidence. Every engineer on the team should be able to deploy to production without fear.

Need this set up for your team?

We implement production CI/CD pipelines as part of our DevOps consulting service. Typically takes 1–2 weeks.

Book Free Consultation