GitHub Actions — Complete Handbook 2025–2026
V1
GitHub Actions
Complete Reference
v5 / v6 Actions 2025–2026 Handbook
complete reference · 2025–2026

GitHub Actions
Complete Handbook

Every surface of GitHub Actions in one place — workflow anatomy, runners, the latest official actions (v5/v6), custom action types, caching, matrix builds, reusable workflows, secrets, OIDC, Dependabot automation, bots, usage limits, 2026 pricing changes, and production-ready CI/CD templates.

Workflow Anatomy Runners v5 / v6 Actions Custom Actions Caching & Artifacts Matrix Builds Dependabot CI / CD Templates
Core Concepts

Workflow Anatomy

A GitHub Actions workflow is a YAML file in .github/workflows/. It defines when to run (triggers), where to run (runners), and what to run (jobs and steps).

yaml — .github/workflows/build.yml (annotated)
name: CI Build                 # Display name in GitHub UI

on:                               # Trigger definition
  push:
    branches: [main, develop]
    paths-ignore: ['**.md']      # skip if only docs changed
  pull_request:
    branches: [main]

concurrency:                      # Cancel redundant runs
  group: ci-${{ github.ref }}
  cancel-in-progress: true

permissions:                      # Least-privilege GITHUB_TOKEN
  contents: read
  pull-requests: write

env:                              # Workflow-level env vars
  NODE_VERSION: '22'

jobs:
  build:
    name: Build & Test
    runs-on: ubuntu-latest       # Runner label
    timeout-minutes: 15          # Fail-safe timeout

    outputs:                      # Pass data between jobs
      version: ${{ steps.ver.outputs.version }}

    steps:
      - name: Checkout
        uses: actions/checkout@v6  # Pin major version

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'             # Built-in dep caching

      - name: Install
        run: npm ci

      - name: Get version
        id: ver
        run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm test

      - name: Upload coverage
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ github.sha }}
          path: coverage/
          retention-days: 14

Key YAML Fields

FieldScopePurpose
nameworkflow / job / stepDisplay name in GitHub UI. Use descriptive names.
onworkflowTrigger(s) — event type, filters, branches, paths
concurrencyworkflow / jobGroup concurrent runs. cancel-in-progress: true kills stale runs.
permissionsworkflow / jobScope GITHUB_TOKEN. Default is write-all — always restrict.
envworkflow / job / stepEnvironment variables; more specific scope wins
runs-onjobRunner label, runner group, or matrix expression
needsjobDeclares job dependencies — controls execution order
outputsjobExpose step outputs for downstream jobs
timeout-minutesjob / stepHard timeout. Default is 360 min. Always set lower.
ifjob / stepConditional expression — skips or runs based on context
usesstepReference an action or reusable workflow
withstepInput parameters for an action
runstepInline shell script (bash by default)
continue-on-errorsteptrue — step failure does not fail the job
strategy.matrixjobDefine a build matrix (OS × language version etc.)
Events

Triggers & Events

TriggerWhen it firesCommon use
pushPush to branch or tagCI on every commit, CD on tag push
pull_requestPR opened / sync / closedCI gating — runs with PR head, limited secrets
pull_request_targetSame but runs with base branch contextDependabot auto-merge, label actions (has write access)
workflow_dispatchManual trigger via UI or APIHotfix deploys, manual release process
workflow_callCalled by another workflowReusable workflow entry point
scheduleCron expression (UTC)Nightly builds, security scans, dependency checks
releaseRelease published / createdPublish to npm, PyPI, container registry
create / deleteBranch or tag created / deletedNotify, clean environments
issuesIssue opened / edited / closedAuto-labelling, project board automation
issue_commentComment created on issue or PRBot commands e.g. /deploy staging
repository_dispatchExternal webhook POST to GitHub APICross-repo triggers, external CI integration
merge_groupMerge Queue entryRequired status checks for Merge Queue
deployment_statusExternal deployment status updatePost-deploy test triggers
registry_packageContainer package publishedDownstream build triggers on image publish
yaml — trigger patterns
# Manual trigger with typed inputs
on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy target'
        required: true
        type: choice
        options: [staging, production]
      dry_run:
        type: boolean
        default: false

