Skip to main content
Technology & EngineeringCicd Patterns221 lines

Github Actions

GitHub Actions workflows for CI/CD automation including reusable workflows, matrix builds, and deployment pipelines

Quick Summary26 lines
You are an expert in GitHub Actions for continuous integration and deployment.

## Key Points

- uses: actions/cache@v4
- uses: aws-actions/configure-aws-credentials@v4
- Pin action versions to full commit SHAs for security, not just tags: `uses: actions/checkout@abcdef1234567890`.
- Set minimal `permissions` at the workflow or job level; default to `contents: read`.
- Use `concurrency` groups to prevent redundant workflow runs on rapid pushes.
- Cache dependencies aggressively with `actions/cache` or built-in setup action caching.
- Use GitHub Environments with protection rules (required reviewers, wait timers) for production deploys.
- Keep secrets out of logs; mask values with `::add-mask::`.
- Use `workflow_dispatch` inputs for manual trigger parameters.
- Split large workflows into reusable workflows for maintainability.
- Use `timeout-minutes` on jobs to prevent hung runners from burning minutes.
- Prefer `npm ci` over `npm install` in CI for reproducible builds.

## Quick Example

```yaml
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
```
skilldb get cicd-patterns-skills/Github ActionsFull skill: 221 lines
Paste into your CLAUDE.md or agent config

GitHub Actions — CI/CD

You are an expert in GitHub Actions for continuous integration and deployment.

Overview

GitHub Actions is a CI/CD platform built into GitHub that uses YAML workflow files in .github/workflows/. Workflows are triggered by events (push, pull_request, schedule, workflow_dispatch) and run jobs on GitHub-hosted or self-hosted runners. Each job contains steps that execute shell commands or reference reusable actions.

Setup & Configuration

Workflow files live in .github/workflows/ and must be valid YAML with the .yml or .yaml extension.

Basic workflow structure:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

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

Environment and secrets configuration in repository Settings > Secrets and variables > Actions. Reference secrets with ${{ secrets.SECRET_NAME }}.

Core Patterns

Matrix Builds

Run the same job across multiple configurations:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, windows-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

Reusable Workflows

Define a callable workflow:

# .github/workflows/deploy-reusable.yml
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      deploy-key:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.deploy-key }}

Call it from another workflow:

jobs:
  deploy-staging:
    uses: ./.github/workflows/deploy-reusable.yml
    with:
      environment: staging
    secrets:
      deploy-key: ${{ secrets.STAGING_DEPLOY_KEY }}

Caching Dependencies

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

Concurrency Control

Prevent duplicate runs and enable deployment queuing:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Conditional Jobs and Steps

jobs:
  deploy:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    needs: [build, test]
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying"

Job Outputs

Pass data between jobs:

jobs:
  version:
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.get_tag.outputs.tag }}
    steps:
      - id: get_tag
        run: echo "tag=v1.2.3" >> "$GITHUB_OUTPUT"

  deploy:
    needs: version
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying ${{ needs.version.outputs.tag }}"

OIDC for Cloud Authentication

permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
      aws-region: us-east-1

Core Philosophy

GitHub Actions succeeds because it meets developers where they already are — inside GitHub. The tight integration with pull requests, issues, releases, and the repository itself means CI/CD is not a separate system to configure and maintain but a natural extension of the development workflow. This proximity is also its greatest risk: the ease of adding workflows can lead to sprawling, duplicated YAML that no one fully understands. Treating workflows as code — with the same standards for modularity, testing, and review — is essential as a repository's automation grows.

Security in GitHub Actions requires explicit, intentional configuration because the defaults are permissive. The GITHUB_TOKEN can have write access to your repository, actions from the marketplace run arbitrary code in your CI environment, and secrets are available to any workflow triggered by a push. The principle of least privilege must be applied at every layer: set minimal permissions on each job, pin actions to commit SHAs (not mutable tags), scope secrets to environments with protection rules, and use OIDC instead of long-lived cloud credentials. Every default you do not override is a security decision you made implicitly.

Reusable workflows and composite actions are how GitHub Actions scales beyond a single repository. When ten repositories need the same deploy pipeline, copying the workflow YAML into each one creates a maintenance nightmare where security patches and improvements must be applied ten times. Extracting shared logic into reusable workflows (called with workflow_call) or composite actions (published in a shared repository) centralizes the logic so updates propagate automatically. The investment in building these abstractions pays off the moment the second repository needs the same pipeline.

Anti-Patterns

  • Write-all permissions by default. Not setting permissions on workflows means jobs may run with overly broad token scopes. Always declare the minimum permissions needed at the workflow or job level, starting with contents: read and adding only what is required.

  • Tag-pinned marketplace actions. Using actions/checkout@v4 instead of a full commit SHA means a compromised or force-pushed tag could inject malicious code into your pipeline. Pin to the immutable commit SHA for any action that touches secrets or deploys to production.

  • Duplicated workflow YAML across repositories. Copy-pasting the same CI workflow into every repository means a bug fix or security update must be applied N times. Extract shared logic into reusable workflows or composite actions in a central repository.

  • Secrets in fork-triggered workflows. Pull request workflows from forks do not have access to repository secrets. Designing a workflow that silently skips critical steps when secrets are missing (rather than failing explicitly) produces false confidence in fork PRs.

  • No concurrency control on busy branches. Without concurrency groups and cancel-in-progress, rapid pushes to a branch queue multiple redundant workflow runs that waste compute and delay results. Set concurrency groups on every workflow triggered by push or pull_request events.

Best Practices

  • Pin action versions to full commit SHAs for security, not just tags: uses: actions/checkout@abcdef1234567890.
  • Set minimal permissions at the workflow or job level; default to contents: read.
  • Use concurrency groups to prevent redundant workflow runs on rapid pushes.
  • Cache dependencies aggressively with actions/cache or built-in setup action caching.
  • Use GitHub Environments with protection rules (required reviewers, wait timers) for production deploys.
  • Keep secrets out of logs; mask values with ::add-mask::.
  • Use workflow_dispatch inputs for manual trigger parameters.
  • Split large workflows into reusable workflows for maintainability.
  • Use timeout-minutes on jobs to prevent hung runners from burning minutes.
  • Prefer npm ci over npm install in CI for reproducible builds.

Common Pitfalls

  • Forgetting permissions leads to overly broad default write-all tokens in public repos (or restricted tokens in newer repos).
  • Using actions/checkout without fetch-depth: 0 when you need full git history (e.g., for changelog generation).
  • Matrix builds with fail-fast: true (the default) cancel sibling jobs on first failure, hiding additional errors.
  • Secrets are not available in pull requests from forks—workflows relying on secrets will fail silently.
  • GITHUB_TOKEN permissions differ between push and pull_request events.
  • Caching the wrong paths or using unstable cache keys leads to cache misses and slow builds.
  • Not using concurrency.cancel-in-progress causes queued runs to pile up on busy repos.
  • Assuming environment variables set in one step are available in the next without using $GITHUB_ENV.

Install this skill directly: skilldb add cicd-patterns-skills

Get CLI access →