Back to Software Engineering & Delivery Mastery Series GitHub Actions Bootcamp

Module 11: Monitoring, Optimization, and Migration

June 2, 2026 Wasil Zafar 25 min read

Advanced GitHub Actions topics — workflow monitoring and debugging, performance optimization, cost management, integrating popular tools (Slack, Docker Hub, AWS, Terraform), and migrating from Jenkins, GitLab CI, and CircleCI.

Table of Contents

  1. Monitoring & Debugging
  2. Performance Optimization
  3. Cost Management
  4. Integrating Popular Tools
  5. Migrating from Other Platforms
  6. Exercises
  7. Bootcamp Complete

Monitoring and Debugging GitHub Actions

When workflows fail in production, you need to diagnose problems quickly. GitHub Actions provides several layers of observability — from basic run logs to interactive SSH debugging sessions. Mastering these tools separates teams that fix issues in minutes from those that spend hours staring at cryptic error messages.

Workflow Run Logs

Every workflow run produces structured logs organized by job and step. Each step's output is collapsible, timestamped, and searchable. Key navigation techniques:

  • Log grouping — Use ::group::Title and ::endgroup:: to organize verbose output into collapsible sections
  • Annotations::warning file=app.js,line=1::Missing semicolon creates clickable file annotations
  • Error masking::add-mask::SECRET_VALUE redacts sensitive data from logs
  • Log download — Full logs are available as downloadable archives for 90 days
# Adding structured log output to your workflow steps
name: Build with Grouped Logs
on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: |
          echo "::group::Installing npm packages"
          npm ci
          echo "::endgroup::"

          echo "::group::Checking installed versions"
          node --version
          npm --version
          echo "::endgroup::"

      - name: Run tests
        run: |
          npm test 2>&1 | while IFS= read -r line; do
            if echo "$line" | grep -q "FAIL"; then
              echo "::error::$line"
            else
              echo "$line"
            fi
          done

Debug Logging and SSH Debugging

When standard logs aren't enough, GitHub Actions offers two escalation paths: verbose debug logging and interactive SSH sessions.

Debug Logging — Set the repository secret ACTIONS_STEP_DEBUG to true to enable step-level debug output. This reveals internal action execution details, environment variable resolution, and path operations that are normally hidden:

# Enable debug logging per-run via workflow_dispatch
name: Debug Build
on:
  workflow_dispatch:
    inputs:
      debug_enabled:
        description: 'Enable debug logging'
        required: false
        default: 'false'
        type: boolean

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      ACTIONS_STEP_DEBUG: ${{ inputs.debug_enabled }}
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: |
          echo "::debug::Starting build at $(date)"
          make build
          echo "::debug::Build completed with exit code $?"

SSH Debugging with tmate — For truly puzzling failures, you can SSH into the runner mid-workflow. The mxschmitt/action-tmate action pauses execution and provides an SSH connection string:

# Interactive SSH debugging session
name: Debug via SSH
on: workflow_dispatch

jobs:
  debug:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup environment
        run: npm ci

      - name: Start SSH session
        uses: mxschmitt/action-tmate@v3
        if: ${{ github.event_name == 'workflow_dispatch' }}
        with:
          limit-access-to-actor: true  # Only workflow triggerer can connect
          detached: true               # Continue workflow after timeout
Security Note: Always set limit-access-to-actor: true on tmate sessions and never use SSH debugging on workflows triggered by pull requests from forks. The runner has access to your repository secrets during the session.

Workflow Status Badges and GitHub CLI

Status Badges — Embed workflow status in your README to provide instant visibility:

# Badge URL format
# https://github.com/OWNER/REPO/actions/workflows/WORKFLOW_FILE/badge.svg