# Path-filtered push (skip pure docs changes)
on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'package*.json'
    paths-ignore:
      - 'docs/**'
      - '**.md'

# Tag-based release trigger
on:
  push:
    tags: ['v*.*.*']

# Nightly scheduled job (UTC)
on:
  schedule:
    - cron: '0 2 * * *'         # 02:00 UTC daily
Expressions

Contexts & Expressions

ContextKey properties
githubgithub.sha · github.ref · github.event_name · github.actor · github.repository · github.head_ref
envAll env vars defined at workflow / job / step scope
varsConfiguration variables (non-secret) set at repo / org level
secretssecrets.GITHUB_TOKEN and any custom secrets
stepssteps.<id>.outputs.* · steps.<id>.conclusion
jobsjobs.<job_id>.result (reusable workflows)
needsneeds.<job_id>.outputs.* · needs.<job_id>.result
runnerrunner.os · runner.arch · runner.temp
matrixmatrix.<key> — current cell values in a matrix strategy
yaml — expression patterns
# Conditional step — only on main
if: github.ref == 'refs/heads/main'

# Always run (even if job failed)
if: always()

# Run on failure only
if: failure()

# Check if PR is from Dependabot
if: github.actor == 'dependabot[bot]'

# Ternary-style using fromJSON
env:
  TARGET: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}

# Write to GITHUB_OUTPUT (step outputs)
run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT

# Write to GITHUB_STEP_SUMMARY (shows in UI)
run: echo "## Build passed ✅" >> $GITHUB_STEP_SUMMARY

# Mask a value in logs
run: echo "::add-mask::$SECRET_VALUE"

# Add a warning annotation
run: echo "::warning file=src/main.ts,line=42::Deprecated API"
Compute

GitHub-Hosted Runners

GitHub-hosted runners are ephemeral VMs provisioned fresh for every job. Free for public repos. Private repos consume included minutes then billed. Prices reduced up to 39% from January 2026.

Runner labelOSvCPURAMStorageBilling
ubuntu-latest
ubuntu-24.04
Ubuntu 24.04416 GB14 GB SSD 1× Linux rate
ubuntu-22.04 Ubuntu 22.04416 GB14 GB SSD 1× Linux rate
windows-latest
windows-2025
Windows Server 2025416 GB14 GB SSD
macos-latest
macos-15
macOS 15 (Apple Silicon)37 GB14 GB
macos-13 macOS 13 (Intel)414 GB14 GB
⚠️
Pin runner versions for stability. ubuntu-latest tracks the current LTS. When it moves (e.g. 22.04→24.04), your workflow may break. Pin to ubuntu-24.04 in production workflows and update deliberately.

Larger Runners

Larger runners offer more CPU/RAM and advanced features. Available on GitHub Team and Enterprise Cloud plans.

RunnervCPURAMKey feature
ubuntu-latest-4-cores416 GBLarger baseline
ubuntu-latest-8-cores832 GBHeavy builds
ubuntu-latest-16-cores1664 GBML / large Docker builds
ubuntu-latest-64-cores64256 GBEnterprise-grade parallelism
ARM runners4–6416–256 GBNative ARM builds (no QEMU)
GPU runners4+16+ GBNVIDIA T4/A10G — ML training

Larger runners also support: static IP addresses, VNet injection (Azure), custom machine images, and dedicated allocations.

Self-Hosted Runners

✓ Use self-hosted when
  • You need custom hardware (GPU, ARM, FPGA, high memory)
  • Jobs access internal services or private network resources
  • Build times are very long and cost of hosted runners is high
  • You need to pre-install proprietary software or licenses
  • Compliance requires data to stay on-premises
yaml — self-hosted runner labels
# Single label
runs-on: self-hosted

# Multiple labels (all must match)
runs-on: [self-hosted, linux, gpu]

# Runner group (org-level)
runs-on:
  group: production-runners
  labels: [linux, x64]

