Understanding GitHub Actions for CI/CD

GitHub Actions

What is GitHub Actions?

GitHub Actions is a CI/CD and automation platform built directly into GitHub. It lets you automate workflows — build, test, scan, deploy — triggered by any GitHub event.

WITHOUT GitHub Actions: WITH GitHub Actions:
─────────────────────── ───────────────────
Developer pushes code Developer pushes code
↓ ↓
Manually run tests GitHub automatically:
↓ ├── runs tests
Manually build image ├── builds image
↓ ├── scans for vulnerabilities
Manually deploy ├── deploys to staging
↓ └── notifies team
Hope nothing broke
All in minutes, automatically

Core Concepts

┌─────────────────────────────────────────────────────────────┐
│ GITHUB ACTIONS │
│ │
│ Event (trigger) │
│ └──▶ Workflow (.github/workflows/deploy.yml) │
│ └──▶ Job (runs on a runner) │
│ └──▶ Steps │
│ ├── Action (reusable unit) │
│ └── Shell command (run:) │
└─────────────────────────────────────────────────────────────┘
ConceptWhat it is
WorkflowA YAML file defining automation (.github/workflows/)
EventWhat triggers the workflow (push, PR, schedule, etc.)
JobA set of steps that run on one runner
StepA single task — action or shell command
ActionA reusable, pre-built step from marketplace
RunnerThe machine that executes jobs (GitHub-hosted or self-hosted)
SecretEncrypted variable stored in GitHub settings
ArtifactFiles saved from a workflow run
CacheSaved dependencies to speed up workflows

Workflow File Structure

# .github/workflows/ci.yml
# ── Workflow name ─────────────────────────────────────────────
name: CI Pipeline
# ── Triggers ─────────────────────────────────────────────────
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 8 * * 1-5' # weekdays at 8 AM UTC
workflow_dispatch: # manual trigger button
# ── Environment variables ─────────────────────────────────────
env:
APP_NAME: myapp
REGISTRY: ghcr.io
# ── Jobs ─────────────────────────────────────────────────────
jobs:
# Each key is a job ID
build:
name: Build and Test # display name
runs-on: ubuntu-latest # runner type
# Job-level environment
env:
NODE_ENV: test
steps:
# Each step has a name and action or run command
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/

Triggers (Events)

on:
# ── Code events ───────────────────────────────────────────
push:
branches:
- main
- 'release/**' # wildcard
tags:
- 'v*' # version tags
paths:
- 'src/**' # only when these files change
- '!docs/**' # exclude docs changes
pull_request:
types:
- opened
- synchronize # new commits pushed
- reopened
branches:
- main
pull_request_review:
types:
- submitted
# ── Manual triggers ───────────────────────────────────────
workflow_dispatch:
inputs:
environment:
description: 'Deploy to environment'
required: true
type: choice
options:
- dev
- staging
- production
debug:
description: 'Enable debug logging'
type: boolean
default: false
# ── Scheduled ────────────────────────────────────────────
schedule:
- cron: '0 2 * * *' # daily at 2 AM
- cron: '0 8 * * 1' # monday at 8 AM
# ── Other workflows ───────────────────────────────────────
workflow_call: # called by another workflow
inputs:
version:
type: string
required: true
secrets:
DEPLOY_KEY:
required: true
# ── Repository events ─────────────────────────────────────
release:
types: [published]
issues:
types: [opened, labeled]
issue_comment:
types: [created]

Runners

jobs:
build:
# ── GitHub-hosted runners ─────────────────────────────
runs-on: ubuntu-latest # Ubuntu 22.04
runs-on: ubuntu-22.04 # specific version
runs-on: windows-latest # Windows Server 2022
runs-on: macos-latest # macOS 13
# ── Self-hosted runners ───────────────────────────────
runs-on: self-hosted
runs-on: [self-hosted, linux, x64]
runs-on: [self-hosted, linux, gpu]
# ── Matrix of runners ────────────────────────────────
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}

GitHub-Hosted Runner Specs