# Markdown for README.md
![CI](https://github.com/octocat/my-app/actions/workflows/ci.yml/badge.svg)
![Deploy](https://github.com/octocat/my-app/actions/workflows/deploy.yml/badge.svg?branch=main)

GitHub CLI for Run Inspection — The gh CLI provides powerful commands for workflow management:

# List recent workflow runs
gh run list --limit 10

# View a specific run's details
gh run view 12345678

# Watch a run in real-time
gh run watch 12345678

# Download run logs
gh run download 12345678

# Re-run failed jobs only
gh run rerun 12345678 --failed

# List workflows and their status
gh workflow list

# Trigger a workflow manually
gh workflow run deploy.yml -f environment=staging

# View run usage (billable time)
gh api repos/OWNER/REPO/actions/runs/12345678/timing

Workflow Best Practices and Performance Optimization

Slow workflows waste developer time and consume billable minutes. The difference between a 2-minute and 15-minute CI pipeline compounds across hundreds of daily pushes. Here are the highest-impact optimization techniques.

Minimizing Checkout Depth and Job Parallelism

Shallow clones — Most workflows don't need full git history. Use fetch-depth: 1 (or a small number) to dramatically reduce clone time on large repositories:

# Optimized checkout for CI builds
name: Fast CI
on: push

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1  # Shallow clone — fastest

      # If you need tags for versioning:
      - name: Fetch tags only
        run: git fetch --tags --no-recurse-submodules

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history — only when needed (e.g., changelog generation)

Job Parallelism — Split independent tasks into parallel jobs. Each job gets its own runner, so they execute simultaneously:

# Parallel job execution — all 3 jobs start simultaneously
name: Parallel CI
on: push

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint

  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test -- --shard=1/2

  integration-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:integration

  # Deploy only after all parallel jobs pass
  deploy:
    needs: [lint, unit-tests, integration-tests]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - run: echo "All checks passed — deploying"

Reusable Workflows and Composite Actions

Reusable Workflows — Extract common workflow patterns into callable templates. Teams call them with uses: just like actions:

# .github/workflows/reusable-node-ci.yml (the reusable workflow)
name: Reusable Node.js CI
on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20'
      working-directory:
        required: false
        type: string
        default: '.'
    secrets:
      NPM_TOKEN:
        required: false

jobs:
  ci:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ${{ inputs.working-directory }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
          cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
      - run: npm ci
      - run: npm test
      - run: npm run build
# Calling the reusable workflow from another repo
name: My App CI
on: push

jobs:
  ci:
    uses: my-org/.github/.github/workflows/reusable-node-ci.yml@main
    with:
      node-version: '22'
      working-directory: './frontend'
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Composite Actions — Bundle multiple steps into a single reusable action for shared setup patterns:

# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Install dependencies and configure environment'
inputs:
  node-version:
    description: 'Node.js version'
    default: '20'
runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
    - run: npm ci
      shell: bash
    - run: npx playwright install --with-deps
      shell: bash

Self-Hosted Runners for Heavy Workloads

For builds requiring GPU access, large memory, specific hardware, or reduced network latency, self-hosted runners provide dedicated infrastructure:

  • No minute billing — you pay only for the infrastructure itself
  • Persistent caches — tools and dependencies stay installed between runs
  • Custom hardware — GPUs, ARM processors, high-memory machines
  • Network access — reach internal services without VPN tunnels
Performance Checklist: (1) Use fetch-depth: 1 unless you need history. (2) Cache dependencies aggressively (actions/cache). (3) Run independent jobs in parallel. (4) Use matrix strategies to test multiple versions simultaneously. (5) Skip unnecessary steps with path filters. (6) Use larger runners (4-core) for CPU-bound builds.

Cost Management and Usage Limits

GitHub Actions is free for public repositories with generous limits, but private repository usage can become expensive quickly — especially with macOS runners or large teams. Understanding the pricing model is essential for budget planning.

Free Tier and Pricing Breakdown

Plan Included Minutes/Month Storage Concurrent Jobs
Free 2,000 (Linux) 500 MB 20
Team 3,000 (Linux) 2 GB 40
Enterprise 50,000 (Linux) 50 GB 500
Public repos Unlimited (free) Unlimited 20

Minute multipliers by OS — GitHub charges different rates depending on the runner OS:

Runner OS Minute Multiplier Cost per Minute (overage) Effective Rate
Linux $0.008 $0.48/hour
Windows $0.016 $0.96/hour
macOS 10× $0.08 $4.80/hour
Linux (4-core) $0.016 $0.96/hour
Linux (8-core) $0.032 $1.92/hour

Strategies to Reduce Spend

Cost Optimization Tips:
  • Path filters — Only trigger workflows when relevant files change (paths: filter)
  • Concurrency groups — Cancel redundant runs when new commits push (concurrency: cancel-in-progress: true)
  • Timeout limits — Set timeout-minutes on jobs to prevent runaway builds
  • macOS avoidance — Run iOS builds only on tagged releases, not every push
  • Cache everything — Dependencies, Docker layers, build outputs
  • Self-hosted for bulk — Move high-volume builds to self-hosted runners
  • Scheduled cleanup — Delete old workflow run logs and artifacts to reduce storage
# Cost-optimized workflow with concurrency and timeouts
name: Efficient CI
on:
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - 'tests/**'
      - 'package.json'
      - 'package-lock.json'
  pull_request:
    paths:
      - 'src/**'
      - 'tests/**'

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true  # Cancel previous runs on same branch

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15  # Kill if stuck longer than 15 minutes
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'  # Built-in dependency caching

      - run: npm ci
      - run: npm test

  # macOS builds only on release tags
  ios-build:
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: macos-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - run: xcodebuild -scheme MyApp -sdk iphonesimulator build

Integrating with Popular Tools

GitHub Actions shines when connected to the broader DevOps ecosystem. Here are production-ready integration patterns for the most commonly requested tools.

Slack Notifications

Send deployment notifications, build failures, and PR updates directly to Slack channels using the official slackapi/slack-github-action:

# Slack notification on deployment success/failure
name: Deploy and Notify
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        id: deploy
        run: ./deploy.sh

      - name: Notify Slack - Success
        if: success()
        uses: slackapi/slack-github-action@v2.0.0
        with:
          webhook-type: incoming-webhook
          webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": ":white_check_mark: *Deploy Succeeded*\n*Repo:* ${{ github.repository }}\n*Branch:* `${{ github.ref_name }}`\n*Commit:* `${{ github.sha }}` by ${{ github.actor }}"
                  }
                }
              ]
            }

      - name: Notify Slack - Failure
        if: failure()
        uses: slackapi/slack-github-action@v2.0.0
        with:
          webhook-type: incoming-webhook
          webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": ":x: *Deploy FAILED*\n*Repo:* ${{ github.repository }}\n*Branch:* `${{ github.ref_name }}`\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Logs>"
                  }
                }
              ]
            }

