Phase 1: Code Quality & Linting
Overview & Objectives
High code quality prevents bugs early in the development cycle. In this phase, you’ll configure automated linting workflows to enforce style rules, catch syntax errors, and generate shareable reports. By the end, you will:
- Understand GitHub Actions workflow structure for code quality
- Configure ESLint for JavaScript/TypeScript and Flake8/Black for Python
- Cache dependencies to speed up CI runs
- Upload lint results as artifacts for team visibility
- Verify and exercise your workflows end-to-end
Prerequisites
- A GitHub repository containing your application code
- JavaScript/TypeScript projects should have
eslint
configured (npm install eslint --save-dev
) - Python projects should have
flake8, black, mypy
configured (pip install flake8 black mypy
) - Basic understanding of GitHub Actions and YAML
Step 1: Create the Lint Workflow File
Create a file .github/workflows/lint.yml
in your repo root. This defines triggers and jobs for linting.
name: Code Linting on: push: branches: [ main, develop ] pull_request: branches: [ main ]
Step 2: Define the Lint Job
Under jobs:
, define a lint
job running on Ubuntu.
jobs: lint: name: Lint Code runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4
Step 3: Configure JavaScript/TypeScript Linting
Add steps to set up Node.js, install dependencies, run ESLint, and upload the JSON report.
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: npm - name: Install dependencies run: npm ci - name: Run ESLint run: | npm run lint -- --format=json --output-file=eslint-report.json npm run lint continue-on-error: true - name: Upload ESLint report uses: actions/upload-artifact@v4 if: always() with: name: eslint-report path: eslint-report.json
continue-on-error: true
so the workflow continues to upload the report even if lint fails.Step 4: Configure Python Linting
For Python projects, add a separate step block to run Flake8, Black, and MyPy.
- name: Setup Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install Python tools run: pip install flake8 black mypy - name: Run Flake8 run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - name: Run Black run: black --check --diff . - name: Run MyPy run: mypy . --ignore-missing-imports
Step 5: Verify Caching
Ensure dependency installation is cached across runs. The cache: npm
option in setup-node
reuses the ~/.npm
directory.
Step 6: Full Workflow Example
name: Code Linting on: [push, pull_request] jobs: lint: 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 lint -- --format=json --output-file=eslint-report.json npm run lint continue-on-error: true - uses: actions/upload-artifact@v4 if: always() with: name: eslint-report path: eslint-report.json - uses: actions/setup-python@v4 with: python-version: '3.11' - run: pip install flake8 black mypy - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - run: black --check --diff . - run: mypy . --ignore-missing-imports
Verify Your Work
- 🔍 Create a branch
feature/lint-test
, introduce a lint error, and open a PR. - ✅ Confirm “Lint Code” job runs, and ESLint report artifact appears.
- 📂 Download
eslint-report.json
and inspect errors.
Exercises
- Create a separate workflow
python-lint.yml
that only runs Python linters and uploads aflake8-report.txt
artifact. - Modify ESLint configuration to enforce a maximum line length of 100 and verify failures for >100 character lines.
***
Phase 2: Comprehensive Testing Framework
Overview & Objectives
Thorough testing uncovers defects before production. In this phase, you’ll set up a GitHub Actions workflow that runs unit, integration, and end-to-end tests in parallel using a test matrix. You will:
- Configure a test matrix for three test types
- Spin up dependent services—PostgreSQL for integration tests
- Upload coverage reports only for unit tests
- Optimize concurrency with
fail-fast
andmax-parallel
- Verify results and extend with a “smoke” test
Prerequisites
- Node.js project with scripts:
npm run test:unit
,npm run test:integration
,npm run test:e2e
- Python and pytest for Python projects (if applicable)
- Codecov or similar coverage uploader configured
- Basic Docker familiarity for services
Step 1: Create Test Workflow
Create .github/workflows/test.yml
:
name: Test Pipeline on: push: branches: [ main, develop ] pull_request: branches: [ main ]
Step 2: Define Test Job & Matrix
Under jobs:
, define test
with matrix strategy:
jobs: test: name: Run Tests runs-on: ubuntu-latest strategy: fail-fast: false max-parallel: 3 matrix: test-type: [unit, integration, e2e]
Step 3: Add PostgreSQL Service
Integration tests often require a database. Use services:
to start PostgreSQL:
services: postgres: image: postgres:15 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: testdb options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
Step 4: Checkout & Install
steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: npm - name: Install dependencies run: npm ci
Step 5: Run Tests by Type
Use a bash case
to execute each test type:
- name: Run ${{ matrix.test-type }} tests run: | case ${{ matrix.test-type }} in unit) npm run test:unit -- --coverage --ci ;; integration) npm run test:integration -- --ci --testTimeout=30000 ;; e2e) npm run test:e2e -- --headless ;; esac
Step 6: Upload Coverage for Unit Tests
Only unit tests generate coverage:
- name: Upload coverage report uses: codecov/codecov-action@v4 if: matrix.test-type == 'unit' with: file: ./coverage/lcov.info flags: ${{ matrix.test-type }}
coverage/lcov.info
. If missing, the upload step will fail.Step 7: Verify Workflow
- ▶️ Push to a feature branch and open a PR.
- 🔢 Confirm three parallel jobs: unit, integration, e2e.
- 📂 Download the coverage artifact for unit tests.
Exercises
- Add a fourth test type,
smoke
, running a minimal health check script (npm run test:smoke
), and include it in the matrix. - Modify the workflow to skip end-to-end tests for pull requests from forks for speed and security.
Extended Tips
- Retry Tests: For flaky tests, wrap
npm run
in a retry loop:for i in {1..3}; do npm run test && break; done
. - Parallelize within a job: Use
npm run test:unit -- --maxWorkers=4
to parallelize Jest tests. - Test Reports: Generate JUnit XML (
--reporters=junit
) and upload withactions/upload-artifact
for CI dashboards.
Phase 3: Build Optimization & Artifact Management
Why It Matters
Efficient builds accelerate delivery, reduce CI time and cost, and enable reliable downstream jobs. Multi-stage Docker builds produce minimal runtime images, BuildKit caching slashes rebuild times, and artifact uploads decouple build and deploy stages.
What You’ll Learn
- Multi-stage Dockerfile architecture
- Inline BuildKit caching in GitHub Actions
- Cross-platform images with Buildx
- Uploading images and caches as artifacts
Prerequisites
- Project Dockerfile supporting production build
- Familiarity with Docker layers and caching
- GitHub Actions runner with Docker support
1. Sample Multi-Stage Dockerfile
# Builder stage FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Runtime stage FROM node:18-alpine AS runner WORKDIR /app COPY --from=builder /app/dist ./dist COPY package*.json ./ RUN npm ci --production EXPOSE 3000 CMD ["node","dist/index.js"]
2. GitHub Actions Workflow
name: Build & Cache on: push: branches: [ main, develop ] jobs: build: runs-on: ubuntu-latest strategy: matrix: platform: [linux/amd64,linux/arm64] fail-fast: false steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build with inline cache uses: docker/build-push-action@v5 with: context: . file: Dockerfile platforms: ${{ matrix.platform }} tags: myapp:${{ github.sha }}-${{ matrix.platform }} cache-from: type=gha cache-to: type=gha,mode=max - name: Save image as artifact run: | docker save myapp:${{ github.sha }}-${{ matrix.platform }} \ | gzip > image-${{ matrix.platform }}.tar.gz - uses: actions/upload-artifact@v4 with: name: image-${{ matrix.platform }} path: image-${{ matrix.platform }}.tar.gz
type=gha
caching to eliminate manual cache upload steps and leverage GitHub’s built-in cache storage.3. Verify the Build
- ✅ Confirm 2 parallel jobs (amd64 & arm64) complete under “Actions.”
- 📂 Download
image-linux/amd64.tar.gz
and rungunzip -c image-*.tar.gz | docker load
. - ⚡ Re-run push to see caching speedup.
Phase 4: Deployment Automation with Blue/Green and Helm
Why Blue/Green Deployments?
Blue/green strategies reduce downtime and risk by running two identical production environments (“blue” and “green”), switching traffic to the new version only after smoke tests pass.
Prerequisites
- Helm charts in
charts/myapp
- Kubernetes cluster context via
KUBECONFIG
or service account
1. Workflow Configuration
name: Helm Blue/Green Deploy on: push: branches: [ main ] jobs: deploy: runs-on: ubuntu-latest environment: name: production url: ${{ steps.url.outputs.url }} steps: - uses: actions/checkout@v4 - name: Setup Helm & kubectl uses: azure/setup-helm@v3 with: version: '3.12.0' - uses: azure/setup-kubectl@v3 with: version: '1.27.0'
2. Determine Active Color
- name: Get active color id: active run: | BLUE_EXISTS=$(helm status myapp-blue -n prod && echo yes || echo no) if [ "$BLUE_EXISTS" = yes ]; then echo "color=green" >> $GITHUB_OUTPUT else echo "color=blue" >> $GITHUB_OUTPUT fi
3. Deploy Inactive Color
- name: Deploy ${{ steps.active.outputs.color }} run: | helm upgrade --install myapp-${{ steps.active.outputs.color }} charts/myapp \ --namespace prod \ --set image.tag=${{ github.sha }} \ --wait --timeout 180s
4. Smoke Test & Swap
- name: Smoke test run: | HOSTNAME=$(kubectl get svc myapp-${{ steps.active.outputs.color }} -n prod \ -o jsonpath='{.status.loadBalancer.ingress.hostname}') STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://$HOSTNAME/health) if [ "$STATUS" != "200" ]; then exit 1; fi - name: Switch service run: | kubectl patch svc myapp -n prod \ -p '{"spec":{"selector":{"app":"myapp-'${{ steps.active.outputs.color }}'"}}}' - name: Cleanup old run: | OLD=$( [ "${{ steps.active.outputs.color }}" = "blue" ] && echo green || echo blue ) helm uninstall myapp-$OLD -n prod
5. Verify Deployment
- 🌐 Visit
myapp
service URL to confirm new version. - 🔄 Inspect Helm releases:
helm list -n prod
.
5. Phase 5: Advanced Secrets & Environment Management
What You’ll Learn
- Integrate HashiCorp Vault and AWS Parameter Store
- Use External Secrets Operator for Kubernetes
- Automate secret freshness checks
5.1 HashiCorp Vault Integration
- name: Vault Login env: VAULT_ADDR: ${{ secrets.VAULT_ADDR }} VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }} run: | vault kv get -field=password secret/data/myapp/db > db_password.txt - name: Use DB Password run: | DB_PASS=$(cat db_password.txt) echo "Connecting with DB_PASS=$DB_PASS"
5.2 AWS Parameter Store
- name: Fetch AWS Parameter run: | aws ssm get-parameter --name "/myapp/db" --with-decryption \ --query "Parameter.Value" --output text > db_pass.txt - name: Use DB Password run: | DB_PASS=$(cat db_pass.txt) echo "Param store DB_PASS=$DB_PASS"
5.3 Kubernetes External Secrets
apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: myapp-db-secret spec: secretStoreRef: name: vault-backend kind: SecretStore target: name: myapp-db data: - secretKey: password remoteRef: key: secret/data/myapp/db property: password
Apply via:
kubectl apply -f external-secret.yaml
5.4 Secret Freshness Check
- name: Check Secret Expiry run: | if [[ -n "${{ secrets.API_TOKEN_EXPIRES }}" ]]; then expires=$(date -d "${{ secrets.API_TOKEN_EXPIRES }}" +%s) now=$(date +%s) days=$(( (expires - now) / 86400 )) if [ $days -lt 7 ]; then echo "Warning: token expires in $days days" fi fi
***
6. Phase 6: Troubleshooting & Debugging Deep Dive
What You’ll Learn
- Enable verbose runner and step logging
- Use tmate for live debugging
- Analyze real error logs and apply fixes
6.1 Verbose Logging
env: ACTIONS_RUNNER_DEBUG: true ACTIONS_STEP_DEBUG: true
6.2 Interactive tmate Session
- name: Setup tmate if: failure() uses: mxschmitt/action-tmate@v3 with: limit-access-to-actor: true github-token: ${{ secrets.GITHUB_TOKEN }}
6.3 Sample Error Log Analysis
npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! myapp@1.0.0 test:unit: `jest --config jest.config.js` npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the myapp@1.0.0 test:unit script.**Fix:** Ensure
jest.config.js
exists, check syntax errors in tests, install missing devDependencies.
***
7. Phase 7: Performance & Cost Analysis
What You’ll Learn
- Select cost-effective runners
- Balance matrix parallelism vs. cost
- Implement artifact retention policies
7.1 Runner Cost Comparison
Runner Type | Price/min | Avg Build Time | Cost/build |
---|---|---|---|
GitHub Ubuntu | $0.008 | 8m | $0.064 |
Self-hosted ARM | $0.005 | 9m | $0.045 |
7.2 Matrix Concurrency Trade-off
With max-parallel: 4
, 12 jobs run in 3 waves. Reducing to 2
increases waves but halves peak concurrency cost.
7.3 Artifact Retention
uses: actions/upload-artifact@v4 with: name: build-artifacts path: dist/ retention-days: 3 # default 90 days
***
8. Phase 8: Extended Case Studies & Diagrams
8.1 E-Commerce Modernization
Architecture Diagram:
- Monorepo with 50 microservices
- Selective builds via
git diff
- Parallel matrix builds
Key Metrics:
Metric | Before | After |
---|---|---|
Avg Build Time | 15m | 5m (↓67%) |
Failure Rate | 23% | 2.5% (↓89%) |
Infra Cost | $2.8M/yr | $0.5M/yr (↓82%) |
8.2 Open Source Automation
Workflow Automations:
- Auto-label first-time contributors
- Mandatory CodeQL scans on PRs
Impact:
Metric | Before | After |
---|---|---|
First-time Merge Rate | 12% | 52% (↑340%) |
Vulnerabilities | 45/mo | 10/mo (↓78%) |
8.3 Financial Compliance Pipeline
Compliance Steps:
- Generate immutable metadata JSON
- Post to internal audit log API
- Verify CloudTrail logs daily
Results:
- 100% audit pass rate over 24 quarters
- Manual effort ↓60%
Section 3 of 4: Custom Actions, Advanced YAML Deep Dive, Bonus Tips
9. Developing a Custom GitHub Action in JavaScript
What You’ll Learn
- Structure of a JavaScript-based GitHub Action
- Using the GitHub Actions toolkit
- Publishing an action to the GitHub Marketplace
Prerequisites
- Node.js and npm installed locally
- Basic JavaScript knowledge
- Familiarity with Git and GitHub repositories
1. Scaffold the Action
mkdir actions/hello-world cd actions/hello-world npm init -y npm install @actions/core @actions/github
2. Write the Action Code
// index.js const core = require('@actions/core'); const github = require('@actions/github'); async function run() { try { const name = core.getInput('name'); console.log(`::notice::Hello, ${name}!`); core.setOutput('greeting', `Hello, ${name}!`); } catch (error) { core.setFailed(error.message); } } run();
3. Define Action Metadata
# action.yml name: 'Hello World Action' description: 'Greets the user by name' inputs: name: description: 'Name to greet' required: true outputs: greeting: description: 'The greeting message' runs: using: 'node16' main: 'index.js'
4. Use Your Action in a Workflow
jobs: demo: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/hello-world with: name: 'Perplexity AI' - run: echo "Greeting: ${{ steps.demo.outputs.greeting }}"
5. Publish to Marketplace
- Create a public GitHub repo for the action.
- Tag a release matching the version in
action.yml
. - Go to the repository’s “Actions” → “New marketplace release.”
***
10. Advanced YAML Deep Dive
10.1 Dynamic Matrix Generation
Generate matrix values at runtime via fromJson
and outputs from previous jobs.
jobs: prepare: runs-on: ubuntu-latest outputs: services: ${{ steps.set.outputs.services }} steps: - id: set run: | services=$(jq -r '.services | join(",")' services.json) echo "::set-output name=services::$services" deploy: needs: prepare strategy: matrix: service: ${{ fromJson(needs.prepare.outputs.services) }} runs-on: ubuntu-latest steps: - run: echo "Deploying ${{ matrix.service }}"
10.2 Conditional Job Execution
Use expressions in if
to skip jobs based on variables or files changed.
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 publish-docs: needs: build runs-on: ubuntu-latest if: contains(join(github.event.head_commit.modified, ''), 'docs/') steps: - run: echo "Publishing docs..."
10.3 Using Anchors for Large Configs
Define a reusable step block and merge with <<: *
.
defaults: &defaults runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 jobs: job1: <<: *defaults steps: - run: echo "Job1" job2: <<: *defaults steps: - run: echo "Job2"
***
11. Bonus Tips & Tricks
- Workflow Visualization: View DAG graph under “Actions” to understand job dependencies.
- Reusable Workflows: Break common tasks into
workflow_call
workflows. - Self-hosted Runners: Tag runners and use
runs-on: [self-hosted, linux]
for dedicated infrastructure. - Security Scanning: Integrate
trivy
oranchore
as pre-deploy steps. - Notifications: Send Slack/Teams alerts via webhook actions on failure or on threshold metrics.
Section 4 of 4: Conclusion, Roadmap Recap, Bonus Case Study & Diagrams
12. Conclusion & Action Plan Recap
Congratulations on building a robust, scalable CI/CD pipeline with GitHub Actions! Here’s your action plan:
- Phase 1: Implement linting workflows with ESLint and Python linters; verify artifacts.
- Phase 2: Configure a test matrix for unit, integration, and E2E tests; upload coverage.
- Phase 3: Optimize builds with multi-stage Dockerfiles and BuildKit caching; manage artifacts.
- Phase 4: Automate deployments using blue/green, Helm, or kubectl; include rollbacks.
- Phase 5: Secure secrets via Vault, Parameter Store, and External Secrets Operator.
- Phase 6: Troubleshoot with verbose logs, tmate sessions, and real log analysis.
- Phase 7: Drive performance and cost efficiency: runner selection, matrix tuning, retention policies.
- Extended Sections: Develop custom GitHub Actions, leverage advanced YAML, and apply best practices.
13. Bonus Case Study: SaaS Multi-Tenant Deployment
Background: SaaS platform with per-tenant isolated environments, on-demand deployments for 100+ tenants.
Challenges
- Provision dedicated namespaces per tenant
- Automate isolated deployments
- Manage secrets and resource quotas per namespace
Solution Outline
- Define a tenant list in JSON in the repo (
tenants.json
). - Generate dynamic matrix jobs to loop through tenants.
- Use Helm with
--create-namespace
and--namespace
flags. - Inject tenant-specific secrets via Kubernetes External Secrets.
- Monitor per-namespace resource usage with Prometheus.
Sample Workflow Snippet
jobs: deploy-tenants: runs-on: ubuntu-latest strategy: matrix: tenant: ${{ fromJson(needs.prepare.outputs.tenants) }} needs: prepare steps: - uses: actions/checkout@v4 - name: Deploy tenant run: | helm upgrade --install myapp-${{ matrix.tenant }} charts/myapp \ --namespace tenant-${{ matrix.tenant }} \ --create-namespace \ --set image.tag=${{ github.sha }}
14. Architecture Diagrams
Embed these diagrams in your blog for visual clarity:
- CI/CD Workflow Diagram: Show each GitHub Actions phase and artifact flow.
- Blue/Green Deployment: Illustrate traffic switch between environments.
- SaaS Multi-Tenant: Depict per-tenant namespaces and dynamic matrices.