RunnerCPURAMStorage
ubuntu-latest2 cores7 GB14 GB
windows-latest2 cores7 GB14 GB
macos-latest3 cores14 GB14 GB
ubuntu-latest (larger)4-64 cores16-256 GBpaid

Steps — Actions vs Run

steps:
# ── Using a pre-built Action ─────────────────────────────────
- name: Checkout repository
uses: actions/checkout@v4 # GitHub's official action
with:
fetch-depth: 0 # fetch all history
# ── Running shell commands ────────────────────────────────────
- name: Build application
run: |
echo "Building..."
npm run build
echo "Done"
# ── Multi-line with specific shell ────────────────────────────
- name: Run script
shell: bash
run: |
set -euo pipefail
echo "Error-safe script"
./deploy.sh
# ── Python script ─────────────────────────────────────────────
- name: Run Python
run: |
python3 -c "
import json
data = {'status': 'ok'}
print(json.dumps(data))
"
# ── Set environment variables for next steps ─────────────────
- name: Set variables
run: |
echo "VERSION=1.2.3" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +%Y-%m-%d)" >> $GITHUB_ENV
- name: Use variables
run: echo "Version is $VERSION built on $BUILD_DATE"
# ── Set step outputs ─────────────────────────────────────────
- name: Get version
id: version
run: echo "tag=v1.2.3" >> $GITHUB_OUTPUT
- name: Use output
run: echo "Tag is ${{ steps.version.outputs.tag }}"
# ── Conditional steps ─────────────────────────────────────────
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./deploy-prod.sh
- name: Notify on failure
if: failure()
run: ./notify-team.sh
- name: Always cleanup
if: always()
run: ./cleanup.sh
# ── Continue on error ─────────────────────────────────────────
- name: Optional check
continue-on-error: true
run: ./optional-scan.sh
# ── Timeout ───────────────────────────────────────────────────
- name: Long running test
timeout-minutes: 30
run: ./integration-tests.sh

Jobs — Dependencies and Parallelism

jobs:
# ── Parallel jobs (run at same time) ──────────────────────
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit
# ── Sequential job (waits for others) ────────────────────
build:
runs-on: ubuntu-latest
needs: [lint, test, security] # waits for all three
steps:
- uses: actions/checkout@v4
- run: npm run build
# ── Deploy (waits for build) ──────────────────────────────
deploy-staging:
runs-on: ubuntu-latest
needs: build
steps:
- run: ./deploy.sh staging
deploy-prod:
runs-on: ubuntu-latest
needs: deploy-staging # sequential deploys
steps:
- run: ./deploy.sh production
# Execution flow:
# lint ──┐
# test ──┼──▶ build ──▶ deploy-staging ──▶ deploy-prod
# security ──┘

Job Outputs