# Ephemeral self-hosted (recommended for security)
# Register with --ephemeral flag so runner deregisters after one job
# Use Actions Runner Controller (ARC) for Kubernetes-based autoscaling
🚨
Never use self-hosted runners on public repos. Fork PRs can modify workflow files and exfiltrate environment secrets. Use GitHub-hosted runners for public repositories, or enforce strict fork PR controls (no secrets, restricted permissions).
💡
2026 Pricing Change: From March 2026, self-hosted runners incur a $0.002/minute platform charge for private repos. Public repos remain free. Self-hosted usage now consumes from plan free-minutes quota. GitHub Enterprise Server customers are not affected.
Official Actions

Official Actions Reference (v5/v6)

actions/checkout@v6 is stable. Most actions are now on v4–v6. Use the highest stable major version — minor/patch updates are automatic when pinning to a major tag.

Most-Used Official Actions

ActionLatestKey changes
actions/checkout v6 v6 stable — sparse checkout, multi-repo, filter blobs. v5 added submodule performance improvements.
actions/setup-node v4 Built-in npm/yarn/pnpm cache via cache: input. Supports .nvmrc, .node-version.
actions/setup-python v5 pip/pipenv/poetry cache built-in. python-version-file: .python-version support.
actions/setup-java v4 Maven/Gradle cache. Multiple JDK distributions (temurin, corretto, zulu, liberica).
actions/setup-dotnet v4 Multi-version support. NuGet cache. Works with global.json.
actions/setup-go v5 Module cache built-in. Reads go.mod for version.
actions/cache v4 Cache >10 GB per repo (Nov 2025). save-always: true input. Parallel restore.
actions/upload-artifact v4 v3 deprecated Jan 2025. Non-zipped upload option (Feb 2026). 98% faster than v3.
actions/download-artifact v4 Parallel downloads. pattern: glob matching. merge-multiple: true for matrix.
actions/github-script v7 Run JS with full Octokit API client. TypeScript support.
actions/create-release deprecated Use softprops/action-gh-release@v2 instead — richer features.
docker/build-push-action v6 Buildx / BuildKit. Multi-platform (amd64 + arm64). Cache from/to registry.
docker/login-action v3 Log in to GHCR, Docker Hub, ECR, ACR, GCR, custom registry.
docker/metadata-action v5 Generate image tags/labels from git refs, SemVer, SHA.

actions/checkout@v6 — Key Inputs

yaml — checkout@v6 reference
- uses: actions/checkout@v6
  with:
    ref: ${{ github.event.pull_request.head.sha }}  # pin PR SHA
    fetch-depth: 0          # full history (needed for git log, SemVer)
    sparse-checkout: |      # only checkout specific dirs
      src/
      tests/
    token: ${{ secrets.MY_PAT }}    # for private repo or push-back
    submodules: recursive  # init all submodules
    lfs: true               # checkout Git LFS files

actions/upload-artifact@v4 — Non-Zipped (Feb 2026)

yaml — upload-artifact@v4 / download-artifact@v4
# Upload — v4 default is zipped
- uses: actions/upload-artifact@v4
  with:
    name: build-${{ github.sha }}
    path: |
      dist/
      !dist/**/*.map          # exclude source maps
    retention-days: 30
    if-no-files-found: error # fail if no files match
    compression-level: 0     # 0 = no zip (Feb 2026 feature)

# Download — pattern matches multiple artifacts from matrix
- uses: actions/download-artifact@v4
  with:
    pattern: build-*          # glob across all named artifacts
    path: all-builds/
    merge-multiple: true    # merge into single dir
Extensibility

Custom Actions

Custom actions let you package reusable logic. Three types exist — choose based on dependencies, performance needs, and portability requirements.

🐚
Composite Action

Shell steps + uses: references. No build step. Runs directly on runner. Best for multi-step workflows you want to reuse.

🟨
JavaScript Action

Node.js. Fastest startup. Runs directly on runner (no Docker). Use @actions/core, @actions/github. Best for most custom logic.

🐳
Docker Container Action

