Skip to main content

CI/CD Best Practices

DevOpsGenie is pipeline-agnostic. It integrates with GitHub Actions, GitLab CI, Tekton, and Jenkins. This guide covers the patterns and practices that apply regardless of your CI platform.

The Golden Path Pipeline

DevOpsGenie recommends a Golden Path pipeline structure that every team should adopt:

Code Push → CI (Lint, Test, Build) → Image Push → GitOps Update → CD (ArgoCD) → Production

This separation of CI (build, test, package) and CD (deploy via Git) is intentional. It ensures:

  • Every deployment is traceable to a Git commit
  • Rollbacks are a git revert away
  • The cluster state is always the authoritative source of truth

GitHub Actions Integration

.github/workflows/ci-cd.yaml
name: CI/CD Pipeline

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

env:
AWS_REGION: us-east-1
ECR_REPOSITORY: my-service
IMAGE_TAG: ${{ github.sha }}

jobs:
# ── Step 1: Quality Gates ────────────────────────────────
lint-and-test:
name: Lint & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

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

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Type check
run: npm run typecheck

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

- name: Upload coverage
uses: codecov/codecov-action@v4

# ── Step 2: Build & Push Image ───────────────────────────
build-and-push:
name: Build & Push
runs-on: ubuntu-latest
needs: lint-and-test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
id-token: write
contents: read
outputs:
image: ${{ steps.build.outputs.image }}
steps:
- uses: actions/checkout@v4

- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}: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, tag, and push image
id: build
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
docker build \
--build-arg GIT_COMMIT=${{ github.sha }} \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--cache-from $REGISTRY/$ECR_REPOSITORY:latest \
-t $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
-t $REGISTRY/$ECR_REPOSITORY:latest \
.
docker push $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $REGISTRY/$ECR_REPOSITORY:latest
echo "image=$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

- name: Scan image for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ steps.build.outputs.image }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: '1'

# ── Step 3: Update GitOps Repository ────────────────────
update-gitops:
name: Update GitOps
runs-on: ubuntu-latest
needs: build-and-push
steps:
- name: Checkout GitOps repository
uses: actions/checkout@v4
with:
repository: your-org/gitops
token: ${{ secrets.GITOPS_TOKEN }}
path: gitops

- name: Update image tag
working-directory: gitops
run: |
# Using yq to update image tag in Kustomize overlay
yq e '.spec.template.spec.containers[0].image = "${{ needs.build-and-push.outputs.image }}"' \
-i kubernetes/apps/my-service/overlays/production/deployment-patch.yaml

git config user.email "github-actions@your-org.com"
git config user.name "GitHub Actions"
git add -A
git commit -m "chore(deploy): bump my-service to ${{ env.IMAGE_TAG }}

Deployed by: ${{ github.actor }}
Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
git push

Secrets Management in CI

Never store secrets as plaintext in CI environment variables. DevOpsGenie integrates with AWS Secrets Manager and OIDC:

# Use OIDC for AWS authentication — no long-lived credentials
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions
aws-region: us-east-1

# Fetch runtime secrets from Secrets Manager
- name: Fetch secrets
uses: aws-actions/aws-secretsmanager-get-secrets@v2
with:
secret-ids: |
SONAR_TOKEN, /ci/sonarcloud/token
NPM_TOKEN, /ci/npm/token

Kustomize Overlays

Use Kustomize for environment-specific configuration without duplicating base manifests:

kubernetes/apps/my-service/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── hpa.yaml
│ └── kustomization.yaml
└── overlays/
├── staging/
│ ├── kustomization.yaml
│ └── deployment-patch.yaml # 2 replicas, smaller resources
└── production/
├── kustomization.yaml
└── deployment-patch.yaml # 6 replicas, larger resources, PDB
kubernetes/apps/my-service/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../base

namespace: team-payments

images:
- name: my-service
newTag: "abc1234" # updated by CI pipeline

patches:
- path: deployment-patch.yaml
- path: hpa-patch.yaml

commonLabels:
environment: production

Pipeline Quality Gates

Define mandatory quality gates that must pass before promotion:

GateToolFailure Action
Unit testsJest / pytestBlock PR merge
Lint & type checkESLint / mypyBlock PR merge
Container scanTrivyBlock image push (CRITICAL)
SASTCodeQL / SemgrepBlock PR merge
Integration testsPlaywright / pytestBlock staging deploy
Smoke testsk6 / customBlock production promote
SLO analysisPrometheus + ArgoAuto-rollback

Branching Strategy

DevOpsGenie recommends trunk-based development with short-lived feature branches:

  • main — the trunk, always deployable
  • feature/* — short-lived, merged within 2 days
  • hotfix/* — emergency fixes, merged directly to main

Avoid long-running release branches. Use feature flags to decouple deployment from feature release.

Next Steps