Docker Hub Build and Push

# Build and push Docker image to Docker Hub
name: Docker Build & Push
on:
  push:
    tags: ['v*']

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: myorg/myapp
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

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

AWS and Terraform

# AWS deployment with OIDC authentication (no long-lived credentials)
name: Deploy to AWS
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS Credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1

      - name: Deploy to S3
        run: aws s3 sync ./dist s3://my-bucket --delete

      - name: Invalidate CloudFront
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \
            --paths "/*"
# Terraform plan and apply workflow
name: Terraform
on:
  push:
    branches: [main]
    paths: ['infra/**']
  pull_request:
    paths: ['infra/**']

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  terraform:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./infra
    steps:
      - uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.0

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/TerraformRole
          aws-region: us-east-1

      - name: Terraform Init
        run: terraform init

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan
        continue-on-error: true

      - name: Comment Plan on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const plan = `${{ steps.plan.outputs.stdout }}`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `#### Terraform Plan 📖\n\`\`\`\n${plan.substring(0, 65000)}\n\`\`\``
            });

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve tfplan

SonarCloud, Datadog, and New Relic

# SonarCloud code quality analysis
name: SonarCloud Analysis
on:
  push:
    branches: [main]
  pull_request:

jobs:
  sonarcloud:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history needed for blame data

      - name: SonarCloud Scan
        uses: SonarSource/sonarcloud-github-action@v3
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        with:
          args: >
            -Dsonar.organization=my-org
            -Dsonar.projectKey=my-project
            -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
# Datadog CI visibility — send test results and pipeline metrics
name: CI with Datadog
on: push

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci

      - name: Run tests with Datadog tracing
        env:
          DD_API_KEY: ${{ secrets.DD_API_KEY }}
          DD_ENV: ci
          DD_SERVICE: my-app
          NODE_OPTIONS: '-r dd-trace/ci/init'
        run: npm test

      - name: Upload test results to Datadog
        if: always()
        uses: datadog/junit-upload-github-action@v2
        with:
          api-key: ${{ secrets.DD_API_KEY }}
          service: my-app
          files: junit-results.xml

Migrating from Other CI/CD Platforms

Moving to GitHub Actions from an established CI/CD platform requires understanding how concepts map between systems. Every platform has workflows, jobs, and steps — but they use different terminology, file formats, and capabilities.