Any language inside a container. Consistent environment. Slower startup (image pull). Linux only. Best for heavy dependencies or non-JS runtimes.

Composite Action

yaml — .github/actions/setup-env/action.yml
name: 'Setup Environment'
description: 'Install Node, cache deps, set env vars'

inputs:
  node-version:
    description: 'Node version to use'
    required: false
    default: '22'

outputs:
  cache-hit:
    description: 'Whether the cache was hit'
    value: ${{ steps.cache.outputs.cache-hit }}

runs:
  using: composite
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: npm
      id: cache

    - name: Install dependencies
      run: npm ci
      shell: bash            # required in composite actions

# Usage in a workflow:
# - uses: ./.github/actions/setup-env
#   with:
#     node-version: '22'

JavaScript Action

javascript — src/index.js
const core = require('@actions/core');
const github = require('@actions/github');

async function run() {
  try {
    // Read inputs defined in action.yml
    const token = core.getInput('github-token', { required: true });
    const message = core.getInput('message');

    // GitHub API client
    const octokit = github.getOctokit(token);
    const { context } = github;

    // Comment on the PR
    await octokit.rest.issues.createComment({
      ...context.repo,
      issue_number: context.payload.pull_request.number,
      body: message,
    });

    // Set output
    core.setOutput('commented', 'true');
    core.info('Comment posted successfully');

  } catch (err) {
    core.setFailed(err.message);  // fails the step
  }
}

run();
yaml — action.yml (JavaScript action)
name: 'PR Commenter'
description: 'Post a comment on a pull request'
inputs:
  github-token:
    required: true
  message:
    required: true
outputs:
  commented:
    description: 'Whether comment was posted'
runs:
  using: node20              # node16 / node20 — use node20
  main: dist/index.js        # bundled output (ncc, esbuild)
  post: dist/cleanup.js      # optional: run after job completes
Security

Secrets, Variables & Environments

TypeAccess in workflowScopeUse for
Encrypted secretsecrets.MY_SECRETRepo / Org / EnvAPI keys, passwords, tokens — never logged
Configuration variablevars.MY_VARRepo / Org / EnvNon-sensitive config: URLs, feature flags, version numbers
GITHUB_TOKENsecrets.GITHUB_TOKENPer-job (auto)GitHub API calls — expires after job. Scope with permissions:
Environment secretsecrets.PROD_KEYNamed environmentSecrets that differ per deploy target (staging vs prod)
Dependabot secretsecrets.MY_SECRETDependabot-specificSecrets for Dependabot workflows — separate from Actions secrets

Permissions & OIDC

yaml — OIDC to authenticate with cloud (keyless)
permissions:
  id-token: write             # Required for OIDC
  contents: read

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

      # AWS — no long-lived access keys needed
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/GitHubDeploy
          aws-region: us-east-1

      # Azure — no service principal password needed
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      # GCP — Workload Identity Federation
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/...'
          service_account: 'deploy@myproject.iam.gserviceaccount.com'
Performance

Caching & Artifacts

💡
Nov 2025: Cache storage per repo can now exceed 10 GB. LRU eviction policy — caches not accessed in 7 days are evicted regardless of size. Max per cache entry: 10 GB. Total repo limit configurable by org admins.
yaml — cache patterns
# npm — explicit cache (or use setup-node cache: 'npm')
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |         # Fallback prefix matches
      npm-${{ runner.os }}-
      npm-
    save-always: true       # Save cache even if later steps fail

# pip / Python
- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${{ runner.os }}-${{ hashFiles('**/requirements*.txt') }}

# Gradle / JVM
- uses: actions/cache@v4
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}

# Docker layer cache to registry
- uses: docker/build-push-action@v6
  with:
    cache-from: type=registry,ref=ghcr.io/org/app:cache
    cache-to:   type=registry,ref=ghcr.io/org/app:cache,mode=max
Parallelism

Matrix Strategy

