|$ curl https://forge-ai.dev/api/markdown?path=docs/github-actions
$cat docs/github-actions-—-ci/cd.md
updated Recently·55 min read·published

GitHub Actions — CI/CD

CI/CDAutomationIntermediate to Advanced
Introduction

GitHub Actions is an event-driven CI/CD platform that automates software workflows directly in your GitHub repository. You can build, test, and deploy your code with reusable workflows triggered by GitHub events like pushes, pull requests, or scheduled intervals.

Actions run on GitHub-hosted runners (Linux, Windows, macOS) or self-hosted runners. Each workflow is defined in YAML and lives in the .github/workflows/ directory. This section covers workflow syntax, triggers, jobs, matrix builds, caching, secrets, artifacts, and reusable workflows.

Workflow Structure

Every workflow is a YAML file in .github/workflows/. The file name becomes the workflow name on GitHub. A workflow consists of triggers, jobs, and steps.

.github/workflows/ci.yml
YAML
1# .github/workflows/ci.yml
2name: Continuous Integration
3
4on:
5 push:
6 branches: [main, develop]
7 pull_request:
8 branches: [main]
9
10env:
11 NODE_VERSION: "20"
12
13jobs:
14 lint:
15 runs-on: ubuntu-latest
16 steps:
17 - uses: actions/checkout@v4
18 - uses: actions/setup-node@v4
19 with:
20 node-version: ${{ env.NODE_VERSION }}
21 - run: npm ci
22 - run: npm run lint
23
24 test:
25 runs-on: ubuntu-latest
26 needs: lint
27 steps:
28 - uses: actions/checkout@v4
29 - uses: actions/setup-node@v4
30 with:
31 node-version: ${{ env.NODE_VERSION }}
32 - run: npm ci
33 - run: npm test
34 - run: npm run test:coverage
35 - uses: codecov/codecov-action@v4
36 with:
37 token: ${{ secrets.CODECOV_TOKEN }}
38
39 build:
40 runs-on: ubuntu-latest
41 needs: test
42 steps:
43 - uses: actions/checkout@v4
44 - uses: actions/setup-node@v4
45 with:
46 node-version: ${{ env.NODE_VERSION }}
47 - run: npm ci
48 - run: npm run build

info

The needs keyword creates job dependencies. In the example above, test waits for lint, and build waits for test. Jobs without needs run in parallel by default. This creates efficient pipelines where independent checks run simultaneously.
Triggers

Triggers tell GitHub Actions when to run your workflow. You can use a single trigger or combine multiple triggers for complex scenarios.

triggers.yml
YAML
1on:
2 # Push to any branch
3 push:
4
5 # Push to specific branches
6 push:
7 branches:
8 - main
9 - "releases/**"
10
11 # Pull request to main
12 pull_request:
13 branches: [main]
14
15 # Scheduled (cron syntax)
16 schedule:
17 - cron: "0 6 * * 1-5" # Weekdays at 6 AM UTC
18
19 # Manual trigger (workflow_dispatch)
20 workflow_dispatch:
21 inputs:
22 environment:
23 description: "Deploy environment"
24 required: true
25 default: "staging"
26 type: choice
27 options:
28 - staging
29 - production
30
31 # On release publish
32 release:
33 types: [published]
34
35 # On issue comment
36 issue_comment:
37 types: [created]
38
39 # On label added to PR
40 pull_request:
41 types: [labeled]
42
43 # Path filtering (only run if specific paths change)
44 push:
45 paths:
46 - "src/**"
47 - "!src/docs/**"
48 - ".github/workflows/**"

best practice

Use path filtering to avoid running CI on documentation changes or non-code updates. The ! prefix negates a pattern. Combine this with paths-ignore for maximum control over when workflows execute.
Matrix Builds

Matrix strategies run a job across multiple combinations of variables. This is essential for testing across operating systems, language versions, and dependency configurations.

