Skip to content

GitHub Actions

First PublishedByAtif Alam

GitHub Actions is GitHub’s built-in CI/CD platform. Workflows are defined as YAML files in your repository and run on GitHub-hosted or self-hosted runners. Its tight integration with GitHub (PRs, issues, releases, packages) and a large marketplace of reusable actions make it the most popular CI/CD tool for GitHub-hosted projects.

Event (push, PR, schedule, ...)
Workflow (.github/workflows/ci.yml)
├── Job: build (runs on ubuntu-latest)
│ ├── Step: actions/checkout@v4
│ ├── Step: actions/setup-node@v4
│ ├── Step: npm ci
│ └── Step: npm run build
└── Job: test (runs on ubuntu-latest, needs: build)
├── Step: actions/checkout@v4
├── Step: npm ci
└── Step: npm test
ConceptWhat It Is
EventWhat triggers the workflow (push, pull_request, schedule, etc.)
WorkflowA YAML file in .github/workflows/ that defines the automation
JobA set of steps that run on the same runner; jobs run in parallel by default
StepA single action or shell command within a job
RunnerThe machine that executes a job (GitHub-hosted VM or self-hosted)
ActionA reusable unit of work (from the marketplace or custom)
.github/workflows/ci.yml
name: CI # Workflow name (shown in UI)
on: # Trigger(s)
push:
branches: [main]
pull_request:
branches: [main]
permissions: # Least-privilege token permissions
contents: read
env: # Workflow-level environment variables
NODE_ENV: production
jobs:
build: # Job ID
name: Build and Test # Display name
runs-on: ubuntu-latest # Runner
timeout-minutes: 15 # Kill job if it takes too long
steps:
- uses: actions/checkout@v4 # Step using a marketplace action
- uses: actions/setup-node@v4 # Setup Node.js
with:
node-version: 20
cache: npm # Built-in dependency caching
- run: npm ci # Step using a shell command
- run: npm run build
- run: npm test
on:
push:
branches: [main, develop] # Only these branches
paths: ['src/**', 'package.json'] # Only when these files change
tags: ['v*'] # Only version tags
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
schedule:
- cron: '0 6 * * 1' # Every Monday at 6 AM UTC
workflow_dispatch: # Manual trigger (button in UI)
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
type: choice
options: [staging, production]
release:
types: [published] # When a GitHub release is published
TriggerFires WhenCommon Use
pushCode pushed to a branch or tagCI on every commit
pull_requestPR opened, updated, or reopenedValidate before merge
scheduleCron schedule (UTC)Nightly builds, drift detection
workflow_dispatchManual button click or API callProduction deploys, ad-hoc runs
releaseGitHub release created/publishedPublish packages, deploy release
workflow_callCalled by another workflowReusable workflow (see below)
repository_dispatchExternal webhook via APICross-repo triggers, ChatOps
issue_commentComment on an issue or PRChatOps (/deploy, /test)

The GitHub Marketplace has thousands of reusable actions. Common ones:

ActionWhat It Does
actions/checkout@v4Check out your repository
actions/setup-node@v4Install Node.js (also: setup-python, setup-go, setup-java)
actions/cache@v4Cache dependencies (npm, pip, Gradle, etc.)
actions/upload-artifact@v4Upload build artifacts
actions/download-artifact@v4Download artifacts from another job
docker/build-push-action@v6Build and push Docker images
docker/login-action@v3Log in to a container registry
aws-actions/configure-aws-credentials@v4Configure AWS credentials (supports OIDC)
azure/login@v2Log in to Azure (supports OIDC)
hashicorp/setup-terraform@v3Install Terraform
steps:
- uses: actions/checkout@v4 # org/repo@version
- uses: actions/setup-node@v4
with: # Input parameters
node-version: 20
cache: npm
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: myregistry/myapp:${{ github.sha }}

Always pin actions to a version (@v4, or better, a full SHA) to avoid supply-chain attacks.

A composite action is a reusable set of steps (no separate runtime):

.github/actions/setup-and-build/action.yml
name: Setup and Build
description: Install dependencies and build the project
inputs:
node-version:
description: Node.js version
required: false
default: '20'
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
shell: bash
- run: npm run build
shell: bash
# Use in a workflow
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-and-build
with:
node-version: 22
TypeRuntimeBest For
CompositeRuns steps in the workflow runnerGrouping common steps (most common)
JavaScriptNode.jsComplex logic, API calls, fast startup
DockerDocker containerTools that need a specific OS/environment

Reusable workflows let you define an entire workflow that other workflows can call — like a function:

.github/workflows/reusable-deploy.yml
name: Deploy
on:
workflow_call: # This makes it callable
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
secrets:
DEPLOY_TOKEN:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- run: |
echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"
# ... deployment commands using ${{ secrets.DEPLOY_TOKEN }}
# .github/workflows/ci.yml — caller workflow
name: CI
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
- id: tag
run: echo "tag=${{ github.sha }}" >> $GITHUB_OUTPUT
- run: docker build -t myapp:${{ github.sha }} .
deploy-staging:
needs: build
uses: ./.github/workflows/reusable-deploy.yml # Call the reusable workflow
with:
environment: staging
image-tag: ${{ needs.build.outputs.image-tag }}
secrets:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
deploy-production:
needs: deploy-staging
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
image-tag: ${{ needs.build.outputs.image-tag }}
secrets:
DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}

Run the same job across multiple configurations:

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
fail-fast: false # Don't cancel other matrix jobs if one fails
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test

This creates 9 parallel jobs (3 OS x 3 Node versions).

strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20]
exclude:
- os: windows-latest
node: 18 # Skip Node 18 on Windows
include:
- os: ubuntu-latest
node: 22
experimental: true # Add an extra combination with a custom variable
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: |
curl -H "Authorization: Bearer ${{ secrets.API_TOKEN }}" \
https://api.example.com/deploy

Secrets are:

  • Encrypted at rest and masked in logs.
  • Available at the repository, environment, or organization level.
  • Not passed to workflows triggered from forks (security).
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment: # Associate with a GitHub environment
name: production
url: https://myapp.com
steps:
- run: echo "Deploying to production"

In the GitHub UI, configure protection rules for the production environment:

  • Required reviewers — one or more people must approve.
  • Wait timer — delay N minutes before running.
  • Branch restriction — only main can deploy to production.
  • Deployment branches — restrict which branches can target this environment.

OIDC lets workflows authenticate to cloud providers without storing long-lived credentials:

GitHub Actions ──► request short-lived token ──► Cloud Provider (AWS, Azure, GCP)
(using OIDC federation) verifies token, issues temp credentials

This eliminates the need to store AWS access keys or Azure service principal secrets in GitHub.

For cloud-specific OIDC setup, see:

steps:
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-

Many setup-* actions have built-in caching:

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # Automatically caches ~/.npm
# Upload in one job
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
# Download in another job
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/

For jobs that need special hardware, internal network access, or cost savings:

Terminal window
# On your server — download and configure the runner
./config.sh --url https://github.com/myorg/myrepo --token <TOKEN>
./run.sh
# Use in a workflow
jobs:
build:
runs-on: self-hosted # or a custom label
steps:
- uses: actions/checkout@v4
- run: make build

Self-hosted runners can have labels for targeting:

runs-on: [self-hosted, linux, gpu] # Requires all three labels

For Kubernetes, use ARC to auto-scale GitHub Actions runners as pods:

Terminal window
helm install arc \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller \
--namespace arc-systems --create-namespace
name: Node.js CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test -- --coverage
- uses: actions/upload-artifact@v4
if: always()
with:
name: coverage
path: coverage/
name: Docker
on:
push:
branches: [main]
jobs:
build-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
name: Terraform
on:
push:
branches: [main]
paths: ['infra/**']
pull_request:
paths: ['infra/**']
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: infra
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-role
aws-region: us-east-1
- uses: hashicorp/setup-terraform@v3
- run: terraform init
- run: terraform validate
- name: Plan
id: plan
run: terraform plan -no-color -out=tfplan
continue-on-error: true
- name: Comment plan on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const output = `#### Terraform Plan
\`\`\`
${{ steps.plan.outputs.stdout }}
\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});
- name: Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
FeatureGitHub ActionsGitLab CI
Config file.github/workflows/*.yml (multiple).gitlab-ci.yml (single)
StagesImplicit via needsExplicit stages: block
ReuseReusable workflows, composite actionsinclude, extends, templates
MarketplaceLarge action marketplaceSmaller, but include from URLs
RunnersGitHub-hosted + self-hostedShared + group + project runners
EnvironmentsBuilt-in with protection rulesBuilt-in with approval gates
OIDCNative supportNative support
DAGVia needsVia needs keyword
Container registryGitHub Packages (GHCR)Built-in container registry
Best forGitHub-hosted projectsGitLab-hosted projects, all-in-one platform
  • Workflows live in .github/workflows/ and are triggered by events (push, PR, schedule, manual).
  • Actions from the marketplace are the building blocks — always pin to a version.
  • Use reusable workflows to share common pipeline logic across repositories.
  • Matrix builds test across multiple configurations in parallel.
  • OIDC eliminates long-lived cloud credentials — use it for AWS, Azure, and GCP.
  • Environment protection rules enforce approval gates and branch restrictions for production.
  • Self-hosted runners (or ARC on Kubernetes) give you control over hardware and networking.
  • Use permissions to apply least-privilege to the GITHUB_TOKEN.