yaml — matrix builds
jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: ['20', '22']
        include:                    # Add extra matrix cells
          - os: ubuntu-latest
            node: '22'
            coverage: true
        exclude:                    # Remove specific combinations
          - os: windows-latest
            node: '20'
      fail-fast: false            # Don't cancel all on first failure
      max-parallel: 4             # Limit concurrent jobs

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci && npm test
      - name: Coverage
        if: matrix.coverage       # only for specific matrix cell
        run: npm run coverage
      - uses: actions/upload-artifact@v4
        with:
          name: results-${{ matrix.os }}-node${{ matrix.node }}
          path: test-results/
DRY Workflows

Reusable Workflows

Reusable workflows let you call a workflow from another workflow — sharing CI logic across repos without copy-pasting YAML. Called via workflow_call trigger.

yaml — .github/workflows/shared-build.yml (callee)
on:
  workflow_call:
    inputs:
      environment:
        type: string
        required: true
    secrets:
      DEPLOY_KEY:
        required: true
    outputs:
      version:
        description: 'Built version'
        value: ${{ jobs.build.outputs.version }}

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.v.outputs.version }}
    steps:
      - uses: actions/checkout@v6
      - id: v
        run: echo "version=1.0.0" >> $GITHUB_OUTPUT
yaml — caller workflow
jobs:
  build:
    uses: my-org/.github/.github/workflows/shared-build.yml@main
    with:
      environment: production
    secrets:
      DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
    # Or pass ALL caller secrets:
    # secrets: inherit

  notify:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Built ${{ needs.build.outputs.version }}"
Flow Control

Concurrency & Job Ordering

yaml — concurrency, needs, outputs
# Cancel in-progress runs for the same branch
concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]

  test:
    needs: lint               # runs after lint
    runs-on: ubuntu-latest
    steps: [...]

  build:
    needs: test
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.tag.outputs.tag }}
    steps: [...]

  deploy-staging:
    needs: build
    environment: staging       # triggers protection rules / approvals
    runs-on: ubuntu-latest
    steps: [...]

  deploy-prod:
    needs: [build, deploy-staging]
    environment: production
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps: [...]
Platform Limits

Usage Limits

Job timeout (max)
6 hrs
Always set lower with timeout-minutes
Workflow run (max)
35 days
Including wait time for approval
Concurrent jobs (Free)
20
5 macOS concurrent
Concurrent jobs (Enterprise)
500
50 macOS concurrent
Secrets per scope
100
Per repo; 100 per org; 100 per env
Secret size (max)
48 KB
Per secret value
Cache per repo
10+ GB
Expanded Nov 2025. LRU eviction (7 days)
Artifact retention
90 days
Default. Min 1 day. Set per-artifact.
Workflows per repo
1000
Per hour per repo (queue limit)
Workflow file size
512 KB
Per .yml file
Matrix size
256
Max jobs per matrix strategy
API requests
1000/hr
Per repo GITHUB_TOKEN rate limit

2026 Billing & Pricing Changes

PlanIncluded minutes/moIncluded storageExtra cost
Free2,000 (Linux)500 MB$0.008/min Linux
Pro3,000 (Linux)1 GB$0.008/min Linux
Team3,000 (Linux)2 GB$0.008/min Linux
Enterprise50,000 (Linux)50 GB$0.008/min Linux
Runner typeRate (from Jan 2026)vs Linux multiplier
Linux (standard)$0.008/min
Windows (standard)$0.016/min
macOS (standard)$0.08/min10×
Self-hosted (from Mar 2026)$0.002/min platform charge— (plus your own compute costs)
Larger Linux (4-core)$0.016/min
Larger Linux (16-core)$0.064/min
ℹ️
GitHub-hosted runner prices were reduced up to 39% on January 1, 2026. Self-hosted runners in private repos now incur a $0.002/min platform charge from March 2026 — but this counts toward your plan's free minutes. Public repos remain entirely free for both hosted and self-hosted.
Dependency Automation

Dependabot — Full Reference

Dependabot is GitHub's native dependency scanner and updater. It creates PRs to update outdated or vulnerable dependencies. Works with 20+ package ecosystems.

dependabot.yml Configuration

