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.
