GitHub Actions Real-World Project

Now, you will see how CI, CD, Docker, secrets, environments, caching, artifacts, concurrency control, and notifications work together in a single multi-workflow project for a Node.js web application.

Project Overview

The project is a Node.js REST API that needs the following automated pipeline:

  • Run lint and tests on every pull request
  • Block merges when tests fail
  • Build and push a Docker image on every merge to main
  • Deploy automatically to staging after a successful image build
  • Require manual approval before deploying to production
  • Notify the team on Slack after every deployment
  • Run a weekly dependency audit every Monday at 8:00 AM UTC

Repository Structure

my-api/
├── src/
│   └── index.js
├── tests/
│   └── api.test.js
├── Dockerfile
├── package.json
├── package-lock.json
└── .github/
    └── workflows/
        ├── ci.yml             ← Pull request checks
        ├── release.yml        ← Build, push, deploy
        └── weekly-audit.yml   ← Scheduled security scan

Workflow 1: ci.yml — Pull Request Checks

name: CI

on:
  pull_request:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: ci-pr-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run lint

  test:
    name: Test (Node ${{ matrix.node }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node: [18, 20, 22]

    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: 'npm'

      - run: npm ci

      - name: Run tests with coverage
        run: npm test -- --coverage --ci

      - name: Upload coverage report
        if: matrix.node == 20
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: ./coverage
          retention-days: 5

What this workflow does

PR opened or updated
        │
        ├── lint job ──────────────────────────────────┐
        │                                              │
        ├── test job (Node 18) ─────────────────────── ┤ parallel
        ├── test job (Node 20) ─────────────────────── ┤
        └── test job (Node 22) ─────────────────────── ┘
                                                       │
                              All green → PR shows ✓ All checks passed
                              Any red   → PR shows ✗ Merge blocked

Workflow 2: release.yml — Build, Push, and Deploy

name: Release

on:
  push:
    branches: [main]

permissions:
  contents: read
  packages: write
  id-token: write

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

jobs:
  build-image:
    name: Build and Push Docker Image
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.version }}

    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

      - 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 image metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha,format=short
            type=raw,value=latest

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-staging:
    name: Deploy to Staging
    needs: build-image
    runs-on: ubuntu-latest
    environment: staging

    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_STAGING_ROLE_ARN }}
          aws-region: us-east-1

      - name: Deploy to ECS staging
        run: |
          aws ecs update-service \
            --cluster staging-cluster \
            --service my-api-staging \
            --force-new-deployment

      - name: Wait for deployment to stabilize
        run: |
          aws ecs wait services-stable \
            --cluster staging-cluster \
            --services my-api-staging

      - name: Notify Slack — staging deployed
        if: success()
        run: |
          curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
            -H 'Content-type: application/json' \
            --data '{
              "text": "✅ Deployed to *staging* — image tag: ${{ needs.build-image.outputs.image-tag }}"
            }'

      - name: Notify Slack — staging failed
        if: failure()
        run: |
          curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
            -H 'Content-type: application/json' \
            --data '{
              "text": "🚨 *Staging deployment FAILED* — investigate immediately."
            }'

  deploy-production:
    name: Deploy to Production
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_PROD_ROLE_ARN }}
          aws-region: us-east-1

      - name: Deploy to ECS production
        run: |
          aws ecs update-service \
            --cluster production-cluster \
            --service my-api-prod \
            --force-new-deployment

      - name: Wait for production to stabilize
        run: |
          aws ecs wait services-stable \
            --cluster production-cluster \
            --services my-api-prod

      - name: Notify Slack — production deployed
        if: success()
        run: |
          curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
            -H 'Content-type: application/json' \
            --data '{
              "text": "🚀 Deployed to *production* — image tag: ${{ needs.build-image.outputs.image-tag }}"
            }'

      - name: Notify Slack — production failed
        if: failure()
        run: |
          curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
            -H 'Content-type: application/json' \
            --data '{
              "text": "🔥 *PRODUCTION deployment FAILED* — rollback required!"
            }'

Full release pipeline flow

Merge to main
      │
      ▼
build-image job
  → Builds Docker image
  → Pushes to GHCR with SHA tag + latest tag
  → Outputs image tag
      │ success
      ▼
