A github actions ci/cd pipeline is now the default choice for most engineering teams – and for good reason. It’s native to GitHub, requires zero infrastructure to maintain, and scales from a solo developer’s side project to a Series B startup shipping dozens of times per day.
This guide covers everything you need to build a production-ready CI/CD pipeline with GitHub Actions: automated testing, Docker builds, container registry pushes, Kubernetes deployments, secrets management, and environment-based promotion workflows. Every example is based on real patterns used in production.
What Makes GitHub Actions the Right CI/CD Choice in 2026
Before we build, it’s worth understanding why GitHub Actions has become the dominant CI/CD platform. The answer is integration depth – Actions is built into the same platform where your code lives, your pull requests are reviewed, and your issues are tracked.
That means no separate login, no webhook configuration, no separate YAML syntax to learn for your pipeline tool vs your infrastructure tool. One platform, one place.
The other reason is the marketplace. With thousands of community-built actions – for everything from Slack notifications to Terraform deployments to Kubernetes rollouts – you rarely need to write boilerplate from scratch.
Prerequisites
Before you start, make sure you have:
- A GitHub repository with your application code.
- Docker installed locally for testing builds.
- A container registry – Docker Hub, GitHub Container Registry (GHCR), or AWS ECR.
- A Kubernetes cluster if you want to follow the deployment sections – EKS, GKE, AKS, or local with kind.
Step 1 – Understanding the GitHub Actions Structure
Every github actions ci/cd pipeline lives in .github/workflows/ in your repository. Each workflow is a YAML file that defines when it runs and what it does.
The core concepts:
- Workflow – the top-level file, triggered by events.
- Job – a set of steps that run on the same runner.
- Step – a single action or shell command.
- Runner – the machine that executes the job (GitHub-hosted or self-hosted).
- Action – a reusable unit of code from the marketplace or your own repo.
# .github/workflows/ci.yaml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run tests
run: |
echo "Running tests..."This is the minimum viable workflow – it triggers on pushes and PRs to main and develop, checks out the code, and runs a command.
Step 2 – Build a Complete CI Workflow with Testing
A production CI workflow does more than run tests. It lints, tests, builds, and reports. Here’s a complete example for a Node.js application:
# .github/workflows/ci.yaml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage report
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: trueThe matrix strategy runs your tests against multiple Node.js versions in parallel – if it works on 18 and 20, you’re confident it’ll work in production. The cache: 'npm' line caches your node_modules between runs, cutting install time from 60 seconds to under 5.
Step 3 – Add Docker Build and Push to Your Pipeline
Once tests pass, build your Docker image and push it to your container registry. This example uses GitHub Container Registry (GHCR), which is free for public repos and included with GitHub plans for private repos:
build-and-push:
runs-on: ubuntu-latest
needs: lint-and-test
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=maxThe needs: lint-and-test line ensures this job only runs if tests pass – no point building a broken image. The cache-from: type=gha uses GitHub Actions cache for Docker layer caching, which dramatically speeds up subsequent builds.
Step 4 – Secrets Management
Never hardcode credentials in your workflows. GitHub Actions has two levels of secrets:
- Repository secrets – available to all workflows in a repo.
- Environment secrets – only available to workflows deploying to a specific environment.
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1To add secrets: go to your repo → Settings → Secrets and variables → Actions → New repository secret.
For more sensitive environments, use OIDC (OpenID Connect) instead of long-lived credentials. OIDC lets GitHub Actions authenticate directly with AWS, GCP, or Azure without storing static keys:
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: eu-west-1This is the production-grade approach – no static keys, no rotation headaches.
Step 5 – Deploy to Kubernetes
With your image built and pushed, the next step in a github actions ci/cd pipeline is deploying to Kubernetes. This example deploys to EKS using kubectl.
This is where a github actions ci/cd pipeline really earns its place – automated deployments that are auditable, repeatable, and triggered by a single git push.
deploy-staging:
runs-on: ubuntu-latest
needs: build-and-push
environment: staging
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: eu-west-1
- name: Update kubeconfig
run: |
aws eks update-kubeconfig \
--name your-cluster-name \
--region eu-west-1
- name: Deploy to Kubernetes
run: |
# Update image tag in deployment
kubectl set image deployment/your-app \
your-app=ghcr.io/${{ github.repository }}:sha-${{ github.sha }} \
--namespace staging
# Wait for rollout to complete
kubectl rollout status deployment/your-app \
--namespace staging \
--timeout=5m
- name: Verify deployment
run: |
kubectl get pods -n staging -l app=your-appThe environment: staging line activates GitHub Environments – you can configure required reviewers, deployment protection rules, and environment-specific secrets all from the GitHub UI.
Step 6 – Promotion Workflow: Staging to Production
The most important part of a mature github actions ci/cd pipeline is controlled promotion between environments. Here’s a pattern where staging deploys automatically but production requires manual approval:
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
environment: production # This environment has required reviewers configured
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_PROD_ROLE_ARN }}
aws-region: eu-west-1
- name: Update kubeconfig for production
run: |
aws eks update-kubeconfig \
--name your-prod-cluster \
--region eu-west-1
- name: Deploy to production
run: |
kubectl set image deployment/your-app \
your-app=ghcr.io/${{ github.repository }}:sha-${{ github.sha }} \
--namespace production
kubectl rollout status deployment/your-app \
--namespace production \
--timeout=10m
- name: Notify Slack on success
uses: slackapi/[email protected]
with:
payload: |
{
"text": "✅ Production deployment successful: ${{ github.repository }} @ ${{ github.sha }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}Configure the production environment in GitHub: Settings → Environments → production → add required reviewers. This creates a manual gate – GitHub will pause the workflow and send an email to reviewers before deploying to production.
Step 7 – Reusable Workflows for Multi-Service Repos
If you have multiple services sharing the same github actions ci/cd pipeline pattern, reusable workflows prevent duplication. Define a workflow once and call it from multiple places – this is how mature engineering teams scale their CI/CD without copy-pasting YAML across repos.
# .github/workflows/reusable-deploy.yaml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
secrets:
aws-role-arn:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Deploy
run: |
echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"Call it from another workflow:
deploy:
uses: ./.github/workflows/reusable-deploy.yaml
with:
environment: staging
image-tag: sha-${{ github.sha }}
secrets:
aws-role-arn: ${{ secrets.AWS_ROLE_ARN }}Common Issues and How to Fix Them
Workflow not triggering on push – check your on block. Branch names are case-sensitive and must match exactly. Use branches-ignore if you want to exclude certain branches.
Docker build failing on ARM – add platforms: linux/amd64,linux/arm64 to your build-push-action if you’re deploying to ARM nodes (AWS Graviton, Apple Silicon development).
kubectl: command not found – add uses: azure/setup-kubectl@v3 as a step before your kubectl commands.
Deployment timeout – increase --timeout on kubectl rollout status. The default is 5 minutes – complex deployments with PodDisruptionBudgets or slow health checks may need more.
Secrets not available in forked PRs – this is a security feature, not a bug. Secrets are intentionally not passed to workflows triggered by pull requests from forks. Use environments with required approvals for external contributors.
What to Set Up Next
Once your github actions ci/cd pipeline is running in production, these are the logical next improvements:
- Dependabot for Actions – automatically update your action versions when new releases come out.
- Branch protection rules – require CI to pass before merging to main.
- Deployment frequency tracking – use DORA metrics to measure how often you’re shipping.
- Rollback automation – configure
kubectl rollout undoto trigger automatically on failed health checks.
For the official GitHub Actions documentation and full syntax reference, see the GitHub Actions documentation.
Conclusion
A production-ready github actions ci/cd pipeline transforms how your team ships software – from manual, error-prone deployments to automated, auditable, repeatable releases. The patterns in this guide cover the full lifecycle: test, build, push, deploy, and promote across environments with proper secrets management and manual gates where you need them.
If you need someone to design and implement this infrastructure end to end – or to audit a pipeline that’s causing problems – this is exactly what we do at The Good Shell. See our DevOps and SRE services or read our case studies to see what production CI/CD looks like in practice.