yaml — .github/dependabot.yml
version: 2
updates:
  # npm — weekly batch, grouped updates
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Europe/London
    groups:                       # Group related deps into one PR
      dev-dependencies:
        patterns: ['*']
        dependency-type: development
      production-dependencies:
        patterns: ['*']
        dependency-type: production
    open-pull-requests-limit: 5  # cap PR count
    reviewers: [myorg/platform-team]
    labels: [dependencies, automated]
    cooldown:                     # Wait before opening PR (supply chain safety)
      default-days: 3

  # GitHub Actions — pin by SHA, update weekly
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly

  # Docker
  - package-ecosystem: docker
    directory: /
    schedule:
      interval: monthly
    ignore:
      - dependency-name: node
        update-types: [version-update:semver-major]

Dependabot Auto-Merge Workflow

yaml — .github/workflows/dependabot-automerge.yml
name: Dependabot Auto-Merge

on:
  pull_request_target:
    types: [opened, synchronize, reopened]

permissions:
  contents: write
  pull-requests: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: github.actor == 'dependabot[bot]'
    steps:
      # Fetch PR metadata (update-type, dependency-name, etc.)
      - name: Fetch Dependabot metadata
        id: meta
        uses: dependabot/fetch-metadata@v2
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

      # Auto-approve + merge patch updates
      - name: Approve patch updates
        if: steps.meta.outputs.update-type == 'version-update:semver-patch'
        run: |
          gh pr review --approve "$PR_URL"
          gh pr merge --auto --squash "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      # Auto-approve + merge minor updates for dev deps
      - name: Approve minor dev dep updates
        if: |
          steps.meta.outputs.update-type == 'version-update:semver-minor' &&
          steps.meta.outputs.dependency-type == 'direct:development'
        run: |
          gh pr review --approve "$PR_URL"
          gh pr merge --auto --squash "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      # Label major updates for manual review
      - name: Label major updates
        if: steps.meta.outputs.update-type == 'version-update:semver-major'
        run: gh pr edit "$PR_URL" --add-label "needs-review"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
⚠️
Supply chain security warning (2025–2026): Several popular Actions were compromised (tj-actions/changed-files, trivy-action, Axios npm package) — often through auto-merging dependency updates that included malicious SHA changes. Mitigations: use cooldown.default-days: 3, pin Actions to commit SHAs (not tags), do NOT auto-merge major updates, and review Actions update PRs manually.

Dependabot Supported Ecosystems

EcosystemPackage managerDirectory
npmnpm / yarn / pnpmpackage.json location
pippip / pip-compile / pipenv / Poetryrequirements.txt / pyproject.toml
nugetNuGet (.NET)*.csproj / packages.lock.json
mavenMaven / Gradlepom.xml / build.gradle
cargoRust/CargoCargo.toml
gomodGo modulesgo.mod
bundlerRuby/BundlerGemfile
github-actionsGitHub Actions.github/workflows/
dockerDockerfileDockerfile location
terraformTerraform registry*.tf files
helmHelm chartsChart.yaml location
pubDart/Flutterpubspec.yaml
Automation Bots

GitHub Bots & Automation

🤖
Dependabot
GitHub Native
Opens PRs for outdated or vulnerable dependencies
Security alerts with auto-fix PRs
20+ ecosystems: npm, pip, cargo, Go, NuGet, Actions
Configure via .github/dependabot.yml
🔄
Renovate
Third-party
More powerful than Dependabot — custom schedules, grouping, regex versioning
Automerge rules by update type, confidence score, or test results
Dashboard PR showing all pending updates
Configure via renovate.json
🏷️
actions-labeler
Third-party
Auto-labels PRs based on changed file paths
Configure label rules in .github/labeler.yml
Use: actions/labeler@v5
github-actions[bot]
GitHub Native
The identity when workflows use GITHUB_TOKEN to create comments, labels, or commits
Commits appear as github-actions[bot]
Used for auto-generated release notes, changelogs
🔐
CodeQL / Security bot
GitHub Native
Static analysis (SAST) for common vulnerabilities
Runs on push and PR via github/codeql-action
Alerts appear in Security tab, not as PRs
🚀
release-please
Google
Automates release PRs and changelogs from Conventional Commits
Creates versioned release PRs; merging triggers GitHub Release
Use: google-github-actions/release-please-action@v4
💬
stale-bot
Third-party
Marks issues and PRs as stale after inactivity
Comments and closes after further inactivity
Use: actions/stale@v9
📋
PR Size Labeler
Third-party
Labels PRs by diff size (XS/S/M/L/XL)
Encourages small, reviewable changesets
Use: codelytv/pr-size-labeler@v1

