GitHub Actions & Workflows
Overview
GitHub Actions automates builds, tests, and deployments directly from your repository. Workflows are YAML files stored in .github/workflows/ that trigger on events (push, PR, schedule, manual) and run jobs on hosted or self-hosted runners.
Workflow syntax
Every workflow shares the same YAML structure — a name, triggering events, and a set of jobs. This section covers the building blocks.
File location and naming
Workflows live as YAML files in the .github/workflows/ directory — one file per workflow, each triggering on its own set of events. A typical repo keeps one workflow per concern:
.github/workflows/ci.yml # runs on push/PR
.github/workflows/deploy.yml # runs on merge to main
.github/workflows/scheduled.yml # runs on cron schedule
Skeleton workflow
The minimal structure every workflow shares: a name, the events that trigger it (on), and one or more jobs with steps. This example runs tests on pushes and pull requests to main:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
Triggers (on)
The on key controls when a workflow runs.
Event types
The most common triggers are push, pull request, schedule (cron), manual dispatch, and tag pushes. Use filters to narrow when each event fires:
# Push to any branch
on: push
# Specific branches
on:
push:
branches: [main, develop]
paths:
- "src/**"
- "package.json"
# Pull requests
on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
# Schedule (cron)
on:
schedule:
- cron: "0 8 * * 1-5" # weekdays at 8 AM UTC
# Manual trigger
on:
workflow_dispatch:
inputs:
environment:
description: "Deployment environment"
required: true
type: choice
options: [staging, production]
# Tag push
on:
push:
tags: ["v*"]
# Multiple events
on: [push, pull_request]
Jobs and steps
Jobs are the units of work in a workflow; each job runs on its own runner.
Job definition
A job groups steps that share one runner and its configuration. This example builds and tests a Node.js app, then uploads the output as an artifact for later jobs:
jobs:
build:
name: Build and test
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
shell: bash
working-directory: ./app
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: dist
path: app/dist/
Job dependencies
Use needs to order jobs: a job waits until its dependencies succeed, so test runs after lint and deploy after test. Add if conditions to gate jobs on the branch or context:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lint
test:
needs: lint # runs after lint succeeds
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
Matrix builds
A matrix runs the same job across multiple combinations of values from a single definition — ideal for testing against several Node versions and operating systems:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
os: [ubuntu-latest, windows-latest]
fail-fast: false # don't cancel all on first failure
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
This runs 6 jobs in parallel (3 node versions × 2 operating systems).
Secrets and environment variables
Secrets keep credentials out of your workflow files. GitHub supports three scopes — repository, environment, and OIDC-based cloud credentials — each suited to different situations.
Repository secrets
Repository-level secrets (configured under Settings → Secrets and variables → Actions) are available to every workflow in the repository. Inject them into steps through the env block instead of hard-coding values:
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: ./deploy.sh
Environment-level secrets
Environment-scoped secrets and protection rules let you gate deployments — for example, requiring approval before production runs while keeping production credentials separate from staging:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # requires approval, uses env-specific secrets
steps:
- run: ./deploy.sh
Configure environment protection rules in repo settings: required reviewers, wait timer, deployment branches.
OIDC (no long-lived secrets)
With OpenID Connect, the runner exchanges a short-lived token for cloud credentials, so no long-lived secrets need to be stored in GitHub. This example assumes an AWS IAM role that trusts the GitHub Actions provider:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # required for OIDC
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: us-east-2
- name: Deploy
run: terraform apply -auto-approve
Common workflow patterns
Ready-to-adapt workflows for the most frequent CI/CD tasks.
Node.js CI
A complete Node.js pipeline: checkout, install dependencies with caching, then lint, typecheck, test with coverage, and build — run across a matrix of Node versions:
name: Node.js CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
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 run lint
- run: npm run typecheck
- run: npm test -- --coverage
- run: npm run build
Docker build and push
Build an image with Buildx, tag it from the branch or tag that triggered the run, and push it to a registry. This is the standard pattern for publishing container images with GitHub Actions:
name: Docker
on:
push:
branches: [main]
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
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: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=
- name: Build and push
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
Terraform plan on PR
On pull requests that touch terraform/, run a plan with OIDC credentials and post the output as a PR comment so reviewers can see the change before approving:
name: Terraform Plan
on:
pull_request:
paths:
- "terraform/**"
jobs:
plan:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.TERRAFORM_ROLE_ARN }}
aws-region: us-east-2
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.9"
- name: Terraform fmt
run: terraform fmt -check -recursive
working-directory: terraform
- name: Terraform plan
id: plan
run: |
terraform plan -no-color -out=tfplan 2>&1 | tee plan.txt
echo 'PLAN<<EOF' >> $GITHUB_OUTPUT
cat plan.txt >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
working-directory: terraform
- name: Post plan to PR
uses: actions/github-script@v7
with:
script: |
const output = `## Terraform Plan\n\n\`\`\`hcl\n${{ steps.plan.outputs.PLAN }}\n\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output,
});
Deploy on merge
The simplest production pipeline: every push to main runs a deploy script, gated by the production environment and its protection rules:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
ArgoCD GitOps pipeline (build, push, update)
When an image is pushed, update the image tag in the GitOps repository so ArgoCD picks it up and deploys:
name: Build, Push, and Update GitOps
on:
push:
tags:
- "v*" # Triggers workflow on tag push (e.g., v1.0.0, v2.1.3)
env:
IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/gitops-example-backend
GITOPS_REPO: subaquatic-pierre/gitops-example
GITOPS_BRANCH: main
jobs:
build-push-update:
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
- name: Get tag version
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create Docker Config
env:
DOCKER_CONFIG_CONTENT: ${{ secrets.DOCKER_CONFIG_JSON }}
run: |
mkdir -p ~/.docker
echo "$DOCKER_CONFIG_CONTENT" > ~/.docker/config.json
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.prod
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ env.VERSION }}
${{ env.IMAGE_NAME }}:latest
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache,mode=max
- name: Checkout GitOps Repository
uses: actions/checkout@v4
with:
repository: ${{ env.GITOPS_REPO }}
token: ${{ secrets.GITOPS_PAT }}
path: gitops-repo
- name: Update Image Tag in GitOps Repo
working-directory: gitops-repo/environments/development/backend
run: |
# Use sed to update newTag in kustomization.yaml
sed -i -E "s/newTag: .*/newTag: ${{ env.VERSION }}/g" kustomization.yaml
git diff
- name: Commit and Push Changes to GitOps Repo
working-directory: gitops-repo
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Add the correctly named file
git add environments/development/backend/kustomization.yaml
if git diff --staged --quiet; then
echo "No changes to commit."
else
git commit -m "chore(backend): bump development image tag to ${{ env.VERSION }}"
git push origin ${{ env.GITOPS_BRANCH }}
fi
Once the workflow pushes the updated kustomization.yaml, ArgoCD detects the change (within 3 minutes by default) and syncs the cluster to match. The full flow:
Push tag → GitHub Actions builds image → pushes to registry → updates image tag in GitOps repo → ArgoCD detects change → deploys to cluster
Reusable workflows
Call a shared workflow from multiple repositories:
# .github/workflows/_shared-deploy.yml (in a shared repo or same repo)
name: Shared Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
API_KEY:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- run: deploy --env ${{ inputs.environment }} --key ${{ secrets.API_KEY }}
# Consumer workflow
jobs:
deploy-staging:
uses: myorg/shared-workflows/.github/workflows/_shared-deploy.yml@main
with:
environment: staging
secrets:
API_KEY: ${{ secrets.STAGING_API_KEY }}
GitHub CLI (gh)
gh is GitHub's official command-line tool for working with repositories, issues, pull requests, releases, and workflows without leaving the terminal. Key commands, grouped by task:
# Authentication
gh auth login
gh auth status
# Repository
gh repo create myorg/new-repo --public --clone
gh repo clone myorg/repo
# Pull requests
gh pr create --title "Fix login bug" --body "Closes #42"
gh pr list
gh pr checkout 123
gh pr review 123 --approve
gh pr merge 123 --squash --delete-branch
# Issues
gh issue create --title "Memory leak in worker" --body "Detailed description..."
gh issue list --label bug
gh issue view 42
# Workflows and runs
gh run list
gh run watch
gh run view --log
gh workflow list
gh workflow run deploy.yml --ref main
gh workflow run deploy.yml -f environment=staging
# Releases
gh release create v1.2.0 --title "v1.2.0" --notes "Release notes here"
gh release list
# Gist
gh gist create script.sh --public --desc "Useful utility"
Workflow tips
| Tip | Detail |
|---|---|
| Cache dependencies | Use actions/setup-node with cache: "npm" or actions/cache@v4 for other ecosystems. |
| Concurrency | concurrency: { group: deploy, cancel-in-progress: true } — cancels in-progress runs of the same group. |
| Conditional steps | if: github.ref == 'refs/heads/main' or if: ${{ failure() }} for cleanup steps. |
| Expressions | ${{ env.STAGE }}, ${{ matrix.os }}, ${{ secrets.TOKEN }} |
| Built-in env vars | ${{ github.sha }}, ${{ github.ref }}, ${{ github.event_name }}, ${{ github.repository }} |
| Job outputs | echo "result=success" >> $GITHUB_OUTPUT then reference with ${{ steps.id.outputs.result }} |
See also
- GitOps with ArgoCD — continuous deployment patterns
- Terraform Team Collaboration — CI/CD for infrastructure