matrix.yml
YAML
1jobs:
2 test:
3 runs-on: ${{ matrix.os }}
4 strategy:
5 matrix:
6 os: [ubuntu-latest, windows-latest, macos-latest]
7 node: [18, 20, 22]
8 include:
9 - os: ubuntu-latest
10 node: 20
11 coverage: true
12 exclude:
13 - os: macos-latest
14 node: 18
15 steps:
16 - uses: actions/checkout@v4
17 - uses: actions/setup-node@v4
18 with:
19 node-version: ${{ matrix.node }}
20 - run: npm ci
21 - run: npm test
22 - if: matrix.coverage
23 run: npm run test:coverage
24
25 # Dynamic matrix from JSON output
26 deploy:
27 runs-on: ubuntu-latest
28 strategy:
29 matrix:
30 service: [api, web, worker]
31 steps:
32 - run: echo "Deploying ${{ matrix.service }}"

Key matrix keywords:

KeywordPurpose
matrixDefine the variable combinations
includeAdd extra combinations with additional variables
excludeRemove specific combinations
fail-fastCancel all matrix jobs if one fails (default: true)
max-parallelLimit concurrent matrix jobs
🔥

pro tip

Use fail-fast: false for test matrices where you want to see results across all configurations even if some fail. This is useful for identifying OS-specific bugs without fixing them in sequence.
Caching Dependencies

Caching speeds up workflows by reusing dependencies from previous runs. The actions/cache action stores and retrieves cached files based on a key.

caching.yml
YAML
1jobs:
2 test:
3 runs-on: ubuntu-latest
4 steps:
5 - uses: actions/checkout@v4
6
7 # Cache npm dependencies
8 - uses: actions/cache@v4
9 id: npm-cache
10 with:
11 path: ~/.npm
12 key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
13 restore-keys: |
14 ${{ runner.os }}-node-
15
16 # Install only if cache miss
17 - if: steps.npm-cache.outputs.cache-hit != 'true'
18 run: npm ci
19
20 # Cache pip dependencies (Python)
21 - uses: actions/cache@v4
22 with:
23 path: ~/.cache/pip
24 key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
25 restore-keys: |
26 ${{ runner.os }}-pip-
27
28 # Cache Gradle dependencies
29 - uses: actions/cache@v4
30 with:
31 path: |
32 ~/.gradle/caches
33 ~/.gradle/wrapper
34 key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
35 restore-keys: |
36 ${{ runner.os }}-gradle-
37
38 # Cache Docker layers
39 - uses: actions/cache@v4
40 with:
41 path: /tmp/.buildx-cache
42 key: ${{ runner.os }}-buildx-${{ github.sha }}
43 restore-keys: |
44 ${{ runner.os }}-buildx-

info

The cache key determines when to invalidate the cache. Use hashFiles('package-lock.json') in the key so the cache is invalidated when dependencies change. Use restore-keys to fall back to a less specific cache if the exact key does not match.
Secrets & Environment Variables

Secrets are encrypted environment variables stored at the repository, environment, or organization level. They are masked in logs and never exposed to forks.

secrets.yml
YAML
1jobs:
2 deploy:
3 runs-on: ubuntu-latest
4 environment: production
5 env:
6 APP_ENV: production
7 NODE_OPTIONS: --max-old-space-size=4096
8 steps:
9 - uses: actions/checkout@v4
10
11 # Access repository secrets
12 - run: |
13 echo "${{ secrets.DEPLOY_KEY }}" > deploy-key.pem
14 chmod 600 deploy-key.pem
15
16 # Environment-specific secrets
17 - run: ./deploy.sh
18 env:
19 API_TOKEN: ${{ secrets.API_TOKEN }}
20 DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
21
22 # Pass secrets as environment variables
23 - uses: some/action@v1
24 with:
25 api-key: ${{ secrets.API_KEY }}
26
27 # Use GitHub token (auto-generated)
28 - run: gh pr comment "$PR_URL" --body "Deployed!"
29 env:
30 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31
32 # Organization-level secret
33 - run: npm publish
34 env:
35 NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Secret TypeScopeConfiguration
RepositorySingle repoSettings > Secrets and variables > Actions
EnvironmentSpecific deployment environmentSettings > Environments > <name>
OrganizationAll repos in orgOrganization settings > Secrets and variables
GITHUB_TOKENAuto-generated per workflow runAvailable as { secrets.GITHUB_TOKEN }

