DevOps
CI/CD Pipeline with GitHub Actions
Build production-grade CI/CD pipelines with GitHub Actions including testing, security scanning, Docker builds, and multi-environment deployment strategies.
What you'll learn
- Structure a multi-job GitHub Actions workflow
- Run tests and enforce code quality gates
- Build and push Docker images to GitHub Container Registry
- Implement matrix builds across Node.js versions
- Run security scans with Trivy and CodeQL
- Deploy to staging and production with environment approvals
- Cache dependencies for fast build times
Workflow Basics
YAML
1# .github/workflows/ci.yml
2name: CI
3
4on:
5 push:
6 branches: [main, develop]
7 pull_request:
8 branches: [main]
9
10jobs:
11 build:
12 runs-on: ubuntu-24.04
13 strategy:
14 matrix:
15 node-version: [20, 22]
16
17 steps:
18 - uses: actions/checkout@v4
19
20 - uses: pnpm/action-setup@v4
21 with:
22 version: 10
23
24 - uses: actions/setup-node@v4
25 with:
26 node-version: ${{ matrix.node-version }}
27 cache: pnpm
28
29 - run: pnpm install --frozen-lockfile
30 - run: pnpm type-check
31 - run: pnpm lint
32 - run: pnpm test --coverageTesting & Quality
YAML
1 - name: Run tests with coverage
2 run: pnpm test --coverage --reporter=junit --outputFile=test-results.xml
3
4 - name: Upload test results
5 uses: actions/upload-artifact@v4
6 if: always()
7 with:
8 name: test-results-${{ matrix.node-version }}
9 path: test-results.xml
10
11 - name: Upload coverage to Codecov
12 uses: codecov/codecov-action@v4
13 with:
14 token: ${{ secrets.CODECOV_TOKEN }}
15 fail_ci_if_error: true
16 threshold: 80%Docker Build & Push
YAML
1 build-image:
2 needs: build
3 runs-on: ubuntu-24.04
4 permissions:
5 contents: read
6 packages: write
7
8 steps:
9 - uses: actions/checkout@v4
10
11 - uses: docker/setup-buildx-action@v3
12
13 - uses: docker/login-action@v3
14 with:
15 registry: ghcr.io
16 username: ${{ github.actor }}
17 password: ${{ secrets.GITHUB_TOKEN }}
18
19 - uses: docker/metadata-action@v5
20 id: meta
21 with:
22 images: ghcr.io/${{ github.repository }}
23 tags: |
24 type=ref,event=branch
25 type=sha,prefix=sha-
26 type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
27
28 - uses: docker/build-push-action@v5
29 with:
30 context: .
31 platforms: linux/amd64,linux/arm64
32 push: true
33 tags: ${{ steps.meta.outputs.tags }}
34 cache-from: type=gha
35 cache-to: type=gha,mode=maxSecurity Scanning
YAML
1 security:
2 runs-on: ubuntu-24.04
3 permissions:
4 security-events: write
5 steps:
6 - uses: actions/checkout@v4
7
8 # Dependency audit
9 - run: pnpm audit --audit-level=high
10
11 # Container image scan
12 - uses: aquasecurity/trivy-action@master
13 with:
14 image-ref: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
15 severity: HIGH,CRITICAL
16 exit-code: 1
17
18 # Code analysis
19 - uses: github/codeql-action/init@v3
20 with:
21 languages: javascript
22 - uses: github/codeql-action/analyze@v3Deploy to Production
YAML
1 deploy-staging:
2 needs: [build-image, security]
3 runs-on: ubuntu-24.04
4 environment: staging
5 steps:
6 - uses: appleboy/ssh-action@v1
7 with:
8 host: ${{ secrets.STAGING_HOST }}
9 username: deploy
10 key: ${{ secrets.DEPLOY_KEY }}
11 script: |
12 docker pull ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
13 docker compose -f /opt/app/compose.prod.yml up -d --force-recreate
14
15 deploy-production:
16 needs: deploy-staging
17 runs-on: ubuntu-24.04
18 environment:
19 name: production # requires manual approval in GitHub Environments
20 steps:
21 - uses: appleboy/ssh-action@v1
22 with:
23 host: ${{ secrets.PROD_HOST }}
24 username: deploy
25 key: ${{ secrets.DEPLOY_KEY }}
26 script: |
27 docker pull ghcr.io/${{ github.repository }}:latest
28 docker compose -f /opt/app/compose.prod.yml up -d --force-recreateCaching & Optimisation
- Use
cache: pnpminsetup-node— caches~/.pnpm-storeautomatically. - Use
cache-from/cache-to: type=ghainbuild-push-actionfor Docker layer caching. - Split large workflows into reusable workflows with
workflow_calltriggers. - Use
concurrencygroups to cancel outdated runs on the same branch. - Run independent jobs in parallel — use
needsonly when there is a true dependency.