How to Set Up a GitHub Actions CI/CD Pipeline from Scratch
Step-by-step guide to building a production-ready GitHub Actions CI/CD pipeline: install, test, build, and deploy automatically on every push.
Most teams manually deploy code far longer than they should. The process is slow, error-prone, and blocks everyone every time something goes wrong. GitHub Actions gives you a straightforward path to full automation — tests run on every push, builds are reproducible, and deployments happen without anyone touching a server.
This guide walks you through setting up a complete CI/CD pipeline with GitHub Actions from an empty workflow file to a production-grade release process.
Setting Up a GitHub Actions CI/CD Pipeline: The Foundation
GitHub Actions organizes work into workflows — YAML files stored in .github/workflows/. Each workflow runs on a trigger (a push, a pull request, a schedule) and contains one or more jobs that execute on virtual runners.
Create your first workflow file at .github/workflows/ci.yml.
Step 1: Define Your Trigger
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]This runs the pipeline whenever code is pushed to main or a pull request targets main. Pull requests get CI feedback before merge; pushes to main trigger the full deploy path.
Step 2: Run Tests in a Dedicated Job
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm testAlways use npm ci instead of npm install in CI — it installs exactly what the lockfile specifies and fails if the lockfile is out of sync. Cache dependencies using the cache key on the setup action; this alone can cut 60-90 seconds off a typical Node.js job.
Step 3: Build and Validate
Add a build job that depends on test passing:
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/Upload the build artifact so the deploy job can download it instead of rebuilding. This keeps your artifacts reproducible — the exact same bytes that passed tests are the bytes that ship.
Step 4: Deploy Only on Main
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: dist
- run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}The if condition is critical. Pull requests run test and build but never trigger deploy. Only a direct push to main reaches production. Secrets are stored in GitHub's encrypted secrets store and injected as environment variables at runtime — never hardcode tokens in workflow files.
Managing Environment Variables Safely
Store all sensitive values in Settings > Secrets and variables > Actions. Reference them as ${{ secrets.MY_SECRET }}. For non-sensitive configuration that differs per environment (API base URLs, feature flags), use Variables rather than Secrets so you can inspect them.
Speeding Up the Pipeline
- Cache aggressively. Node modules, pip packages, Go module caches — cache all of them.
- Parallelize independent jobs. Tests and lint checks do not need to run sequentially.
- Limit the trigger scope. Use
pathsfilters so a docs-only commit does not trigger a full deploy. - Set timeouts. Add
timeout-minutes: 10to jobs so a stuck test does not burn runner minutes indefinitely.
What a Mature Pipeline Looks Like
A production pipeline typically has five stages:
- Lint — enforce code style, fail fast
- Test — unit and integration tests in parallel
- Security scan — dependency audit, static analysis
- Build — produce a reproducible artifact
- Deploy — push the artifact, run smoke tests, notify on failure
Start with the middle three and add the outer two once the core flow is stable.
Common First-Time Mistakes
- Using
npm installinstead ofnpm ciin CI - Storing secrets in the repository or workflow file
- Deploying from feature branches instead of
main - Not caching dependencies, making every run slow
- Skipping a smoke test after deploy — you find out production is broken from users
Setting up a solid pipeline is a one-time investment that pays back on every subsequent release. If you want a team that can ship continuously without incidents, the pipeline is the foundation you build on.