warning

Never pass secrets via echo or print commands. GitHub masks secrets in logs, but the masking is not perfect — secrets in multi-line output, binary data, or with special characters can leak. Use dedicated action inputs or environment variables only.
Artifacts

Artifacts are files produced by a workflow that you can download or pass between jobs. They persist for a configurable retention period (default 90 days).

artifacts.yml
YAML
1jobs:
2 build:
3 runs-on: ubuntu-latest
4 steps:
5 - uses: actions/checkout@v4
6 - run: npm ci
7 - run: npm run build
8
9 # Upload build output as artifact
10 - uses: actions/upload-artifact@v4
11 with:
12 name: build-output
13 path: dist/
14 if-no-files-found: error
15 retention-days: 7
16
17 deploy:
18 runs-on: ubuntu-latest
19 needs: build
20 steps:
21 # Download artifact from build job
22 - uses: actions/download-artifact@v4
23 with:
24 name: build-output
25 path: dist/
26
27 - run: ./deploy.sh
28
29 # Upload test results and reports
30 test:
31 runs-on: ubuntu-latest
32 steps:
33 - uses: actions/checkout@v4
34 - run: npm ci
35 - run: npm test
36
37 - uses: actions/upload-artifact@v4
38 with:
39 name: test-results
40 path: |
41 coverage/
42 test-results.xml
43 retention-days: 30
44
45 - uses: dorny/test-reporter@v1
46 if: success() || failure()
47 with:
48 name: Jest Tests
49 path: test-results.xml
50 reporter: jest-junit
🔥

pro tip

Upload artifacts only when needed — not every workflow run. Use if: always() on test result uploads so they are saved even if tests fail. Set reasonable retention periods: 1-7 days for build artifacts, 30-90 days for test reports.
Reusable Workflows

Reusable workflows (also called composite or called workflows) let you define a workflow once and call it from multiple other workflows. This is the foundation of DRY CI/CD.

reusable-workflows.yml
YAML
1# .github/workflows/deploy-template.yml
2# Reusable workflow with inputs and secrets
3name: Deploy Template
4on:
5 workflow_call:
6 inputs:
7 environment:
8 required: true
9 type: string
10 version:
11 required: true
12 type: string
13 secrets:
14 cloud_token:
15 required: true
16
17jobs:
18 deploy:
19 runs-on: ubuntu-latest
20 environment: ${{ inputs.environment }}
21 steps:
22 - uses: actions/checkout@v4
23 - run: |
24 echo "Deploying version ${{ inputs.version }}"
25 echo "To environment: ${{ inputs.environment }}"
26 - run: ./deploy.sh ${{ inputs.version }}
27 env:
28 CLOUD_TOKEN: ${{ secrets.cloud_token }}
29
30---
31# .github/workflows/release.yml
32# Caller workflow
33name: Release
34
35on:
36 push:
37 tags: ["v*"]
38
39jobs:
40 deploy-staging:
41 uses: ./.github/workflows/deploy-template.yml
42 with:
43 environment: staging
44 version: ${{ github.ref_name }}
45 secrets:
46 cloud_token: ${{ secrets.STAGING_TOKEN }}
47
48 deploy-production:
49 needs: deploy-staging
50 uses: ./.github/workflows/deploy-template.yml
51 with:
52 environment: production
53 version: ${{ github.ref_name }}
54 secrets:
55 cloud_token: ${{ secrets.PROD_TOKEN }}

best practice

Reusable workflows must be stored in the .github/workflows/ directory. They accept inputs (typed parameters) and secrets (sensitive values). Use them for standardized CI pipelines, deployment steps, and security checks that every repo in your organization should follow.
Marketplace Actions

