- Published on
GitHub Actions in Production — Reusable Workflows, OIDC Auth, and Cutting Build Times
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
GitHub Actions has evolved from a simple CI/CD tool into a comprehensive automation platform. Many teams still treat it as a black box—copying workflow files and hoping they work. In production environments, you need reproducibility, security, and speed. This post explores advanced patterns that power enterprise deployments: reusable workflows that enforce consistency across repositories, OIDC tokens that eliminate long-lived secrets, dynamic matrix builds that scale horizontally, and caching strategies that cut build times in half.
- Reusable Workflows with workflow_call
- OIDC Authentication for AWS (Zero Secrets)
- Dynamic Matrix Builds from JSON
- Caching Strategies
- Environment Protection and Branch Protection
- Concurrency Groups
- Self-Hosted Runners for Private Resources
- Checklist
- Conclusion
Reusable Workflows with workflow_call
Reusable workflows are the DRY principle applied to CI/CD. Instead of copy-pasting workflows across repos, define once and call from multiple places. This ensures all services follow the same quality gates.
Create .github/workflows/shared-build.yml in a central repository (e.g., org/shared-ci):
name: Build and Test
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '20'
run-e2e:
required: false
type: boolean
default: false
secrets:
npm-token:
required: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run type-check
- name: Unit tests
run: npm run test:unit -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
flags: unittests
- name: Build
run: npm run build
- name: E2E tests
if: ${{ inputs.run-e2e }}
run: npm run test:e2e
- name: Archive artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
Call from your service repository:
name: CI
on:
push:
branches: [main, develop]
pull_request:
jobs:
test:
uses: org/shared-ci/.github/workflows/shared-build.yml@main
with:
node-version: '20'
run-e2e: true
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
OIDC Authentication for AWS (Zero Secrets)
Store secrets in GitHub and you risk exposure. OIDC (OpenID Connect) tokens are short-lived and scoped to specific runs. GitHub Actions can mint these tokens and AWS exchanges them for temporary credentials.
Configure AWS OIDC provider:
# Run once in your AWS account
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
# Create role with trust policy
cat > trust-policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:org/service:*"
}
}
}
]
}
EOF
aws iam create-role \
--role-name github-actions-deploy \
--assume-role-policy-document file://trust-policy.json
Use OIDC in your workflow:
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Assume AWS role
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-deploy
aws-region: us-east-1
role-duration-seconds: 900
- name: Deploy
run: |
aws s3 sync dist/ s3://my-bucket/
aws cloudfront create-invalidation \
--distribution-id E123ABC \
--paths "/*"
Dynamic Matrix Builds from JSON
Run tests across multiple Node versions, databases, or configurations. Matrix builds discover test combinations from your repo metadata.
name: Test Matrix
on: [push, pull_request]
jobs:
setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- id: set
run: |
MATRIX=$(jq -c '{
"node-version": ["18", "20", "22"],
"postgres-version": ["14", "15", "16"],
"exclude": [
{"node-version": "18", "postgres-version": "16"}
]
}' < .github/matrix.json)
echo "matrix=$(echo $MATRIX)" >> $GITHUB_OUTPUT
test:
needs: setup
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
services:
postgres:
image: postgres:${{ matrix.postgres-version }}
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/test
Caching Strategies
Caching is the fastest build—caching nothing is slower than any CI system. Layer your caches: dependencies, build output, and Docker images.
Node.js dependencies cache (built-in with setup-node):
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: 'package-lock.json'
Docker layer caching with buildx:
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: my-registry/my-image:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Environment Protection and Branch Protection
Enforce approvals before production deployments. Combine GitHub's environment protection with required status checks.
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://api.example.com
steps:
- uses: actions/checkout@v4
- name: Deploy
run: |
echo "Deploying to production..."
npm run deploy:prod
Configure in repository settings → Environments → production:
- Required reviewers: select 2 team members
- Deployment branches: allow only main
- Protection rules: require branch protection
Concurrency Groups
Cancel outdated workflow runs automatically. When a new commit pushes, cancel previous runs on the same branch.
name: CI
on:
push:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
Self-Hosted Runners for Private Resources
Cloud runners cost money. Self-hosted runners access internal databases, private NPM registries, and on-premises systems.
Register a runner in your infrastructure:
# On your machine
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.313.0.tar.gz \
-L https://github.com/actions/runner/releases/download/v2.313.0/actions-runner-linux-x64-2.313.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.313.0.tar.gz
./config.sh --url https://github.com/org/repo --token YOUR_TOKEN --runnergroup "default" --labels "private,database"
./run.sh
Use in workflows:
jobs:
integration-test:
runs-on: [self-hosted, private, database]
steps:
- uses: actions/checkout@v4
- run: npm run test:db
Checklist
- Audit all secrets in existing workflows; migrate to OIDC where possible
- Extract common build steps into reusable workflows
- Enable concurrency with cancel-in-progress for branch PRs
- Set up dynamic matrix builds for dependency version testing
- Configure environment protection for production deployments
- Implement Docker layer caching with buildx
- Enable branch protection requiring passing checks
- Test self-hosted runner setup on internal network
- Document runner installation and upgrade process
- Monitor GitHub Actions usage and costs
Conclusion
GitHub Actions scales when you treat workflows as code, eliminate secrets through OIDC, and cache aggressively. Reusable workflows prevent drift across your organization. Dynamic matrices catch compatibility issues early. Self-hosted runners unlock access to private systems. Start by migrating one workflow to OIDC auth and adding concurrency groups—you'll see immediate benefits in build speed and security posture.