jobs:
get-version:
runs-on: ubuntu-latest
outputs: # declare outputs
version: ${{ steps.ver.outputs.version }}
sha: ${{ steps.ver.outputs.sha }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: ver
run: |
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
echo "sha=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
build:
needs: get-version
runs-on: ubuntu-latest
steps:
- name: Use version
run: |
echo "Building version ${{ needs.get-version.outputs.version }}"
echo "SHA: ${{ needs.get-version.outputs.sha }}"

Matrix Builds

Test across multiple versions/platforms simultaneously:

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # don't cancel others if one fails
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python: ['3.10', '3.11', '3.12']
exclude:
- os: windows-latest
python: '3.10' # skip this combination
include:
- os: ubuntu-latest
python: '3.12'
experimental: true # extra combination
steps:
- uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Run tests
run: pytest tests/
# Creates 8 parallel jobs:
# ubuntu + 3.10, 3.11, 3.12
# windows + 3.11, 3.12 (3.10 excluded)
# macos + 3.10, 3.11, 3.12

Secrets and Variables

# Using secrets (encrypted)
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: ./deploy.sh
# Using variables (not encrypted)
- name: Configure
env:
APP_ENV: ${{ vars.APP_ENV }}
REGION: ${{ vars.AWS_REGION }}
run: ./configure.sh
# Built-in secrets
- name: Push to registry
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io -u ${{ github.actor }} --password-stdin

Secret Scopes

Organization secrets → available to all repos in org
Repository secrets → available to one repo
Environment secrets → available only in that environment
Settings → Secrets and variables → Actions

Environments and Approvals

jobs:
deploy-prod:
runs-on: ubuntu-latest
environment:
name: production # must be configured in Settings
url: https://myapp.com # shown in GitHub UI
steps:
- name: Deploy
run: ./deploy.sh production
# In GitHub Settings → Environments → production:
# ✅ Required reviewers: [senior-devops, team-lead]
# ✅ Wait timer: 5 minutes
# ✅ Deployment branches: main only
# → Pipeline pauses and sends approval request
# → Deployment only proceeds after approval

Caching Dependencies

steps:
# ── Node.js / npm ─────────────────────────────────────────────
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # built-in cache
# ── Manual cache control ──────────────────────────────────────
- name: Cache node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# ── Python / pip ──────────────────────────────────────────────
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
# ── Go modules ───────────────────────────────────────────────
- uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true
# ── Docker layers ─────────────────────────────────────────────
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-

Artifacts

steps:
# ── Upload artifact ───────────────────────────────────────────
- name: Build
run: npm run build
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7 # keep for 7 days
# ── Upload test results ───────────────────────────────────────
- name: Upload test results
uses: actions/upload-artifact@v4
if: always() # upload even if tests fail
with:
name: test-results
path: |
test-results/
coverage/
*.xml
# ── Download in another job ───────────────────────────────────
jobs:
build:
steps:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: app-binary
path: bin/app
deploy:
needs: build
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: app-binary
path: bin/
- name: Deploy
run: ./deploy.sh bin/app

Real-World Workflows

Full CI/CD Pipeline
# .github/workflows/cicd.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/${{ github.repository }}
jobs:
# ── 1. Code Quality ────────────────────────────────────────
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- run: pip install -r requirements-dev.txt
- name: Lint
run: |
flake8 src/
black --check src/
isort --check src/
- name: Type check
run: mypy src/
# ── 2. Tests ──────────────────────────────────────────────
test:
name: Tests
runs-on: ubuntu-latest
needs: quality
services: # spin up dependencies
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- run: pip install -r requirements.txt
- name: Run tests with coverage
env:
DATABASE_URL: postgresql://postgres:testpass@localhost/testdb
REDIS_URL: redis://localhost:6379
run: |
pytest tests/ \
--cov=src \
--cov-report=xml \
--cov-report=html \
--junitxml=test-results.xml \
-v
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: coverage.xml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results.xml
# ── 3. Security Scanning ──────────────────────────────────
security:
name: Security
runs-on: ubuntu-latest
needs: quality
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- name: Dependency audit
run: pip-audit -r requirements.txt
- name: SAST scan
uses: github/codeql-action/init@v3
with:
languages: python
- uses: github/codeql-action/analyze@v3
- name: Secret scanning
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
# ── 4. Build Image ────────────────────────────────────────
build:
name: Build Image
runs-on: ubuntu-latest
needs: [test, security]
permissions:
contents: read
packages: write
outputs:
image-digest: ${{ steps.build.outputs.digest }}
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=sha,prefix={{branch}}-
type=ref,event=branch
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.sha }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
- name: Scan image for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE }}:${{ github.sha }}
format: sarif
output: trivy.sarif
severity: CRITICAL,HIGH
exit-code: '1'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy.sarif
# ── 5. Deploy Staging ─────────────────────────────────────
deploy-staging:
name: Deploy Staging
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
environment:
name: staging
url: https://staging.myapp.com
steps:
- uses: actions/checkout@v4
- name: Auth to GCP
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.GKE_SA }}
- name: Get GKE credentials
uses: google-github-actions/get-gke-credentials@v2
with:
cluster_name: staging-cluster
location: us-central1
- name: Deploy to staging
run: |
kubectl set image deployment/api \
api=${{ env.IMAGE }}@${{ needs.build.outputs.image-digest }} \
-n staging
kubectl rollout status deployment/api -n staging
- name: Run smoke tests
run: |
sleep 30
curl -f https://staging.myapp.com/health
# ── 6. Deploy Production ──────────────────────────────────
deploy-prod:
name: Deploy Production
runs-on: ubuntu-latest
needs: deploy-staging
environment:
name: production # requires approval
url: https://myapp.com
steps:
- uses: actions/checkout@v4
- name: Auth to GCP
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.GKE_PROD_SA }}
- name: Get GKE credentials
uses: google-github-actions/get-gke-credentials@v2
with:
cluster_name: prod-cluster
location: us-central1
- name: Deploy to production
run: |
kubectl set image deployment/api \
api=${{ env.IMAGE }}@${{ needs.build.outputs.image-digest }} \
-n production
kubectl rollout status deployment/api -n production \
--timeout=5m
- name: Verify deployment
run: |
sleep 60
curl -f https://myapp.com/health
- name: Notify success
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "✅ Deployed to production: ${{ github.sha }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
- name: Rollback on failure
if: failure()
run: |
kubectl rollout undo deployment/api -n production
echo "🔴 Rolled back production deployment"