The GitHub Marketplace hosts thousands of community and verified actions for common tasks. Always check the action source, popularity, and maintenance status before using.

Essential Actions

actions/checkoutCheckout repository code into the runner
actions/setup-nodeSet up Node.js with caching
actions/cacheCache dependencies and build outputs
actions/upload-artifactUpload files between jobs
actions/download-artifactDownload previously uploaded artifacts
actions/setup-pythonSet up Python environment
actions/docker-setup-buildxSet up Docker Buildx for multi-platform builds
docker/login-actionAuthenticate with container registries
docker/build-push-actionBuild and push Docker images
codecov/codecov-actionUpload coverage reports to Codecov
github/codeql-actionRun CodeQL security analysis
dorny/test-reporterPublish test results in PR checks
marketplace-usage.yml
YAML
1# Using marketplace actions
2steps:
3 - uses: actions/checkout@v4
4
5 # Pin specific version (avoid @latest)
6 - uses: actions/setup-node@v4
7 with:
8 node-version: "20"
9
10 # Use Docker actions
11 - uses: docker/login-action@v3
12 with:
13 registry: ghcr.io
14 username: ${{ github.actor }}
15 password: ${{ secrets.GITHUB_TOKEN }}
16
17 - uses: docker/build-push-action@v5
18 with:
19 context: .
20 push: true
21 tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
22
23 # Security scanning
24 - uses: github/codeql-action/init@v3
25 with:
26 languages: javascript
27 - uses: github/codeql-action/analyze@v3
28
29 # Notifications
30 - uses: 8398a7/action-slack@v3
31 with:
32 status: ${{ job.status }}
33 fields: repo,message,commit,author
34 if: always()

warning

Always pin marketplace actions to a specific major version (v4, v3) or commit SHA. Never use @main or @latest — they can introduce breaking changes without notice. For actions with security implications (checkout, Docker, auth), consider pinning to the exact commit SHA for supply chain security.
Self-Hosted Runners

Self-hosted runners give you control over the execution environment. They are useful for accessing private networks, custom hardware, or specific OS configurations not available on GitHub-hosted runners.

self-hosted.yml
YAML
1# Using self-hosted runners
2jobs:
3 deploy:
4 runs-on: self-hosted
5 # Or use labels for specific runners:
6 # runs-on: [self-hosted, linux, gpu]
7
8 steps:
9 - uses: actions/checkout@v4
10 - run: ./deploy-internal.sh
11
12 # Use runner groups
13 security-scan:
14 runs-on: [self-hosted, security-vm]
15 steps:
16 - run: ./vulnerability-scan.sh
17
18# Register a self-hosted runner:
19# Settings > Actions > Runners > Add runner
20# Follow the OS-specific instructions
21#
22# Key considerations:
23# - Runners must have outbound HTTPS access to GitHub
24# - Apply security patches regularly
25# - Use runner groups for access control
26# - Isolate runners per environment (dev/staging/prod)
27# - Consider ephemeral runners (auto-terminate after use)

warning

Self-hosted runners introduce security risks. Anyone with workflow write access can execute arbitrary code on your runner. Only use self-hosted runners from trusted repositories, and consider using ephemeral runners that are destroyed after each job for better isolation.
Workflow Commands & Expressions

GitHub Actions supports expressions, functions, and workflow commands for dynamic configuration and logging.