When to Use Dependabot vs Renovate

✓ Choose Dependabot when
  • Simple setup — zero additional configuration required
  • You need security alerts and auto-fix PRs natively
  • Team prefers GitHub-native, no external app needed
  • Repo is small, few ecosystems, infrequent updates
✓ Choose Renovate when
  • You need complex grouping, custom versioning rules
  • Monorepo with many packages across ecosystems
  • You want a Dependency Dashboard PR overview
  • Auto-merge based on test results or semantic confidence
  • Pinning Docker/Actions to commit SHAs automatically
CI Sample

CI: Node.js

yaml — node.js CI with matrix, cache, coverage
name: Node.js CI

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

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

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v4
        with: { node-version: '22', cache: npm }
      - run: npm ci --prefer-offline
      - run: npm run lint
      - run: npm run type-check

  test:
    needs: lint
    strategy:
      matrix:
        node: ['20', '22']
      fail-fast: false
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v4
        with: { node-version: '${{ matrix.node }}', cache: npm }
      - run: npm ci --prefer-offline
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-node-${{ matrix.node }}
          path: coverage/
          retention-days: 7
CI Sample

CI: .NET / C#

yaml — .net 8 CI with test reporting
name: .NET CI

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

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

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

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.x'

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build --no-restore --configuration Release

      - name: Test
        run: |
          dotnet test --no-build \
            --configuration Release \
            --logger "trx;LogFileName=results.trx" \
            --collect:"XPlat Code Coverage"

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: '**/*.trx'

      - name: Publish test report
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: .NET Tests
          path: '**/*.trx'
          reporter: dotnet-trx
CI Sample

CI: Docker Build & Push

yaml — multi-platform docker build to ghcr.io
name: Docker Build

on:
  push:
    branches: [main]
    tags: ['v*.*.*']
  pull_request:
    branches: [main]

permissions:
  contents: read
  packages: write              # Required for GHCR push

env:
  REGISTRY: ghcr.io
  IMAGE: ghcr.io/${{ github.repository }}

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

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

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

      - name: Log in to GHCR
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.IMAGE }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=sha-
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to:   type=gha,mode=max
          provenance: false         # Avoid manifest list issues
CD Sample

CD: GitHub Pages

yaml — deploy static site to GitHub Pages
name: Deploy to Pages

on:
  push:
    branches: [main]
  workflow_dispatch:              # Manual deploy button

permissions:
  contents: read
  pages: write
  id-token: write              # Required for Pages deployment

concurrency:
  group: pages
  cancel-in-progress: false   # Never cancel in-flight deploy

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v4
        with: { node-version: '22', cache: npm }
      - run: npm ci && npm run build
      - uses: actions/configure-pages@v5
      - uses: actions/upload-pages-artifact@v3
        with:
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deploy.outputs.page_url }}
    steps:
      - id: deploy
        uses: actions/deploy-pages@v4
CD Sample

CD: Release & Deploy

yaml — tag-triggered release: build → test → release → deploy
name: Release & Deploy

on:
  push:
    tags: ['v*.*.*']

permissions:
  contents: write
  packages: write
  id-token: write

