CI/CD Pipelines with GitHub Actions: From Commit to Production
Manual deployments are one of the highest-risk activities in software engineering. A developer SSHes into a production server, runs commands from memory, makes a mistake, and causes an outage at 5pm on a Friday. Continuous Integration and Continuous Deployment (CI/CD) automates the path from a Git commit to a running system — running tests, building containers, and deploying to production without manual intervention. Git Branching Strategy explains how branch shape affects that pipeline.
This article covers GitHub Actions workflow patterns valid as of July 2026: workflow anatomy, build and test, container publishing, deployment strategies, and secrets. Action releases and runner images move; verify current action inputs and pin reviewed production dependencies before copying these teaching examples. Place the workflow in the wider operating path with What Full Stack Really Means.
CI vs CD: What the Letters Actually Mean
Continuous Integration (CI) is the practice of merging code changes frequently and automatically validating them with a build and test suite. The goal is to catch integration bugs — where two developers' changes conflict — within minutes, not during a quarterly release crunch. CI runs on every pull request and merge.
Continuous Delivery (CD) extends CI by automatically deploying validated code to a staging environment. Deployment to production still requires a manual approval step. Continuous Deployment (also CD) removes that manual step — every green build on main goes to production automatically. Most teams target Continuous Delivery (auto-deploy to staging, manual gate to production) and move to Continuous Deployment as confidence in their test suite grows.
Quick reference
- CI: code is automatically built and tested on every push. Merge only when green.
- Continuous Delivery: CD deploys to staging automatically. Production deploy is manual but always ready.
- Continuous Deployment: every green main build goes directly to production — no human approval.
- Trunk-based development: everyone commits to main (or short-lived branches < 1 day). Required for true CD.
- Feature flags: use flags to deploy dark code to production without activating it — decouples deploy from release.
- Mean time to recovery (MTTR): small, frequent deploys reduce the blast radius of failures and speed recovery.
Remember this
CI ensures every commit is verified. CD ensures it's deployable. Start with Continuous Delivery (auto-staging, manual production) and graduate to Continuous Deployment.
GitHub Actions Anatomy
A GitHub Actions workflow is a YAML file in .github/workflows/. A workflow has triggers, jobs, and sequential steps within each job. Jobs run on GitHub-hosted or self-hosted runners.
Actions are executable third-party dependencies. A major tag such as @v4 is convenient for a readable teaching sample but mutable; it does not automatically deliver a security fix unless a later run resolves to a moved tag. For production, resolve a reviewed release from the action's canonical repository, pin its verified full commit SHA, keep the release tag in a comment, and use Dependabot or review to propose updates. GitHub's official guidance is docs.github.com/en/actions/reference/security/secure-use (see “Pin actions to a full-length commit SHA”); workflow syntax is at docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax.
Quick reference
- on: defines triggers — push, pull_request, schedule (cron), workflow_dispatch (manual).
- jobs: each job runs on a fresh VM in parallel by default. Use needs: [job-name] for sequencing.
- steps: run sequentially within a job. Each step is a uses: (action) or run: (shell command).
- services: spin up Docker containers alongside the job (database, Redis, mock servers).
- setup-node cache: npm caches npm's download cache; npm ci still rebuilds node_modules on each fresh runner.
- Pin actions to verified full commit SHAs for immutability; major tags are mutable update channels.
- Concurrency: cancel in-flight runs on the same branch when a new commit is pushed.
Remember this
One YAML file per workflow. Separate jobs for test, build, and deploy. Use services for database integration tests. Cache dependencies.
Docker Build and Container Registry Push
After tests pass, the deploy artifact can be a Docker image. Tagging it with the full Git SHA makes the artifact traceable to the code that produced it.
Multi-stage Dockerfiles separate build tools from the runtime filesystem and can reduce size and attack surface. The result depends on base image, native libraries, framework output, and whether production dependencies are pruned; measure with docker image ls instead of targeting a universal size.
Quick reference
- Tag images with git SHA (github.sha) — every image traceable to exact code. Also tag latest for convenience.
- Multi-stage builds: builder stage has compiler/tools, runtime stage has only the binary. Smaller, more secure.
- GitHub Container Registry (ghcr.io): free for public, included with GitHub. No extra credentials needed.
- Layer caching (type=gha) can reduce rebuild time when relevant layers are reusable; record cold and warm timings.
- Scan images: add trivy or grype scanning step to catch CVEs before pushing.
- Never bake secrets into images. Use runtime environment variables or secret stores (AWS Secrets Manager, Vault).
Remember this
Build multi-stage images tagged with the Git SHA, push only after tests pass, and measure cache effectiveness on your workload.
Deployment Strategies: Rolling, Blue-Green, Canary
How you deploy matters as much as what you deploy. Rolling deployments replace instances one at a time — new pods come up, old pods come down, traffic gradually shifts. This means during deployment, old and new versions run simultaneously, which requires backward compatibility for any shared database changes.
Blue-green deployments run two identical environments (blue and green). You deploy to the idle environment, run smoke tests, then flip traffic. Rollback is instant — flip traffic back. Canary deployments shift a small percentage of traffic (1–5%) to the new version first, monitor error rates and latency, then gradually increase. Each strategy trades deployment speed for risk reduction.
Quick reference
- Rolling: gradual, requires API backward compatibility during rollout. Default in Kubernetes.
- Blue-green: instant rollback, doubles infrastructure cost during deploy. Good for risky migrations.
- Canary: partial traffic to new version, monitor metrics, increase gradually. Best for high-traffic services.
- Readiness probes: Kubernetes waits for /health to return 200 before routing traffic. Essential for zero-downtime.
- Rollback: kubectl rollout undo deployment/api — reverts to previous ReplicaSet in seconds.
- Feature flags instead of canary: deploy fully, activate for 1% of users via flags. Separates deploy from release.
Remember this
Use rolling deploys with readiness probes for zero-downtime. Add a smoke test step that auto-rolls back on failure. Blue-green for database migrations; canary for high-risk features.
Secrets and Environment Management
Secrets in code are one of the most common security vulnerabilities. API keys, database passwords, and JWT secrets hardcoded in source code get committed to version history, exposed in CI logs, and leaked by any developer who clones the repo. The rule is absolute: secrets never live in the repository.
GitHub Secrets (Settings → Secrets) are encrypted at rest and injected as environment variables into workflows. They're masked in logs — if a secret value appears in a log line, GitHub replaces it with ***. For production systems, a dedicated secret store (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) provides rotation, audit logging, and fine-grained access control.
For cloud deployment credentials specifically, skip GitHub Secrets entirely and use OpenID Connect (OIDC) instead. Rather than storing a long-lived cloud access key that could leak, the job presents a signed token identifying its exact repository, ref, and workflow; the cloud provider checks that token against a trust policy and issues a scoped credential that expires in minutes. There is no static key sitting in Settings → Secrets for a leaked log line, a compromised dependency, or a departed contributor to exploit.
Quick reference
- GitHub Secrets: encrypted, injected as env vars, masked in logs. Free, good for CI/CD secrets.
- OIDC for cloud deploys: the job exchanges a signed identity token for a short-lived role — no static cloud key to store or leak.
- AWS Secrets Manager / Azure Key Vault: rotation support, audit logging, IAM-based access. For production runtime.
- HashiCorp Vault: self-hosted, dynamic secrets (generates temporary DB credentials on demand).
- Commit .env.example with structure but no values. Add .env to .gitignore.
- Scan for secrets in CI: add gitleaks or truffleHog as a pre-merge check to catch accidental commits.
- Rotate secrets regularly. Use separate secrets per environment (dev/staging/prod).
Remember this
Never commit secrets to a repository. Use GitHub Secrets for CI/CD values, OIDC for cloud deployment credentials so no static key exists to leak, and a secret store (Secrets Manager, Vault) for runtime configuration.
Key takeaway
A useful pipeline produces evidence: the same locked install, tests, immutable artifact identity, deployment status, smoke result, and rollback result on every run. Automate only as far as your tests and recovery path justify; production approvals can remain an intentional control.
Practice (30 min): create a tiny Node app with /health, a failing unit-test branch, and the CI workflow above. Open a pull request and prove the failing test blocks the build job. Fix it, merge to a non-production environment, verify the pushed image tag equals github.sha, then deploy an image whose /health returns 503 and confirm the job runs rollout undo, waits for recovery, and exits non-zero.
Related Articles
Explore this topic