expressions.yml
YAML
1# Expressions and context objects
2jobs:
3 example:
4 runs-on: ubuntu-latest
5 steps:
6 # Functions
7 - run: echo "Branch: ${{ github.ref_name }}"
8 - run: echo "SHA: ${{ github.sha }}"
9 - run: echo "Trigger: ${{ github.event_name }}"
10
11 # Conditional execution
12 - run: echo "PR from fork"
13 if: github.event.pull_request.head.repo.fork
14
15 # Status check functions
16 - run: echo "Always runs"
17 if: always()
18 - run: echo "Only on failure"
19 if: failure()
20 - run: echo "Only on success"
21 if: success()
22
23 # Contains function
24 - run: echo "Branch name contains feature"
25 if: contains(github.ref_name, 'feature')
26
27 # Formatting with expressions
28 - run: echo "Deploying ${{ format('v{0}.{1}.{2}', 1, 2, 3) }}"
29
30 # Output variables between steps
31 - id: get-version
32 run: echo "version=1.0.0" >> $GITHUB_OUTPUT
33 - run: echo "Version is ${{ steps.get-version.outputs.version }}"
🔥

pro tip

Use $GITHUB_OUTPUT to set outputs from steps (modern approach). The old ::set-output syntax is deprecated. Use $GITHUB_ENV to set environment variables that persist across steps.
Concurrency & Cancellation

Concurrency controls prevent multiple workflow runs from overlapping. This is critical for deployment workflows where simultaneous runs could cause race conditions.

concurrency.yml
YAML
1# Cancel in-progress runs for the same PR/push
2concurrency:
3 group: ${{ github.workflow }}-${{ github.ref }}
4 cancel-in-progress: true
5
6# Environment-specific concurrency
7concurrency:
8 group: deploy-${{ inputs.environment }}
9 cancel-in-progress: false
10
11# Per-job concurrency
12jobs:
13 deploy:
14 runs-on: ubuntu-latest
15 concurrency:
16 group: deploy-${{ github.ref_name }}
17 cancel-in-progress: false
18 steps:
19 - run: ./deploy.sh
20
21# Prevent concurrent deployments to the same env
22concurrency:
23 group: ${{ github.workflow }}-${{ inputs.environment }}
24 cancel-in-progress: ${{ inputs.environment == 'staging' }}

best practice

Always set cancel-in-progress: true for CI workflows (test, lint) — no need to run outdated checks. Set it to false for deployment workflows to prevent partial deployments.
Troubleshooting

Debugging failing workflows requires inspecting logs, enabling debug logging, and understanding common failure patterns.

troubleshooting.yml
YAML
1# Enable verbose logging (repo secrets)
2# Set these secrets:
3# ACTIONS_STEP_DEBUG: true - Step-level debug
4# ACTIONS_RUNNER_DEBUG: true - Runner-level debug
5
6# Debug job with SSH access (troubleshoot runners)
7jobs:
8 debug:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v4
12 - uses: mxschmitt/action-tmate@v3
13 with:
14 limit-access-to-actor: true
15 # This opens an SSH session to the runner.
16 # Connect with the URL shown in the logs.
17
18# Common debugging steps:
19# - Use 'run: env' to print all environment variables
20# - Use 'run: ls -la' to inspect file structure
21# - Use 'run: pwd' to confirm working directory
22# - Check runner OS: ${{ runner.os }}
23# - Check available tools: run: which node && node --version

info

For complex debugging, use action-tmate to SSH into the runner during a workflow run. This gives you direct access to inspect the filesystem, environment, and running processes. Always set limit-access-to-actor: true for security.
Common Pitfalls

✗ Expired secrets

Error: Resource not accessible by integration

Secrets can expire (e.g., cloud provider tokens). Rotate them regularly and use GitHub Environments for environment-specific secrets.

✗ Unnecessary CI runs

On: push (to all branches including docs/)

Use path filters to skip CI for documentation, markdown, or config changes. This saves runner minutes and reduces feedback time for real changes.

✗ Hardcoded values instead of variables

run: echo "Deploying to us-east-1"

Use variables and environment files for configurable values. Use vars context for non-sensitive configuration variables.

✗ Missing cache invalidation

key: ${{ runner.os }}-node-modules

Cache keys without hashFiles never invalidate. Always include hashFiles('package-lock.json') or equivalent to automatically bust the cache when dependencies change.

$Blueprint — Engineering Documentation·Section ID: CI-01·Revision: 1.0