Reusable Workflows

# .github/workflows/reusable-deploy.yml
# Called by other workflows
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
secrets:
DEPLOY_KEY:
required: true
outputs:
deployment-url:
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- name: Deploy
id: deploy
run: |
./deploy.sh ${{ inputs.environment }} ${{ inputs.image-tag }}
echo "url=https://${{ inputs.environment }}.myapp.com" >> $GITHUB_OUTPUT
---
# .github/workflows/main.yml
# Calls the reusable workflow
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
image-tag: ${{ needs.build.outputs.tag }}
secrets:
DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
deploy-prod:
needs: deploy-staging
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
image-tag: ${{ needs.build.outputs.tag }}
secrets:
DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}

Composite Actions

# .github/actions/setup-app/action.yml
# Custom reusable action
name: Setup Application
description: Install and configure the application
inputs:
python-version:
description: Python version
required: false
default: '3.12'
install-dev:
description: Install dev dependencies
required: false
default: 'false'
outputs:
cache-hit:
description: Whether cache was hit
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: composite
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- name: Cache dependencies
id: cache
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ inputs.python-version }}-${{ hashFiles('requirements*.txt') }}
- name: Install dependencies
shell: bash
run: |
pip install -r requirements.txt
if [ "${{ inputs.install-dev }}" == "true" ]; then
pip install -r requirements-dev.txt
fi
---
# Use in workflow
steps:
- uses: ./.github/actions/setup-app
with:
python-version: '3.12'
install-dev: 'true'

Expressions and Context