deploy-staging job
  → Uses OIDC to authenticate with AWS
  → Triggers ECS rolling deployment on staging cluster
  → Waits for service to become stable
  → Posts Slack notification (success or failure)
      │ success
      ▼
  [APPROVAL GATE]
  → GitHub emails production environment reviewers
  → Reviewer visits Actions tab → clicks Approve
      │ approved
      ▼
deploy-production job
  → Uses OIDC to authenticate with AWS (prod role)
  → Triggers ECS rolling deployment on production cluster
  → Waits for service to become stable
  → Posts Slack notification (success or failure)

Workflow 3: weekly-audit.yml — Scheduled Security Scan

name: Weekly Dependency Audit

on:
  schedule:
    - cron: '0 8 * * 1'      # Every Monday at 8:00 AM UTC
  workflow_dispatch:          # Also allow manual trigger

permissions:
  contents: read
  issues: write               # Permission to create GitHub Issues

jobs:
  audit:
    name: npm Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      - name: Run security audit
        id: audit
        run: |
          npm audit --audit-level=high > audit-report.txt 2>&1
          echo "exit_code=$?" >> $GITHUB_OUTPUT
        continue-on-error: true

      - name: Upload audit report
        uses: actions/upload-artifact@v4
        with:
          name: weekly-audit-report
          path: audit-report.txt
          retention-days: 30

      - name: Create GitHub Issue if vulnerabilities found
        if: steps.audit.outputs.exit_code != '0'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const report = fs.readFileSync('audit-report.txt', 'utf8');
            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `Security: High-severity vulnerabilities found — ${new Date().toDateString()}`,
              body: `## Weekly Dependency Audit\n\n\`\`\`\n${report.substring(0, 5000)}\n\`\`\`\n\nPlease review and update affected packages.`,
              labels: ['security', 'dependencies']
            });

Secrets and Environments Required for This Project

Repository Secrets:
  SLACK_WEBHOOK          → Slack Incoming Webhook URL

Environment: staging
  AWS_STAGING_ROLE_ARN   → IAM Role ARN for staging deployment

Environment: production
  AWS_PROD_ROLE_ARN      → IAM Role ARN for production deployment
  Required reviewers: [lead-engineer, tech-lead]

GITHUB_TOKEN is automatic — no setup needed.

Concepts Used in This Project

Topic                    | Where it appears
-------------------------|-------------------------------------------
Triggers                 | push, pull_request, schedule, workflow_dispatch
Runners                  | ubuntu-latest throughout
Jobs and Steps           | All three workflows
Marketplace Actions      | checkout, setup-node, docker actions, aws actions
Environment Variables    | Throughout
Secrets                  | SLACK_WEBHOOK, AWS role ARNs, GITHUB_TOKEN
Expressions/Contexts     | Image tags, branch names, step outputs
Conditional Steps        | if: success(), if: failure()
Matrix Builds            | Node 18/20/22 in ci.yml
Artifacts                | Coverage report, audit report
Caching                  | npm cache via setup-node
Reusable Patterns        | Notification steps, ECS deploy pattern
CI Pipeline              | ci.yml
CD and Deployments       | release.yml staging and production jobs
Docker Integration       | build-image job with GHCR
Cloud Deployments        | AWS ECS via OIDC
Custom Actions           | github-script for issue creation
Security Best Practices  | OIDC, pinned SHAs, least-privilege permissions
Concurrency Control      | Per-PR concurrency in CI, serialized releases
Debugging                | continue-on-error, step outputs
Self-Hosted Runners      | Swap ubuntu-latest for self-hosted if needed

What to Build Next

The skills you built here apply to every team size, technology stack, and cloud platform. The next step is to apply these patterns to your own projects:

  • Add a CI workflow to a repository you currently maintain
  • Replace any manual deployment process with a CD workflow
  • Build a custom action to share logic across your organization's repositories
  • Set up self-hosted runners if your team has private network requirements
  • Audit your existing workflows against the security checklist from Topic 21

Every workflow you write makes the next one easier. The patterns repeat across all projects, and the YAML structure stays the same whether you are deploying a small side project or a large enterprise platform.

Leave a Comment

Your email address will not be published. Required fields are marked *