jobs:
  # Build matrix: produce binaries for 3 platforms
  build:
    strategy:
      matrix:
        include:
          - { os: ubuntu-latest,  target: linux-x64 }
          - { os: windows-latest, target: win-x64 }
          - { os: macos-latest,   target: osx-arm64 }
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: '8.x' }
      - run: |
          dotnet publish src/App/App.csproj \
            --runtime ${{ matrix.target }} \
            --configuration Release \
            --self-contained true \
            --output dist/
      - uses: actions/upload-artifact@v4
        with:
          name: app-${{ matrix.target }}
          path: dist/

  # Create GitHub Release with all binaries
  release:
    needs: build
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.t.outputs.tag }}
    steps:
      - uses: actions/download-artifact@v4
        with:
          pattern: app-*
          path: release/
          merge-multiple: true
      - id: t
        run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
      - uses: softprops/action-gh-release@v2
        with:
          name: Release ${{ steps.t.outputs.tag }}
          generate_release_notes: true
          files: release/**

  # Deploy to staging (with environment protection)
  deploy-staging:
    needs: release
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/checkout@v6
      # OIDC auth + deploy script here
      - run: ./scripts/deploy.sh staging ${{ needs.release.outputs.tag }}

  # Deploy to production (requires manual approval in GitHub)
  deploy-prod:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production           # Required reviewers set in GitHub UI
      url: https://example.com
    steps:
      - uses: actions/checkout@v6
      - run: ./scripts/deploy.sh production ${{ needs.release.outputs.tag }}
What to Avoid

Anti-Patterns & Pitfalls

Anti-patternProblemFix
No timeout-minutes Runaway jobs consume 6 hours of minutes; blocks concurrent jobs Set timeout-minutes: 15 (or appropriate) on every job
Pinning to floating tags uses: actions/something@v3 may be updated maliciously or break without notice Pin third-party to SHA: uses: org/action@a1b2c3d. Use Dependabot to update.
Secrets in run commands Secrets printed in logs, leaked in error output Use echo "::add-mask::$SECRET"; never interpolate secrets in run strings
Hardcoded permissions write-all Default GITHUB_TOKEN has write access to all scopes — excessive privilege Set permissions: {} at top level and add only what's needed per job
Self-hosted on public repos Fork PRs can modify workflows to exfiltrate runner secrets or environment Use GitHub-hosted runners for all public repos, or restrict with pull_request_target carefully
No concurrency groups Multiple PRs or fast pushes create redundant parallel runs wasting minutes Add concurrency: { group: ..., cancel-in-progress: true }
No cache Every job reinstalls all dependencies from scratch — slow, expensive Use setup-node cache: npm or actions/cache@v4 with lockfile hash key
Auto-merge all Dependabot PRs Malicious package updates merged without review (supply chain attack) Only auto-merge patch; use cooldown; manually review Actions SHA updates
Using workflow_run for auth Complex trust chain — easier to misconfigure than pull_request_target Prefer explicit actor checks and environment protection rules
Large monolithic workflows One failure resets entire 20-minute workflow; no parallelism Split into lint/test/build/deploy jobs with needs:; fail fast at lint
Final Standards

Workflow Checklist

Click each item to track your review.

  • timeout-minutes set on every job — never rely on the 6-hour default.
  • permissions scoped at workflow and job level — default is write-all, always restrict.
  • Actions pinned by SHA for third-party actions, especially in security-sensitive workflows.
  • Secrets not echoed in run: steps. Use add-mask when dynamic secret values must be computed.
  • concurrency group configured to cancel stale runs on PR updates and branch pushes.
  • Dependency caching enabled for npm/pip/gradle/cargo — reduces job time significantly.
  • Dependabot configured for all package ecosystems including github-actions ecosystem.
  • Dependabot auto-merge scoped — patch only. Major updates require manual review.
  • OIDC used for cloud auth — no long-lived secrets stored for AWS/Azure/GCP authentication.
  • Environment protection rules set for production — required reviewers, deployment branch restrictions.
  • Reusable workflows used for shared CI logic across repos — not copy-paste YAML.
  • Self-hosted runner security — ephemeral (--ephemeral), not used on public repos, isolated network.
  • artifact retention-days set appropriately — not left at 90 days default for every artifact type.
  • Lint / fast gates run first — fail early at lint/type-check before expensive test and build jobs.
GitHub Actions — Complete Handbook 2025–2026 v5 / v6 Actions · Dependabot · Runners · CI/CD