Github Action Example Pipeline

1. Git & GitHub Fundamentals

GitHub Actions relies on Git object mechanics. Workflows are declared as code and committed directly to your repository inside the .github/workflows/ directory. GitHub observes changes to Git references (refs/heads/<branch>, refs/tags/<tag>, refs/pull/<number>/merge) to evaluate trigger rules.

How Git Mechanics Map to Actions

  • Commits & Pushes: Every git push transmits a SHA hash and branch ref that GitHub Actions uses to check out the exact state of your code.
  • Pull Requests: A PR trigger creates a virtual merge commit (refs/pull/PR_NUMBER/merge) combining your source branch with the target branch to test collision-free execution.
  • Tags & Releases: Semantic version tags (v1.0.0) trigger build, package, and publish workflows.

Bash

# Developer Workflow triggering CI
git checkout -b feature/user-auth
git commit -m "feat: implement JWT login"
git push origin feature/user-auth # Triggers push / pull_request events on GitHub

2. YAML Syntax

Workflows are written in YAML (YAML Ain’t Markup Language). YAML uses strictly spaces for indentation (tabs are forbidden) and relies on key-value pairs, scalar types, arrays, and dictionaries.

Key Syntax Rules for GitHub Actions

  • Indentation: standard is 2 spaces per level.
  • Lists/Sequences: Prefixed with a hyphen and a space (- item).
  • Multi-line Strings: Use | to preserve line breaks or > to fold line breaks into spaces.
  • Booleans & Numbers: true, false, 10, 3.14 (unquoted). Quoting forces string representation ("true").

YAML

# Demonstration of YAML data structures
string_scalar: "Hello World"
number_scalar: 42
boolean_scalar: true

# List / Array syntax
browser_list:
  - chromium
  - firefox
  - webkit

# Dictionary / Object syntax
runner_config:
  os: ubuntu-latest
  timeout: 30

# Multi-line script execution block (| preserves newlines)
multi_line_command: |
  echo "Line 1"
  echo "Line 2"
  npm test

3. Creating Your First Workflow

A minimal workflow file must be stored in .github/workflows/main.yml. It requires three root keys: name, on, and jobs.

YAML

# .github/workflows/first-workflow.yml
name: First Workflow

# Event trigger
on:
  push:
    branches: [ "main" ]

# Execution unit
jobs:
  welcome-job:
    runs-on: ubuntu-latest
    steps:
      - name: Print welcome message
        run: echo "GitHub Actions workflow executed successfully!"

      - name: Output commit metadata
        run: |
          echo "Commit SHA: ${{ github.sha }}"
          echo "Triggered by: ${{ github.actor }}"

4. Events and Triggers (on)

The on key determines when a workflow runs. You can listen to single events, array events, or apply fine-grained activity filters and path filters.

Filtering Rules

  • branches: Restrict runs to specific target branches.
  • paths / paths-ignore: Trigger only when specific files change.
  • tags: Trigger on Git tags matching glob patterns.

YAML

name: Event Trigger Masterclass

on:
  # Trigger on pushes to main or release branches, but only if code inside src/ changed
  push:
    branches:
      - main
      - 'releases/v*'
    paths:
      - 'src/**'
      - 'package.json'
    paths-ignore:
      - '**.md'

  # Trigger on Pull Requests targeting main branch when opened or synchronized
  pull_request:
    types: [opened, synchronize, reopened]
    branches:
      - main

  # Trigger on tag creation matching v1.0.0, v2.1.0, etc.
  push:
    tags:
      - 'v*.*.*'

5. Jobs and Steps

  • Job: A set of steps running on a specific host environment (runs-on). Jobs run in parallel by default.
  • Step: An individual task executed sequentially inside a job. Steps share a filesystem and environment state on the runner VM.

YAML

name: Job Dependency Pipeline

on: push

jobs:
  compile:
    runs-on: ubuntu-latest
    steps:
      - name: Step 1 - Build
        run: echo "Compiling assets..."

  test:
    needs: compile # Forces 'test' to wait until 'compile' finishes successfully
    runs-on: ubuntu-latest
    steps:
      - name: Step 1 - Unit Test
        run: echo "Executing unit tests..."

  deploy:
    needs: [compile, test] # Waits for both upstream jobs
    runs-on: ubuntu-latest
    steps:
      - name: Step 1 - Deploy
        run: echo "Deploying to production..."

6. Using Marketplace Actions

Instead of writing raw shell scripts for every task, you can import community and official actions from the GitHub Marketplace using the uses: keyword.

