GitHub Actions is a CI/CD and workflow automation platform built directly into GitHub. It lets you build, test, and deploy code from your repository using YAML-defined workflows triggered by events like pushes, pull requests, schedules, or manual dispatch. Over 78% of developers now use CI/CD in their workflow, and GitHub Actions is the most popular platform — processing over 2 billion workflow runs per month across 14+ million repositories. Workflows run on GitHub-hosted runners (Ubuntu, Windows, macOS) or self-hosted machines, with a generous free tier of 2,000 minutes/month for public repos and 500 for private.
How Does GitHub Actions Work?
GitHub Actions revolves around three core concepts: workflows, jobs, and steps. A workflow is a YAML file in .github/workflows/ that defines automated tasks. Each workflow contains one or more jobs that run on a runner. Each job contains steps — individual commands or reusable actions.
Event (push, PR, schedule, manual)
│
▼
Workflow (.github/workflows/*.yml)
│
├── Job 1 (runs-on: ubuntu-latest)
│ ├── Step 1: actions/checkout@v4
│ ├── Step 2: actions/setup-node@v4
│ └── Step 3: run: npm test
│
└── Job 2 (runs-on: ubuntu-latest, needs: job1)
├── Step 1: actions/checkout@v4
└── Step 2: run: npm run deploy
Jobs run in parallel by default.
Use "needs:" to create sequential dependencies.Your First Workflow
Create a file at .github/workflows/ci.yml in your repository:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm testThis workflow runs on every push and pull request to main. It checks out the code, sets up Node.js with npm caching, installs dependencies, and runs tests. That's a production-ready CI pipeline in 15 lines of YAML.
Workflow Triggers (Events)
The on: key defines what triggers your workflow. GitHub supports 30+ event types.
| Trigger | When it fires | Example |
|---|---|---|
| push | Code pushed to branch/tag | on: push |
| pull_request | PR opened, synced, or reopened | on: pull_request |
| schedule | Cron-based schedule | on: schedule: [{cron: '0 8 * * 1'}] |
| workflow_dispatch | Manual trigger via UI/API | on: workflow_dispatch |
| release | Release published | on: release: types: [published] |
| workflow_call | Called by another workflow | on: workflow_call |
| repository_dispatch | External webhook event | on: repository_dispatch |
| issue_comment | Comment on issue/PR | on: issue_comment: types: [created] |
Filtering triggers
Use branches, tags, and paths filters to narrow when workflows run:
on:
push:
branches: [main, 'release/**']
paths:
- 'src/**'
- 'package.json'
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [main]
types: [opened, synchronize, reopened]Path filters are critical for monorepos — they prevent unnecessary CI runs when unrelated files change, saving minutes and money.
Workflow Syntax Reference
The complete YAML structure of a workflow file:
name: Deploy # Display name in Actions tab
run-name: Deploy by @${{ github.actor }} # Custom run name
on:
push:
branches: [main]
permissions: # Least-privilege GITHUB_TOKEN
contents: read
deployments: write
env: # Workflow-level env vars
NODE_ENV: production
concurrency: # Prevent parallel runs
group: deploy-${{ github.ref }}
cancel-in-progress: true
defaults: # Default shell and working dir
run:
shell: bash
working-directory: ./app
jobs:
build:
runs-on: ubuntu-latest # Runner environment
timeout-minutes: 15 # Job timeout (default: 360)
environment: production # Deployment environment
outputs:
version: ${{ steps.ver.outputs.version }}
steps:
- uses: actions/checkout@v4 # Use a published action
with:
fetch-depth: 0 # Action inputs
- name: Get version # Step display name
id: ver # Step ID for referencing outputs
run: echo "version=$(cat version.txt)" >> "$GITHUB_OUTPUT"
- run: npm ci # Inline shell command
- run: npm run build
deploy:
needs: build # Sequential dependency
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # Conditional execution
steps:
- run: echo "Deploying v${{ needs.build.outputs.version }}"What Are Actions?
Actions are reusable units of code that perform a specific task. You reference them with uses: in a step. There are 20,000+ actions on the GitHub Marketplace.
| Action | Purpose | Usage |
|---|---|---|
| actions/checkout@v4 | Clone repo into runner | Almost every workflow |
| actions/setup-node@v4 | Install Node.js + cache | Node.js projects |
| actions/setup-python@v5 | Install Python + cache | Python projects |
| actions/cache@v4 | Cache deps between runs | Speed up builds |
| actions/upload-artifact@v4 | Save build outputs | Share between jobs |
| actions/download-artifact@v4 | Retrieve saved outputs | Downstream jobs |
| github/codeql-action@v3 | Security scanning | Code security |
| docker/build-push-action@v6 | Build + push images | Container workflows |
Matrix Strategy: Testing Across Versions
The strategy.matrix key runs a job multiple times with different configurations — essential for testing across Node versions, operating systems, or database versions:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # Don't cancel other jobs if one fails
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [20, 22]
exclude:
- os: windows-latest
node-version: 20 # Skip this combination
include:
- os: ubuntu-latest
node-version: 22
coverage: true # Add extra variable to one combo
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
- if: matrix.coverage
run: npm run test:coverageThis creates 5 parallel jobs (3 OS × 2 Node versions, minus 1 exclusion). Matrix builds catch platform-specific bugs before users do.
Secrets and Environment Variables
GitHub provides three levels of variable storage: secrets (encrypted, write-only), variables (plaintext configuration), and environments (scoped secrets + protection rules).
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Activate environment secrets + protection rules
steps:
- run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"ref": "${{ github.sha }}"}' \
${{ vars.DEPLOY_URL }}/api/deploy| Feature | Secrets | Variables | Environments |
|---|---|---|---|
| Storage | Encrypted | Plaintext | Scoped secrets + vars |
| Visibility in logs | Auto-masked | Visible | Auto-masked (secrets) |
| Access syntax | ${{ secrets.NAME }} | ${{ vars.NAME }} | Same, scoped by environment |
| Scope levels | Org, repo | Org, repo | Per environment (staging, prod) |
| Protection rules | No | No | Yes (reviewers, wait timer, branches) |
Caching Dependencies
Caching can reduce workflow times by 50-80%. Most setup actions have built-in caching, but you can also use actions/cache directly:
# Option 1: Built-in cache (preferred)
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm # Automatically caches ~/.npm
# Option 2: Explicit cache (for custom paths)
- uses: actions/cache@v4
with:
path: |
node_modules
~/.cache/turbo
key: ${{ runner.os }}-deps-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-deps-Reusable Workflows
Reusable workflows let you define a workflow once and call it from multiple other workflows — DRY for CI/CD. They use the workflow_call trigger and support typed inputs, secrets, and outputs.
name: Reusable Test
on:
workflow_call:
inputs:
node-version:
type: number
default: 22
secrets:
NPM_TOKEN:
required: false
outputs:
coverage:
description: Test coverage percentage
value: ${{ jobs.test.outputs.coverage }}
jobs:
test:
runs-on: ubuntu-latest
outputs:
coverage: ${{ steps.cov.outputs.pct }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm test
- id: cov
run: echo "pct=85" >> "$GITHUB_OUTPUT"name: CI
on: [push, pull_request]
jobs:
test:
uses: ./.github/workflows/reusable-test.yml # Same repo
with:
node-version: 22
secrets: inherit # Forward all secrets
test-external:
uses: org/shared-workflows/.github/workflows/test.yml@v1 # Cross-repo
with:
node-version: 20Reusable workflows can nest up to 10 levels deep, with a maximum of 50 workflow calls per run.
Composite Actions
Composite actions bundle multiple steps into a single reusable action. Unlike reusable workflows, they run inline within the calling job — no separate runner. Use them for smaller, repeated step sequences:
name: Setup Project
description: Checkout, install Node.js, and install dependencies
inputs:
node-version:
default: '22'
runs:
using: composite
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
shell: bashjobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: ./.github/actions/setup-project
with:
node-version: '22'
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: ./.github/actions/setup-project
- run: npm testArtifacts: Sharing Data Between Jobs
Jobs run on separate runners, so they don't share a filesystem. Use artifacts to pass files between jobs:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7 # Default is 90
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- run: ./deploy.sh dist/CI/CD Pipeline Architectures
Every team needs a CI/CD pipeline, but the shape depends on your team size, release cadence, and risk tolerance. Below are three proven architectures — from the simplest PR-to-production flow to progressive multi-environment delivery.
Pipeline 1: Standard PR → Production
The most common pipeline for small-to-medium teams. Every PR triggers parallel CI jobs (lint, test, build). Merging to main triggers deployment. Simple, effective, and easy to debug.
name: CI
on:
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: ci-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/name: Deploy
on:
push:
branches: [main]
permissions:
contents: read
deployments: write
concurrency:
group: deploy-production
cancel-in-progress: false # Don't cancel in-progress deploys
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Requires approval if configured
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- run: npx wrangler deploy # Or: vercel deploy --prod
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}When to use: single-product teams, startups, projects with fast iteration cycles. Branch protection rules enforce that CI must pass before merging.
Pipeline 2: Trunk-Based Development
The fastest path to production. Short-lived branches (hours, not weeks) merge directly to main. Feature flags gate incomplete work. Merging to main triggers an automated deploy. Used by Google, Meta, and most high-velocity teams.
name: Trunk CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
deployments: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
- run: pnpm test
- run: pnpm run build
deploy-staging:
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile && pnpm run build
- run: npx wrangler deploy --env staging
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
deploy-production:
needs: deploy-staging
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production # Manual approval gate
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile && pnpm run build
- run: npx wrangler deploy --env production
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}When to use: teams deploying to production daily or more frequently. Requires good test coverage and feature flag infrastructure.
Pipeline 3: Progressive Delivery (Multi-Environment)
The safest path to production. Changes flow through multiple environments with automated quality gates at each stage. Each environment validates a different dimension — functionality, performance, and real-world traffic.
name: Progressive Deploy
on:
push:
branches: [main]
permissions:
contents: read
deployments: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile && pnpm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/download-artifact@v4
with: { name: dist, path: dist/ }
- run: npx wrangler deploy --env staging
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
- name: Run smoke tests against staging
run: |
curl -sf https://staging.example.com/api/health || exit 1
npx playwright test --config=e2e/staging.config.ts
deploy-canary:
needs: deploy-staging
runs-on: ubuntu-latest
environment: canary # Auto-approve or short timer
steps:
- uses: actions/download-artifact@v4
with: { name: dist, path: dist/ }
- name: Deploy to 5% of traffic
run: npx wrangler deploy --env canary
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
- name: Monitor error rate (5 min)
run: |
sleep 300
ERROR_RATE=$(curl -s https://api.example.com/metrics/error-rate)
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "Error rate $ERROR_RATE exceeds 1% — aborting"
exit 1
fi
deploy-production:
needs: deploy-canary
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- uses: actions/download-artifact@v4
with: { name: dist, path: dist/ }
- run: npx wrangler deploy --env production
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
- name: Post-deploy health check
run: curl -sf https://example.com/api/health || exit 1When to use: teams with SLAs, regulated industries, or user-facing products where downtime has measurable business impact. The extra stages add 10-15 min but catch issues before they reach all users.
Which Pipeline Architecture Should You Choose?
| Factor | Standard PR → Prod | Trunk-Based | Progressive Delivery |
|---|---|---|---|
| Team size | 1-10 devs | 5-50 devs | 10+ devs |
| Deploy frequency | Daily to weekly | Multiple per day | Daily with safety nets |
| Setup complexity | Low (1-2 workflow files) | Medium (feature flags) | High (multiple envs) |
| Risk tolerance | Medium | Low (fast rollback) | Very low (staged rollout) |
| Time to production | 5-10 min | 3-8 min | 15-30 min |
| Best for | Startups, small teams | High-velocity SaaS | Enterprise, regulated |
Common Workflow Patterns
Monorepo: run only affected packages
on:
push:
branches: [main]
pull_request:
jobs:
web:
if: >-
github.event_name == 'push' ||
contains(github.event.pull_request.changed_files, 'apps/web')
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test
- run: pnpm buildDeploy on release
on:
release:
types: [published]
permissions:
contents: read
deployments: write
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}Scheduled job (nightly dependency check)
on:
schedule:
- cron: '0 6 * * 1' # Every Monday at 06:00 UTC
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit --production
- run: npx license-checker --failOn 'GPL'Docker build, scan, and push
name: Docker
on:
push:
branches: [main]
tags: ['v*']
permissions:
contents: read
packages: write
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=
type=semver,pattern={{version}}
type=raw,value=latest,enable={{is_default_branch}}
- uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=maxPR preview deploy with comment
name: Preview
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: write
deployments: write
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile && pnpm run build
- name: Deploy preview
id: deploy
run: |
URL=$(npx wrangler deploy --env preview-${{ github.event.pull_request.number }} 2>&1 | grep -o 'https://[^ ]*')
echo "url=$URL" >> "$GITHUB_OUTPUT"
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
- name: Comment preview URL
run: |
gh pr comment ${{ github.event.pull_request.number }} \
--body "🔍 Preview deployed: ${{ steps.deploy.outputs.url }}" \
--edit-last || \
gh pr comment ${{ github.event.pull_request.number }} \
--body "🔍 Preview deployed: ${{ steps.deploy.outputs.url }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Starter Templates and Sample Repositories
Copy-paste these templates or explore the linked repos for real-world examples:
Node.js CI
Lint, test, and build a Node.js project with caching
push + pull_requestDocker Build & Push
Build multi-platform images and push to GitHub Container Registry
push (tags)Terraform Plan & Apply
Plan on PR, apply on merge with state locking
push + pull_requestGitHub Pages Deploy
Build static site and deploy to GitHub Pages
push (main)CodeQL Security Scanning
Automated vulnerability detection via GitHub code scanning
push + scheduleDependabot Auto-Merge
Auto-approve and merge minor/patch dependency updates
pull_request (dependabot)Concurrency: Preventing Duplicate Runs
When you push multiple commits quickly, you don't want every push to trigger a full CI run. Use concurrency to cancel outdated runs:
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: trueThis groups runs by PR number (or branch ref for pushes) and cancels any running workflow when a new one starts in the same group. This alone can cut your Actions bill by 30-50% on active repos.
Permissions and GITHUB_TOKEN
Every workflow run gets a GITHUB_TOKEN with configurable permissions. Always follow least-privilege:
# Workflow-level (applies to all jobs)
permissions:
contents: read
pull-requests: write
issues: read
# Or per-job (overrides workflow-level)
jobs:
lint:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lint
comment:
permissions:
pull-requests: write
runs-on: ubuntu-latest
steps:
- run: gh pr comment ${{ github.event.number }} --body "All checks passed"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Set the repository default to read in Settings → Actions → General → Workflow permissions.
Security Best Practices
- Pin actions to full commit SHAs — tags can be moved or compromised. Use
uses: actions/checkout@<full-sha>in production - Never use structured data as secrets — JSON/XML/YAML secrets cannot be properly redacted in logs
- Avoid
pull_request_targetunless you understand the security implications — it runs with write access on code from forks - Don't use self-hosted runners for public repos — fork PRs can execute arbitrary code on your runner
- Mask sensitive output — use
echo "::add-mask::$VALUE"for dynamically generated secrets - Audit third-party actions — review source code before using, especially for actions that handle secrets
- Use environments for deployments — protection rules enforce required reviewers, wait timers, and branch restrictions
Performance Optimization
| Technique | Impact | How |
|---|---|---|
| Dependency caching | 50-80% faster installs | cache: npm in setup-node or actions/cache |
| Concurrency + cancel-in-progress | 30-50% fewer minutes | concurrency group per PR/branch |
| Path filters | Skip irrelevant runs | paths: / paths-ignore: in on: push |
| Matrix fail-fast: false | Full test coverage | Don't cancel other matrix jobs on failure |
| Smaller runners | Lower cost | Use ubuntu-latest over macos-latest when possible |
| Timeout limits | Prevent runaway jobs | timeout-minutes: 15 on each job |
| Parallel jobs | Faster pipeline | Split lint, test, build into separate jobs |
Debugging Failed Workflows
When a workflow fails, use these techniques to diagnose the issue:
- Enable debug logging — set repo secret
ACTIONS_STEP_DEBUGtotruefor verbose step output - Re-run with debug — click "Re-run jobs" → "Enable debug logging" in the Actions UI
- Use
ghCLI —gh run view --log-failedshows only failed step logs - Add diagnostic steps — temporarily add
run: env | sortorrun: cat $GITHUB_EVENT_PATHto inspect context - Check runner status —
gh run view <run-id>shows which runner was assigned and its status
# List recent workflow runs
gh run list --limit 5
# View a specific run
gh run view <run-id>
# Watch a run in real-time
gh run watch <run-id>
# View only the failed logs
gh run view <run-id> --log-failed
# Re-run failed jobs
gh run rerun <run-id> --failed
# Trigger a workflow manually
gh workflow run ci.yml --ref mainReusable Workflows vs Composite Actions
| Feature | Reusable Workflow | Composite Action |
|---|---|---|
| Runs on | Separate runner | Same job (inline) |
| Trigger | workflow_call | uses: in a step |
| Can have jobs? | Yes (multiple) | No (steps only) |
| Secrets access | Explicit pass or inherit | Inherits from caller job |
| Environment support | Yes | No |
| Nesting depth | Up to 10 levels | Unlimited (but keep it shallow) |
| Best for | Full pipelines (CI, deploy) | Setup steps, small reusable units |
Rule of thumb: use reusable workflows for big shared pipelines (build + test matrix, deploy). Use composite actions for smaller repeated steps (project setup, formatting, packaging).
Frequently Asked Questions
How much does GitHub Actions cost?
Public repositories get unlimited free minutes. Private repos get 500 free minutes/month on the Free plan, 3,000 on Team, and 50,000 on Enterprise. Linux runners cost $0.008/min, macOS $0.08/min (10x), and Windows $0.016/min (2x). Self-hosted runners have no per-minute charge.
What is the difference between secrets and variables?
Secrets are encrypted and write-only — you can set them but never read them back in the UI. They are automatically masked in logs. Variables are plaintext configuration values visible in the UI and in logs. Use secrets for API tokens, passwords, and keys. Use variables for non-sensitive config like URLs, feature flags, or version numbers.
How do I run a workflow only on specific file changes?
Use path filters in your trigger: on: push: paths: ['src/**', 'package.json']. You can also use paths-ignore to exclude files like documentation. Path filters work with both push and pull_request events. For monorepos, this prevents unnecessary CI runs when unrelated packages change.
Can I run GitHub Actions locally?
Yes, use the open-source tool "act" (github.com/nektos/act). It runs workflows locally using Docker containers that simulate GitHub runners. It supports most features but cannot perfectly replicate GitHub-hosted runner environments. It is useful for fast iteration during workflow development.
How do I pass data between jobs?
For small values (strings, numbers), use job outputs: write to $GITHUB_OUTPUT in a step, declare the output in the job's outputs map, then read it in downstream jobs via needs.job-id.outputs.name. For files, use actions/upload-artifact and actions/download-artifact to pass build outputs, test results, or other files between jobs.
Should I pin actions to tags or commit SHAs?
Pin to full commit SHAs for production workflows. Tags like @v4 can be moved or compromised by the action author, meaning a tag you trusted yesterday could point to different code today. SHAs are immutable. Use Dependabot or Renovate to automatically update pinned SHAs when new versions are released.
References
- GitHub Actions Documentation — official docs covering all features, syntax, and guides
- Workflow Syntax Reference — complete YAML syntax specification for workflow files
- Security Hardening for GitHub Actions — official security best practices for workflows and runners
- Reusable Workflows — guide to defining, calling, and nesting reusable workflows
- Starter Workflows Repository — official collection of workflow templates for various languages and platforms
- GitHub Actions Marketplace — browse 20,000+ community and official actions
- act — Run GitHub Actions Locally — open-source CLI tool for testing workflows on your machine before pushing
- Publishing Docker Images — official guide for building and pushing container images with Actions