How to Set Up CI/CD with GitHub Actions (2026)
GitHub Actions lets you automate testing, building, and deploying your code every time you push. No external CI service needed — it's built into GitHub. Here's a practical setup for web applications in 2026.
What You'll Build
A CI/CD pipeline that:
- Runs on every pull request: Lint, type-check, test
- Runs on merge to main: Build and deploy to production
- Caches dependencies: Fast builds
- Notifies on failure: Slack or email alerts
Prerequisites
- A GitHub repository
- A web app (Next.js, Astro, or similar)
- A deployment target (Vercel, Cloudflare, AWS, etc.)
Step 1: Basic CI Workflow
Create .github/workflows/ci.yml:
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- run: npm ci
- name: Lint
run: npm run lint
- name: Type Check
run: npm run typecheck
- name: Test
run: npm test
This runs lint, type checking, and tests on every PR and push to main.
Step 2: Add Caching
npm ci is slow without caching. The cache: 'npm' in setup-node handles this, but for more control:
- uses: actions/cache@v4
with:
path: |
node_modules
.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-nextjs-
This caches node_modules and Next.js build cache. Cuts build time by 50-70%.
Step 3: Deploy on Merge
Add a separate deployment job that only runs on main:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
needs: ci # Wait for CI to pass
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- run: npm ci
- run: npm run build
# Deploy to Vercel
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Alternative: Deploy to Cloudflare Pages
- name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: pages deploy ./out --project-name=my-app
Alternative: Deploy to AWS S3 + CloudFront
- name: Deploy to S3
uses: jakejarvis/s3-sync-action@v0.5.1
with:
args: --delete
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
SOURCE_DIR: 'out'
Step 4: Add Secrets
Go to your repo → Settings → Secrets and variables → Actions. Add:
VERCEL_TOKEN(or equivalent for your platform)- Any API keys your build needs
Never put secrets in workflow files. Always use ${{ secrets.NAME }}.
Step 5: Add Status Checks
In your repo settings → Branches → Branch protection rules:
- Require status checks to pass before merging
- Select your CI workflow
- Now PRs can't merge if CI fails
Step 6: Matrix Testing (Optional)
Test across multiple Node versions or operating systems:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test
Step 7: Slack Notifications (Optional)
Add failure notifications:
- name: Notify Slack on Failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
channel: '#deployments'
text: 'CI failed on ${{ github.ref }}'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Complete CI/CD Workflow
Here's everything combined:
name: CI/CD
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- run: npm ci
- name: Lint
run: npm run lint
- name: Type Check
run: npx tsc --noEmit
- name: Test
run: npm test -- --coverage
- name: Build
run: npm run build
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: ci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Deploy
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Key features:
concurrencycancels in-progress runs when new commits push (saves minutes)- Deploy only runs on push to main (not on PRs)
- Build runs in CI to catch build errors before deploy
Common Issues & Fixes
"npm ci" fails
Make sure package-lock.json is committed. npm ci requires it.
Slow builds
Add caching (Step 2). Also consider running lint and test in parallel using separate jobs.
Secret not found
Double-check the secret name matches exactly (case-sensitive). Secrets aren't available in forked PRs.
Workflow doesn't trigger
Check the on: section. Common mistake: indentation errors in YAML.
GitHub Actions Pricing
| Plan | Included Minutes | Extra Cost |
|---|---|---|
| Free | 2,000 min/month | N/A |
| Team | 3,000 min/month | $0.008/min |
| Enterprise | 50,000 min/month | $0.008/min |
For most projects, the free tier is more than enough. A typical CI run takes 2-5 minutes.
FAQ
Should I use GitHub Actions or a separate CI service?
For most projects, GitHub Actions is sufficient and free. Consider CircleCI or BuildKite if you need advanced features (GPU runners, massive parallelism).
How do I debug a failing workflow?
Click the failed run in the Actions tab. Expand the failing step. Add - run: env to see environment variables. Use actions/upload-artifact to save logs.
Can I run workflows locally?
Yes, use act (https://github.com/nektos/act). Runs GitHub Actions locally in Docker. Great for debugging.
How do I handle monorepos?
Use path filters: on: push: paths: ['packages/api/**'] to only run when specific paths change.
Bottom Line
GitHub Actions gives you free CI/CD that's deeply integrated with your GitHub workflow. Start with the basic CI workflow (Step 1), add deployment (Step 3), and iterate. Most web apps need under 50 lines of YAML to have a complete CI/CD pipeline.