Versioning Actions

  • @v4 (Major version tag – receives non-breaking updates).
  • @v4.1.2 (Exact semantic version).
  • @8f4b7f8... (Full commit SHA – most secure, immutable).

YAML

jobs:
  setup-environment:
    runs-on: ubuntu-latest
    steps:
      # Action 1: Clone repository code
      - name: Checkout Repository
        uses: actions/checkout@v4

      # Action 2: Provision Node.js runtime environment
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'

      # Step 3: Native shell execution
      - name: Install dependencies
        run: npm ci

7. Environment Variables and Secrets

GitHub Actions handles environment configuration at the workflow, job, or step scope. Sensitive credentials must be stored as Encrypted Secrets in repository/organization settings.

YAML

name: Environment & Secrets Demo

on: push

# Workflow-level environment variables
env:
  GLOBAL_APP_NAME: "MyEnterpriseApp"
  LOG_LEVEL: "debug"

jobs:
  process-payment:
    runs-on: ubuntu-latest
    # Job-level environment variables
    env:
      JOB_ENV: "payment-processor"

    steps:
      - name: Secure API Call
        # Step-level environment variables
        env:
          PAYMENT_API_KEY: ${{ secrets.PAYMENT_GATEWAY_KEY }} # Injecting secret
          DYNAMIC_VAR: "step-specific-value"
        run: |
          echo "Processing payment for $GLOBAL_APP_NAME"
          # Accessing secret safely via environment variable in shell
          python process.py --key "$PAYMENT_API_KEY"

      - name: Set dynamic environment variable for subsequent steps
        run: |
          echo "BUILD_TIMESTAMP=$(date +'%Y-%m-%d_%H-%M-%S')" >> $GITHUB_ENV

      - name: Read dynamic variable
        run: |
          echo "Timestamp was: $BUILD_TIMESTAMP"

8. Expressions and Conditional Execution

Expressions let you evaluate programmatic conditions and access context objects (github, env, runner, steps, secrets). Use if: directives to skip jobs or steps dynamically.

YAML

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

      - name: Execute on main branch only
        if: github.ref == 'refs/heads/main'
        run: echo "This step runs ONLY on pushes to main."

      - name: Execute step with custom status check functions
        id: test_step
        run: npm test

      # Runs ONLY if the previous step failed
      - name: Report Failure
        if: failure() && steps.test_step.outcome == 'failure'
        run: echo "The test execution step failed!"

      # Runs ALWAYS, regardless of prior step pass/fail state
      - name: Always Cleanup
        if: always()
        run: echo "Cleaning up temporary test artifacts..."

9. Matrix Strategy

A Matrix Strategy generates multiple job runs by defining input variables across OSs, runtimes, or database versions. GitHub Actions expands these configurations into a parallel execution grid.

YAML

jobs:
  matrix-build:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false # Keep running other matrix jobs even if one fails
      max-parallel: 4 # Limit concurrent runners
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20]
        # Include specific additional configuration
        include:
          - os: ubuntu-latest
            node-version: 22
            experimental: true
        # Exclude invalid or unwanted combinations
        exclude:
          - os: windows-latest
            node-version: 18

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: node -v

10. Caching Dependencies

To speed up workflow runs, use actions/cache or built-in caching inside setup actions. Caches persist archive files between workflow runs using a unique cache key (typically hashed from lockfiles).

YAML

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

      # Method A: Built-in setup action caching (Recommended)
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip' # Automatically hashes requirements.txt/Pipfile

      # Method B: Manual Cache Configuration via actions/cache
      - name: Cache Custom Directory
        uses: actions/cache@v4
        with:
          path: ~/.custom_cache_dir
          # Primary lookup key
          key: ${{ runner.os }}-custom-${{ hashFiles('**/lockfile.json') }}
          # Fallback lookup keys if primary key misses
          restore-keys: |
            ${{ runner.os }}-custom-

      - name: Install Dependencies
        run: pip install -r requirements.txt

11. Uploading and Downloading Artifacts

Artifacts allow you to persist output files (build binaries, test reports, coverage HTML) beyond the runner’s ephemeral lifecycle and pass data between independent jobs.

YAML

