Introduction
GitHub Actions launched in November 2019 and within three years became the dominant CI/CD platform for open-source software and a major player in enterprise. Its power lies in proximity to code — CI/CD lives in the same platform as your pull requests, issues, code reviews, and package registry. No separate tool. No separate authentication. No context switching.
Before GitHub Actions, teams had to choose between hosted CI services (Travis CI, CircleCI) that required syncing code, or self-managed tools (Jenkins) that demanded dedicated infrastructure. GitHub Actions eliminated that tradeoff by making CI/CD a native feature of the world's largest code hosting platform.
Market Position
As of 2026, GitHub Actions processes over 2 billion workflow runs per month. It's the default CI/CD for virtually all open-source projects and has overtaken Jenkins in new enterprise adoption. Key factors driving adoption include zero-setup for GitHub repositories, generous free tier (2,000 minutes/month for public repos), and deep integration with the GitHub ecosystem (PR checks, branch protection, deployments API).
This deep dive covers everything you need to build production-grade CI/CD pipelines with GitHub Actions — from basic workflow files to advanced patterns like OIDC authentication, organizational reusable workflows, and Kubernetes-based autoscaling runners.
Core Concepts
GitHub Actions has a clear hierarchy of concepts. Understanding how they relate is essential before writing your first workflow.
flowchart TD
A[Repository Event] -->|Triggers| B[Workflow]
B --> C[Job 1]
B --> D[Job 2]
B --> E[Job 3]
C --> F[Step 1: Checkout]
C --> G[Step 2: Setup Node]
C --> H[Step 3: Run Tests]
D --> I[Step 1: Build]
D --> J[Step 2: Push Image]
E --> K[Step 1: Deploy]
F -.->|Runs on| L[Runner: ubuntu-latest]
I -.->|Runs on| M[Runner: ubuntu-latest]
K -.->|Runs on| N[Runner: self-hosted]
style A fill:#3B9797,color:#fff
style B fill:#132440,color:#fff
style L fill:#16476A,color:#fff
style M fill:#16476A,color:#fff
style N fill:#BF092F,color:#fff
The core components:
- Workflow — A YAML file in
.github/workflows/that defines an automated process. A repository can have unlimited workflows. - Event — A trigger that starts a workflow: push, pull_request, schedule, manual dispatch, webhook, etc.
- Job — A set of steps that execute on the same runner. Jobs run in parallel by default, or sequentially when dependencies are declared.
- Step — A single task within a job. Either runs a shell command (
run:) or uses an action (uses:). - Action — A reusable unit of code. Can be JavaScript, Docker, or composite. Published to the Marketplace or defined locally.
- Runner — The machine that executes jobs. GitHub provides hosted runners (Ubuntu, Windows, macOS) or you can use self-hosted runners.
Runners
GitHub-hosted runners are virtual machines provisioned fresh for each job. They come pre-loaded with common tools (Git, Docker, Node.js, Python, Java, .NET, etc.) and are automatically cleaned up after each job — ensuring a pristine environment every time.
GitHub-Hosted: 2-core CPU, 7GB RAM (standard), 14GB RAM (large). Available in Ubuntu, Windows, macOS. Ephemeral — destroyed after each job. Free for public repos, metered for private repos.
Self-Hosted: Your own machines registered with GitHub. Any OS, any hardware. Persistent state between jobs (unless configured as ephemeral). Free to use, but you pay for infrastructure.
Larger Runners: GitHub-managed runners with 4-64 cores, up to 256GB RAM. Available on GitHub Team and Enterprise plans. Ideal for heavy compilation or integration tests.
Workflow Syntax Deep Dive
Every GitHub Actions workflow is a YAML file stored at .github/workflows/<name>.yml. The file defines when to run (triggers), where to run (runners), and what to run (steps).
Here's a complete, production-ready CI workflow that demonstrates the core syntax:
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
paths-ignore:
- '*.md'
- 'docs/**'
pull_request:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: '20'
REGISTRY: ghcr.io
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
needs: lint
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.node-version }}
path: coverage/
build:
runs-on: ubuntu-latest
needs: test
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ env.REGISTRY }}/${{ github.repository }}:${{ github.sha }}
Trigger Types
GitHub Actions supports over 35 event types. The most commonly used:
# Push and PR triggers with path/branch filtering
on:
push:
branches: [main, 'release/**']
tags: ['v*']
paths:
- 'src/**'
- 'package.json'
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
# Scheduled triggers (cron syntax, UTC)
on:
schedule:
- cron: '0 6 * * 1-5' # Weekdays at 6 AM UTC
# Manual trigger with typed inputs
on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., 1.2.3)'
required: true
type: string
dry_run:
description: 'Dry run mode'
required: false
type: boolean
default: true
# Cross-repository trigger via webhook
on:
repository_dispatch:
types: [deploy-trigger]
# Reusable workflow trigger
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
deploy_key:
required: true
pull_request event runs workflows on the merge commit (head branch merged into base), not on the PR head ref directly. This means the workflow has access to a combined view of both branches. For fork-based PRs, pull_request_target runs with base branch permissions — be careful with untrusted code.
Actions Marketplace
The GitHub Actions Marketplace hosts 20,000+ actions. These are pre-built, reusable steps that handle common CI/CD tasks. Understanding how to find, evaluate, and securely use marketplace actions is a critical skill.
Essential First-Party Actions
# actions/checkout — Clone your repository
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history (needed for git log, versioning)
submodules: recursive # Include git submodules
token: ${{ secrets.PAT }} # For private submodules
# actions/setup-node — Install and cache Node.js
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc' # Read version from .nvmrc
cache: 'npm' # Cache npm dependencies
registry-url: 'https://npm.pkg.github.com' # GitHub Packages
# actions/cache — Cache arbitrary directories
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
# actions/upload-artifact / download-artifact — Pass data between jobs
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 5
Pinning & Security
Third-party actions execute code in your workflow with access to your secrets and GITHUB_TOKEN. Pinning to a full commit SHA (not a tag) prevents supply-chain attacks where a tag is moved to malicious code:
# BAD: Tag can be moved to malicious code
- uses: some-org/some-action@v2
# GOOD: Pinned to immutable commit SHA
- uses: some-org/some-action@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
# SHA corresponds to v2.1.0 release
# BEST: Pin SHA + add comment with version for readability
- uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0
with:
push: true
tags: myapp:latest
@main or @master references for third-party actions in production workflows. A compromised upstream repository could inject code into every workflow run. Always pin to a full 40-character SHA and verify it matches a known release.
Matrix Builds
Matrix builds let you test across multiple configurations simultaneously — different OS versions, language versions, dependency versions, or any combination. Each matrix permutation creates a separate job that runs in parallel.
# .github/workflows/matrix-test.yml
name: Cross-Platform Tests
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # Don't cancel other jobs if one fails
max-parallel: 6 # Limit concurrent jobs
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.10', '3.11', '3.12']
include:
# Add specific combinations with extra variables
- os: ubuntu-latest
python-version: '3.12'
experimental: true
coverage: true
exclude:
# Skip expensive macOS + old Python combinations
- os: macos-latest
python-version: '3.10'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- run: pip install -e ".[test]"
- run: pytest --cov=${{ matrix.coverage && '--cov-report=xml' || '' }}
- if: matrix.coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
For dynamic matrices generated at runtime:
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- id: set-matrix
run: |
# Generate matrix from changed directories
CHANGED=$(git diff --name-only HEAD~1 | grep '^services/' | cut -d/ -f2 | sort -u)
MATRIX=$(echo "$CHANGED" | jq -R -s -c 'split("\n") | map(select(. != "")) | {service: .}')
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
test:
needs: generate-matrix
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- run: cd services/${{ matrix.service }} && make test
Reusable Workflows
Reusable workflows let you define a workflow once and call it from multiple other workflows — within the same repository, across repositories in an organization, or from any public repository. They're the functions of GitHub Actions.
# .github/workflows/reusable-deploy.yml (the reusable workflow)
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image_tag:
required: true
type: string
replicas:
required: false
type: number
default: 3
secrets:
KUBE_CONFIG:
required: true
SLACK_WEBHOOK:
required: false
outputs:
deployment_url:
description: "The deployment URL"
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Deploy
id: deploy
run: |
kubectl set image deployment/app app=${{ inputs.image_tag }}
kubectl scale deployment/app --replicas=${{ inputs.replicas }}
URL=$(kubectl get ingress app -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo "url=https://$URL" >> "$GITHUB_OUTPUT"
- name: Notify Slack
if: secrets.SLACK_WEBHOOK != ''
run: |
curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-H 'Content-Type: application/json' \
-d '{"text":"Deployed to ${{ inputs.environment }}: ${{ steps.deploy.outputs.url }}"}'
# .github/workflows/release.yml (the caller workflow)
name: Release
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
deploy-staging:
needs: build
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
image_tag: ${{ needs.build.outputs.image_tag }}
replicas: 2
secrets:
KUBE_CONFIG: ${{ secrets.STAGING_KUBE_CONFIG }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
deploy-production:
needs: [build, deploy-staging]
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
image_tag: ${{ needs.build.outputs.image_tag }}
replicas: 5
secrets:
KUBE_CONFIG: ${{ secrets.PROD_KUBE_CONFIG }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
Composite Actions
Composite actions bundle multiple steps into a single reusable action. Unlike reusable workflows (which create separate workflow runs), composite actions execute inline within the calling job — sharing the same runner, filesystem, and environment variables.
# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Install dependencies, configure environment, and warm caches'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
install-command:
description: 'Package install command'
required: false
default: 'npm ci'
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 }}
- name: Cache node_modules
id: cache
uses: actions/cache@v4
with:
path: node_modules
key: deps-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: ${{ inputs.install-command }}
- name: Verify installation
shell: bash
run: |
echo "Node version: $(node --version)"
echo "NPM version: $(npm --version)"
echo "Dependencies installed: $(ls node_modules | wc -l) packages"
Use composite actions when you want to share steps that run within a single job. Use reusable workflows when you need separate jobs with their own runners, environments, and concurrency controls.
OIDC Authentication
OpenID Connect (OIDC) eliminates the need for long-lived cloud credentials in your repository secrets. Instead, workflows request short-lived tokens directly from cloud providers, authenticated by GitHub's OIDC provider. This is the gold standard for GitHub Actions authentication to AWS, Azure, and GCP.
sequenceDiagram
participant W as GitHub Workflow
participant G as GitHub OIDC Provider
participant C as Cloud Provider (AWS/Azure/GCP)
participant R as Cloud Resources
W->>G: Request OIDC token
G->>W: JWT with claims (repo, branch, workflow)
W->>C: Present JWT + assume role
C->>C: Validate JWT signature & claims
C->>W: Short-lived credentials (15 min)
W->>R: Access resources with temp credentials
# OIDC with AWS
name: Deploy to AWS
on:
push:
branches: [main]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
aws-region: us-east-1
# No access keys needed!
- name: Deploy to S3
run: aws s3 sync ./dist s3://my-bucket/
# OIDC with Azure
- name: Azure Login via OIDC
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# Uses federated identity credential — no client secret needed
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v3
with:
app-name: my-web-app
package: ./dist
Self-Hosted Runners
Self-hosted runners are machines you manage that connect to GitHub to execute workflow jobs. Use them when you need specific hardware (GPUs, ARM processors), network access (internal services, databases), persistent state (large dependencies), or compliance requirements (data residency).
# Using self-hosted runners with labels
jobs:
gpu-tests:
runs-on: [self-hosted, linux, gpu, cuda-12]
steps:
- uses: actions/checkout@v4
- run: python -m pytest tests/gpu/ --gpu-device=0
deploy-internal:
runs-on: [self-hosted, linux, production-vpc]
steps:
- uses: actions/checkout@v4
- run: kubectl apply -f k8s/
Actions Runner Controller (ARC) for Kubernetes autoscales runners based on workflow demand:
# Kubernetes: RunnerDeployment with autoscaling
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: ci-runners
spec:
replicas: 1
template:
spec:
repository: my-org/my-repo
labels:
- self-hosted
- linux
- k8s
ephemeral: true
---
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
name: ci-runners-autoscaler
spec:
scaleTargetRef:
kind: RunnerDeployment
name: ci-runners
minReplicas: 1
maxReplicas: 20
scaleUpTriggers:
- githubEvent:
workflowJob: {}
duration: "30m"
Secrets & Environments
GitHub provides three levels of secrets: repository (accessible by all workflows in a repo), environment (accessible only in jobs targeting that environment), and organization (shared across repos with policy controls).
# Environment-specific deployment with protection rules
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v4
- name: Deploy
id: deploy
env:
# Environment secrets are only available in jobs
# targeting the 'production' environment
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: |
./deploy.sh
echo "url=https://app.example.com" >> "$GITHUB_OUTPUT"
Environment protection rules allow you to require:
- Required reviewers — specific people must approve before deployment
- Wait timer — delay between approval and execution (0-43200 minutes)
- Branch restrictions — only allow deployments from specific branches
- Custom rules — call external APIs to validate deployment readiness
Caching & Artifacts
Caching and artifacts serve different purposes: caches persist data between workflow runs (dependencies, build caches), while artifacts pass data between jobs within a single run or preserve outputs for download.
# Advanced caching with fallback restore keys
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
~/.gradle/configuration-cache
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/gradle.lockfile') }}
restore-keys: |
gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
gradle-${{ runner.os }}-
# Passing build artifacts between jobs
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 1
if-no-files-found: error
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- run: aws s3 sync dist/ s3://my-bucket/
Advanced Patterns
Production workflows require patterns beyond basic build-test-deploy. Here are the most useful advanced patterns:
Conditional Execution
# Skip jobs based on commit message, changed files, or context
jobs:
deploy:
if: |
github.event_name == 'push' &&
github.ref == 'refs/heads/main' &&
!contains(github.event.head_commit.message, '[skip deploy]')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy only if source changed
if: steps.changes.outputs.src == 'true'
run: ./deploy.sh
Concurrency Control
# Cancel in-progress deployments when new ones start
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
# Queue deployments (don't cancel, wait in line)
concurrency:
group: production-deploy
cancel-in-progress: false
Job Outputs & Dependent Jobs
jobs:
version:
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.bump.outputs.version }}
should_release: ${{ steps.check.outputs.release }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: check
run: |
if git log --oneline HEAD~1..HEAD | grep -q "feat\|fix"; then
echo "release=true" >> "$GITHUB_OUTPUT"
else
echo "release=false" >> "$GITHUB_OUTPUT"
fi
- id: bump
if: steps.check.outputs.release == 'true'
run: |
NEW_VERSION=$(npm version patch --no-git-tag-version | tr -d 'v')
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
release:
needs: version
if: needs.version.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- run: echo "Releasing v${{ needs.version.outputs.new_version }}"
Performance Optimization
Workflow execution time directly impacts developer productivity. A 10-minute CI pipeline that runs 50 times daily consumes 8+ hours of waiting time. Here are proven strategies to reduce pipeline duration:
1. Path Filtering — Only run jobs when relevant files change. Use paths: in triggers or dorny/paths-filter action for per-job filtering.
2. Dependency Caching — Cache node_modules, ~/.gradle, ~/.cache/pip between runs. First run is slow; subsequent runs skip installation entirely.
3. Job Parallelization — Split test suites across parallel jobs. Use matrix builds or test-splitting tools like junit-split.
4. Larger Runners — For CPU-bound tasks (compilation, bundling), a 4-core runner can be 3x faster than the standard 2-core runner.
5. Docker Layer Caching — Use docker/build-push-action with cache-from and cache-to to reuse Docker build layers.
# Docker layer caching with GitHub Actions cache backend
- uses: docker/build-push-action@v5
with:
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
# Path-based job filtering
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
infra:
- 'terraform/**'
- name: Run backend tests
if: steps.changes.outputs.backend == 'true'
run: cd backend && make test
Security Best Practices
GitHub Actions workflows run code with access to your secrets, source code, and deployment infrastructure. A single misconfiguration can expose credentials or allow supply-chain attacks.
# Principle of Least Privilege: restrict GITHUB_TOKEN permissions
permissions:
contents: read # Read-only access to repo contents
pull-requests: write # Only what's needed for PR comments
# All other permissions are 'none'
# For workflows that only read code:
permissions: read-all
# For workflows that need no token at all:
permissions: {}
Critical Security Rules:
- Pin actions to SHA — Never use
@mainor mutable tags for third-party actions - Use
permissions:— Always declare minimal permissions at the workflow or job level - Avoid
pull_request_targetwith checkout — This gives fork PRs access to your secrets - Never echo secrets — GitHub redacts known secrets, but transformed values may leak
- Restrict environment access — Use branch protection + environment protection rules together
- Audit third-party actions — Review source code before using. Prefer official (actions/) or verified publishers
# DANGEROUS: Never do this — exposes secrets to fork PRs
on:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # UNSAFE!
- run: npm test
env:
API_KEY: ${{ secrets.API_KEY }} # Accessible to attacker code!
# SAFE: Separate privileged operations from untrusted code
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- run: npm test # No secrets access, runs on merge commit
Exercises
Create a complete CI workflow with three stages: lint → test (matrix: Node 18, 20, 22 on Ubuntu and macOS) → build. Use caching for node_modules, upload test coverage as artifacts, and configure concurrency to cancel in-progress runs on the same branch.
Build a reusable workflow (workflow_call) that accepts environment name, Docker image tag, and replica count as inputs. It should deploy to Kubernetes using OIDC authentication (no static secrets). Create a caller workflow that deploys to staging automatically and production with manual approval.
Design a workflow for a monorepo with frontend/, backend/, and infrastructure/ directories. Each should only build/test when its files change. Use a dynamic matrix generated from git diff to determine which services to test.
Take an existing workflow and apply all security best practices: pin all actions to SHA, add explicit permissions: block with least privilege, replace static cloud credentials with OIDC, add environment protection rules, and configure CODEOWNERS for the .github/ directory.