CI/CD Platform Concept Mapping
                                flowchart LR
                                    subgraph Jenkins
                                        J1[Jenkinsfile]
                                        J2[Pipeline/Stage]
                                        J3[Step/sh]
                                        J4[Agent/Node]
                                        J5[Shared Library]
                                    end
                                    subgraph GitLab CI
                                        G1[.gitlab-ci.yml]
                                        G2[Stage/Job]
                                        G3[Script]
                                        G4[Runner/Tags]
                                        G5[Include/Extend]
                                    end
                                    subgraph CircleCI
                                        C1[.circleci/config.yml]
                                        C2[Workflow/Job]
                                        C3[Step/run]
                                        C4[Executor]
                                        C5[Orb]
                                    end
                                    subgraph GitHub Actions
                                        A1[.github/workflows/*.yml]
                                        A2[Job/Step]
                                        A3[run/uses]
                                        A4[Runner]
                                        A5[Reusable Workflow/Action]
                                    end
                                    J1 --> A1
                                    G1 --> A1
                                    C1 --> A1
                            

Platform Concept Comparison Table

Concept Jenkins GitLab CI CircleCI GitHub Actions
Config file Jenkinsfile .gitlab-ci.yml .circleci/config.yml .github/workflows/*.yml
Pipeline Pipeline Pipeline Workflow Workflow
Stage/Group Stage Stage Job Job
Execution unit Step Script block Step Step
Compute Agent/Node Runner (tagged) Executor Runner (labeled)
Reusability Shared Library include/extends Orbs Actions/Reusable workflows
Secrets Credentials plugin CI/CD Variables Contexts/Env vars Secrets (repo/org/env)
Caching Plugin-based Built-in (cache:) Built-in (save/restore) actions/cache
Artifacts archiveArtifacts artifacts: persist_to_workspace actions/upload-artifact
Manual approval input step when: manual requires approval Environments + reviewers
Migration Decision Factors:
  • Source code host — If you're already on GitHub, Actions eliminates integration complexity
  • Plugin ecosystem — Jenkins has 1800+ plugins; map critical ones to Actions equivalents before migrating
  • Runner requirements — Self-hosted Jenkins agents translate directly to self-hosted GitHub runners
  • Compliance needs — Audit trails, RBAC, and environment protections differ between platforms
  • Migration approach — Run both systems in parallel during transition; migrate team by team

Migrating from Jenkins

Jenkins pipelines (Declarative or Scripted) map to GitHub Actions workflows with some structural differences. Here's a side-by-side comparison:

Before — Jenkinsfile (Declarative Pipeline):

# Jenkinsfile
pipeline {
    agent any
    
    environment {
        NODE_VERSION = '20'
        DEPLOY_ENV = 'production'
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }
        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'npm run test:unit'
                    }
                }
                stage('Integration Tests') {
                    steps {
                        sh 'npm run test:integration'
                    }
                }
            }
        }
        stage('Build') {
            steps {
                sh 'npm run build'
            }
        }
        stage('Deploy') {
            when {
                branch 'main'
            }
            input {
                message 'Deploy to production?'
            }
            steps {
                sh './deploy.sh'
            }
        }
    }
    
    post {
        failure {
            slackSend channel: '#builds', message: "FAILED: ${env.JOB_NAME}"
        }
    }
}

After — GitHub Actions Workflow:

# .github/workflows/ci-cd.yml (equivalent to the Jenkinsfile above)
name: CI/CD Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  NODE_VERSION: '20'
  DEPLOY_ENV: production

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run test:unit

  integration-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run test:integration

  build:
    needs: [unit-tests, integration-tests]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment:
      name: production  # Requires manual approval via environment protection rules
      url: https://myapp.com
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - run: ./deploy.sh

      - name: Notify Slack on failure
        if: failure()
        uses: slackapi/slack-github-action@v2.0.0
        with:
          webhook-type: incoming-webhook
          webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {"text": "FAILED: ${{ github.workflow }} - ${{ github.ref_name }}"}

Migrating from GitLab CI

GitLab CI uses a single .gitlab-ci.yml with stages that run sequentially. Key differences:

GitLab CI Feature GitHub Actions Equivalent
stages: jobs: with needs: for ordering
image: node:20 container: image: node:20 or setup-node action
cache: paths: actions/cache@v4 or built-in setup action caching
artifacts: paths: actions/upload-artifact@v4
include: remote: Reusable workflows (uses: org/repo/.github/workflows/x.yml)
extends: .template Composite actions or reusable workflows
rules: - if: Job-level if: conditions
when: manual workflow_dispatch or environment protection rules
# GitLab CI (.gitlab-ci.yml) — BEFORE
stages:
  - test
  - build
  - deploy

variables:
  NODE_VERSION: "20"

test:
  stage: test
  image: node:20
  cache:
    paths:
      - node_modules/
  script:
    - npm ci
    - npm test
  only:
    - merge_requests
    - main
# GitHub Actions equivalent — AFTER
name: CI
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    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 test

Migrating from CircleCI

CircleCI's config structure is closest to GitHub Actions. The main differences are in naming and how reusable components work (Orbs vs Actions):

# CircleCI (.circleci/config.yml) — BEFORE
version: 2.1
orbs:
  node: circleci/node@5.2

workflows:
  build-and-test:
    jobs:
      - node/test:
          version: '20'
      - build:
          requires:
            - node/test
      - deploy:
          requires:
            - build
          filters:
            branches:
              only: main

jobs:
  build:
    docker:
      - image: cimg/node:20.0
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-deps-{{ checksum "package-lock.json" }}
      - run: npm ci
      - save_cache:
          paths:
            - node_modules
          key: v1-deps-{{ checksum "package-lock.json" }}
      - run: npm run build
      - persist_to_workspace:
          root: .
          paths:
            - dist/

  deploy:
    docker:
      - image: cimg/base:stable
    steps:
      - attach_workspace:
          at: .
      - run: ./deploy.sh
# GitHub Actions equivalent — AFTER
name: Build and Test
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    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 test

  build:
    needs: test
    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 build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - run: ./deploy.sh

Exercises

Exercise 1 Monitoring & Debugging
Build a Self-Diagnosing Workflow

Create a workflow that: (1) runs your test suite, (2) on failure, automatically enables debug logging and re-runs, (3) posts the debug logs to a Slack channel, and (4) opens a tmate SSH session if the second run also fails. Use workflow_dispatch inputs so developers can manually trigger with debug mode enabled.

tmate debug logging Slack conditional re-run
Exercise 2 Performance Optimization
Optimize a Slow Monorepo Pipeline

Given a monorepo with frontend/, backend/, and infra/ directories: (1) Create path-filtered workflows that only build affected services. (2) Implement aggressive caching (npm, Docker layers, Terraform providers). (3) Add concurrency groups to cancel stale runs. (4) Add timing annotations to identify the slowest steps. Target: reduce a 20-minute pipeline to under 5 minutes for single-service changes.

path filters caching concurrency monorepo
Exercise 3 Tool Integration
Full DevOps Integration Pipeline

Build a production-grade workflow that: (1) Runs SonarCloud analysis on every PR. (2) Builds and pushes a Docker image to Docker Hub on merge to main. (3) Deploys infrastructure with Terraform (plan on PR, apply on main). (4) Configures AWS credentials via OIDC. (5) Sends Slack notifications with deployment status and links. All secrets must use environment-scoped secrets with protection rules.

SonarCloud Docker Hub Terraform AWS OIDC Slack
Exercise 4 Migration Project
Migrate a Jenkins Pipeline to GitHub Actions

Take a real or sample Jenkinsfile with: (1) Multiple stages (build, test, scan, deploy). (2) Parallel test execution. (3) Manual approval gates. (4) Post-build notifications. (5) Shared library calls. Convert it completely to GitHub Actions. Document every Jenkins concept and its Actions equivalent. Run both pipelines in parallel for one sprint to validate parity.

Jenkins migration parallel validation concept mapping shared libraries

Bootcamp Complete — All 11 Modules

Congratulations! You've completed the entire GitHub Actions Bootcamp — 11 modules covering everything from first workflow to production-grade CI/CD infrastructure. You now have the knowledge to automate any software delivery pipeline on GitHub.

Here's a summary of everything covered across the bootcamp:

Module Topic Key Skills Gained
1 Introduction to GitHub Actions YAML syntax, first workflow, runners, components
2 Workflow Triggers and Events 35+ event types, filtering, repository dispatch
3 Jobs, Runners, and Execution Job dependencies, matrix builds, containers
4 Actions and the Marketplace Using, creating, and publishing custom actions
5 Secrets, Variables, and Security Secret management, OIDC, security hardening
6 Artifacts, Caching, and Outputs Build artifacts, dependency caching, job outputs
7 CI Workflows Testing, linting, code quality, multi-language CI
8 CD and Deployment Workflows Environments, deployment strategies, rollbacks
9 Advanced Workflow Patterns Reusable workflows, dynamic matrices, fan-out/fan-in
10 Real-World Project End-to-end pipeline for a production application
11 Monitoring, Optimization & Migration Debugging, cost control, integrations, platform migration

Recommended Next Steps

  • Get certified — Pursue the GitHub Actions certification to validate your skills
  • Contribute to Actions — Build and publish your own action to the Marketplace
  • Explore GitHub Advanced Security — CodeQL, secret scanning, and Dependabot integrate deeply with Actions
  • Implement GitOps — Use Actions to drive infrastructure-as-code deployments with ArgoCD or Flux
  • Scale with GitHub Enterprise — Larger organizations benefit from runner groups, audit logs, and GHES