# GitHub Contexts
${{ github.sha }} # commit SHA
${{ github.ref }} # branch/tag ref
${{ github.event_name }} # push, pull_request, etc
${{ github.actor }} # user who triggered
${{ github.repository }} # owner/repo
${{ github.run_id }} # unique run ID
${{ github.run_number }} # sequential run number
${{ runner.os }} # Linux, Windows, macOS
${{ runner.temp }} # temp directory path
${{ secrets.MY_SECRET }} # encrypted secret
${{ vars.MY_VAR }} # variable (not encrypted)
${{ env.MY_ENV_VAR }} # environment variable
${{ steps.my-step.outputs.key }} # step output
${{ needs.my-job.outputs.key }} # job output
${{ needs.my-job.result }} # success/failure/etc
# Expressions
${{ 2 + 2 }} # math
${{ 'hello' == 'hello' }} # comparison
${{ contains(github.ref, 'main') }}
${{ startsWith(github.ref, 'refs/tags/') }}
${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
# Conditionals
if: github.ref == 'refs/heads/main'
if: failure()
if: success()
if: always()
if: cancelled()
if: ${{ github.event.inputs.environment == 'production' }}

Popular Marketplace Actions

# Source control
- uses: actions/checkout@v4
- uses: actions/upload-artifact@v4
- uses: actions/download-artifact@v4
- uses: actions/cache@v4
# Language setup
- uses: actions/setup-node@v4
- uses: actions/setup-python@v5
- uses: actions/setup-go@v5
- uses: actions/setup-java@v4
# Docker
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
- uses: docker/build-push-action@v5
- uses: docker/metadata-action@v5
# Cloud providers
- uses: google-github-actions/auth@v2
- uses: google-github-actions/get-gke-credentials@v2
- uses: azure/login@v1
- uses: aws-actions/configure-aws-credentials@v4
# Security
- uses: aquasecurity/trivy-action@master
- uses: bridgecrewio/checkov-action@master
- uses: github/codeql-action/analyze@v3
- uses: snyk/actions/node@master
# Notifications
- uses: slackapi/slack-github-action@v1
- uses: peter-evans/create-issue-from-file@v5
# Terraform
- uses: hashicorp/setup-terraform@v3
- uses: dflook/terraform-plan@v1
- uses: dflook/terraform-apply@v1

Debugging Workflows

# Enable debug logging
# Set secret: ACTIONS_STEP_DEBUG = true
# Set secret: ACTIONS_RUNNER_DEBUG = true
steps:
# Debug — print all context
- name: Dump context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
RUNNER_CONTEXT: ${{ toJson(runner) }}
run: |
echo "GitHub context:"
echo "$GITHUB_CONTEXT"
echo "Runner context:"
echo "$RUNNER_CONTEXT"
# SSH into runner for debugging
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: failure() # only on failure
timeout-minutes: 15
# Print environment
- name: Debug environment
run: |
echo "PATH=$PATH"
echo "PWD=$PWD"
env | sort
ls -la

Cost and Limits

GitHub-hosted runner minutes (free tier):
├── Public repos: Unlimited free
├── Private repos:
│ ├── Free plan: 2,000 min/month
│ ├── Pro: 3,000 min/month
│ ├── Team: 3,000 min/month
│ └── Enterprise: 50,000 min/month
Minute multipliers:
├── Linux: 1x (1 minute = 1 minute)
├── Windows: 2x (1 minute = 2 minutes)
├── macOS: 10x (1 minute = 10 minutes)
Other limits:
├── Max workflow run time: 35 days
├── Max job run time: 6 hours
├── Max jobs per workflow: 500
├── Max concurrent jobs: depends on plan
├── Artifact storage: 500 MB (free), 2 GB (Pro)
└── Cache storage: 10 GB per repo

Best Practices

Security:
✅ Pin action versions to SHA (not @v1 tag)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
✅ Use OIDC instead of long-lived credentials
✅ Minimal permissions on GITHUB_TOKEN
✅ Never echo secrets in logs
✅ Use environment secrets for production
Performance:
✅ Cache dependencies aggressively
✅ Run jobs in parallel where possible
✅ Use path filters to skip unnecessary runs
✅ Fail fast on cheap checks (lint before test)
✅ Use Docker layer caching for builds
Maintainability:
✅ Use reusable workflows for repeated patterns
✅ Use composite actions for complex step groups
✅ Keep workflows focused — one workflow per concern
✅ Use concurrency groups to cancel stale runs
✅ Add workflow_dispatch for manual control
Reliability:
✅ Set timeouts on all jobs and steps
✅ Always upload test results even on failure
✅ Use continue-on-error for non-blocking checks
✅ Add rollback steps on deployment failure
✅ Test workflows in feature branches
# Concurrency — cancel stale runs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # cancel old run when new one starts
# Minimal permissions
permissions:
contents: read # default — read only
packages: write # only what you need
security-events: write

GitHub Actions is the glue that holds modern DevOps together — it connects your code to every tool in your stack, automates every repetitive task, and enforces quality gates without slowing down your team. The YAML-based workflow model is powerful enough to handle any automation need while staying readable and maintainable.

Leave a Reply