jobs:
  build-job:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: mkdir dist && echo "Compiled App Code" > dist/app.bin

      # Upload artifacts
      - name: Upload Build Binaries
        uses: actions/upload-artifact@v4
        with:
          name: compiled-app-artifact
          path: dist/
          retention-days: 7

  deploy-job:
    needs: build-job # Must wait for build-job to upload
    runs-on: ubuntu-latest
    steps:
      # Download artifacts generated in upstream job
      - name: Download Build Binaries
        uses: actions/download-artifact@v4
        with:
          name: compiled-app-artifact
          path: downloaded-dist/

      - name: Verify Downloaded Assets
        run: cat downloaded-dist/app.bin

12. Manual and Scheduled Workflows

Workflows can run on cron schedules or be triggered manually via the GitHub UI/API with custom user inputs (workflow_dispatch).

YAML

name: Manual & Scheduled Execution

on:
  # Cron trigger: Runs every Monday at 08:00 UTC
  schedule:
    - cron: '0 8 * * 1'

  # Manual trigger with interactive input forms
  workflow_dispatch:
    inputs:
      target_env:
        description: 'Target Deployment Environment'
        type: choice
        required: true
        options:
          - staging
          - production
      perform_cleanup:
        description: 'Run deep database cleanup?'
        type: boolean
        default: false

jobs:
  execute:
    runs-on: ubuntu-latest
    steps:
      - name: Print Trigger Context
        run: |
          echo "Trigger event: ${{ github.event_name }}"
          echo "Selected Environment: ${{ inputs.target_env }}"
          echo "Perform Cleanup: ${{ inputs.perform_cleanup }}"

13. Reusable Workflows and Composite Actions

Composite Action (.github/actions/setup-stack/action.yml)

Bundles multiple run steps into a single reusable step block.

YAML

name: 'Setup Project Stack'
description: 'Standardized setup steps for project'
runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: '20'
    - run: npm ci
      shell: bash # Shell directive is mandatory for composite actions

Reusable Workflow (.github/workflows/reusable-ci.yml)

Encapsulates complete jobs for consumption across callers.

YAML

name: Reusable CI Core
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'
jobs:
  run-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm test

Calling Workflow (.github/workflows/caller.yml)

YAML

jobs:
  invoke-reusable:
    uses: ./.github/workflows/reusable-ci.yml
    with:
      node-version: '22'

14. Docker Integration

GitHub Actions runners come pre-configured with Docker, enabling step containers, containerized jobs, service containers (databases/queues), and image publishing.

YAML

jobs:
  # Integration testing using a Database Service Container
  database-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: mysecretpassword
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        # Health checks to ensure DB is ready before test steps start
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - name: Verify PostgreSQL Connection
        run: |
          PGPASSWORD=mysecretpassword psql -h localhost -U postgres -d testdb -c "SELECT 1;"

  # Build & Publish Docker Container Image
  docker-build-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Log in to GitHub Container Registry (GHCR)
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}/app:latest

15. Cloud Deployments (AWS/Azure/GCP)

To deploy safely to modern cloud infrastructure, never use long-lived access key secrets. Use OpenID Connect (OIDC) to request temporary, short-lived tokens using IAM Role assumption.

YAML

name: Keyless OIDC AWS Deployment

on:
  push:
    branches: [ "main" ]

jobs:
  deploy-aws:
    runs-on: ubuntu-latest
    # Required permission to request the OIDC JWT token from GitHub
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Authenticate with AWS via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeploymentRole
          aws-region: us-east-1

      - name: Deploy to S3 Static Bucket
        run: |
          aws s3 sync ./dist s3://my-production-app-bucket --delete

16. Enterprise CI/CD Patterns and Security

Production enterprise workflows require strict security practices: Zero Trust permissions, Pinned Action Hashes, Concurrency Controls, and Protected Environments.

YAML

name: Enterprise Secure Pipeline

on:
  push:
    branches: [ "main" ]

# Prevent race conditions by cancelling in-progress runs on same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

# Hardened Security Rule 1: Set Least-Privilege permissions globally
permissions: {}

jobs:
  secure-build:
    runs-on: ubuntu-latest
    permissions:
      contents: read # Read-only code access
      id-token: write # For OIDC deployment step

    # Attach workflow to protected GitHub Environment (Approval rules & secrets)
    environment:
      name: production
      url: https://my-app.company.com

    steps:
      # Hardened Security Rule 2: Pin Actions to full immutable commit SHAs
      - name: Checkout Code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Setup Node.js
        uses: actions/setup-node@39370e3970a6d050c080011e813206d11393d18e # v4.1.0
        with:
          node-version: '20'

      - name: Run Dependency Vulnerability Audit
        run: npm audit --audit-level=high

      - name: Build Application
        run: npm run build

Leave a Comment