GitHub Actions
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.
How GitHub Actions Works
Section titled “How GitHub Actions Works”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| Concept | What It Is |
|---|---|
| Event | What triggers the workflow (push, pull_request, schedule, etc.) |
| Workflow | A YAML file in .github/workflows/ that defines the automation |
| Job | A set of steps that run on the same runner; jobs run in parallel by default |
| Step | A single action or shell command within a job |
| Runner | The machine that executes a job (GitHub-hosted VM or self-hosted) |
| Action | A reusable unit of work (from the marketplace or custom) |
Workflow YAML Anatomy
Section titled “Workflow YAML Anatomy”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 testTriggers
Section titled “Triggers”Common Triggers
Section titled “Common Triggers”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 publishedTrigger Reference
Section titled “Trigger Reference”| Trigger | Fires When | Common Use |
|---|---|---|
push | Code pushed to a branch or tag | CI on every commit |
pull_request | PR opened, updated, or reopened | Validate before merge |
schedule | Cron schedule (UTC) | Nightly builds, drift detection |
workflow_dispatch | Manual button click or API call | Production deploys, ad-hoc runs |
release | GitHub release created/published | Publish packages, deploy release |
workflow_call | Called by another workflow | Reusable workflow (see below) |
repository_dispatch | External webhook via API | Cross-repo triggers, ChatOps |
issue_comment | Comment on an issue or PR | ChatOps (/deploy, /test) |
Actions Marketplace
Section titled “Actions Marketplace”The GitHub Marketplace has thousands of reusable actions. Common ones:
| Action | What It Does |
|---|---|
actions/checkout@v4 | Check out your repository |
actions/setup-node@v4 | Install Node.js (also: setup-python, setup-go, setup-java) |
actions/cache@v4 | Cache dependencies (npm, pip, Gradle, etc.) |
actions/upload-artifact@v4 | Upload build artifacts |
actions/download-artifact@v4 | Download artifacts from another job |
docker/build-push-action@v6 | Build and push Docker images |
docker/login-action@v3 | Log in to a container registry |
aws-actions/configure-aws-credentials@v4 | Configure AWS credentials (supports OIDC) |
azure/login@v2 | Log in to Azure (supports OIDC) |
hashicorp/setup-terraform@v3 | Install Terraform |
Using an Action
Section titled “Using an Action”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.
Writing Custom Actions
Section titled “Writing Custom Actions”Composite Action
Section titled “Composite Action”A composite action is a reusable set of steps (no separate runtime):
name: Setup and Builddescription: 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 workflowsteps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-and-build with: node-version: 22Other Action Types
Section titled “Other Action Types”| Type | Runtime | Best For |
|---|---|---|
| Composite | Runs steps in the workflow runner | Grouping common steps (most common) |
| JavaScript | Node.js | Complex logic, API calls, fast startup |
| Docker | Docker container | Tools that need a specific OS/environment |
Reusable Workflows
Section titled “Reusable Workflows”Reusable workflows let you define an entire workflow that other workflows can call — like a function:
name: Deployon: 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 workflowname: CIon: 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 }}Matrix Strategy
Section titled “Matrix Strategy”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 testThis creates 9 parallel jobs (3 OS x 3 Node versions).
Excluding and Including Combinations
Section titled “Excluding and Including Combinations”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 variableSecrets and Environment Protection
Section titled “Secrets and Environment Protection”Secrets
Section titled “Secrets”jobs: deploy: runs-on: ubuntu-latest steps: - run: | curl -H "Authorization: Bearer ${{ secrets.API_TOKEN }}" \ https://api.example.com/deploySecrets are:
- Encrypted at rest and masked in logs.
- Available at the repository, environment, or organization level.
- Not passed to workflows triggered from forks (security).
Environment Protection Rules
Section titled “Environment Protection Rules”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
maincan deploy to production. - Deployment branches — restrict which branches can target this environment.
OIDC (OpenID Connect)
Section titled “OIDC (OpenID Connect)”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 credentialsThis eliminates the need to store AWS access keys or Azure service principal secrets in GitHub.
For cloud-specific OIDC setup, see:
Caching and Artifacts
Section titled “Caching and Artifacts”Dependency Caching
Section titled “Dependency Caching”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 ~/.npmArtifacts
Section titled “Artifacts”# 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/Self-Hosted Runners
Section titled “Self-Hosted Runners”For jobs that need special hardware, internal network access, or cost savings:
# On your server — download and configure the runner./config.sh --url https://github.com/myorg/myrepo --token <TOKEN>./run.sh# Use in a workflowjobs: build: runs-on: self-hosted # or a custom label steps: - uses: actions/checkout@v4 - run: make buildRunner Labels
Section titled “Runner Labels”Self-hosted runners can have labels for targeting:
runs-on: [self-hosted, linux, gpu] # Requires all three labelsActions Runner Controller (ARC)
Section titled “Actions Runner Controller (ARC)”For Kubernetes, use ARC to auto-scale GitHub Actions runners as pods:
helm install arc \ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller \ --namespace arc-systems --create-namespaceExample Workflows
Section titled “Example Workflows”Node.js CI
Section titled “Node.js CI”name: Node.js CIon: 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/Docker Build and Push
Section titled “Docker Build and Push”name: Dockeron: 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=maxTerraform Plan on PR, Apply on Merge
Section titled “Terraform Plan on PR, Apply on Merge”name: Terraformon: 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 tfplanGitHub Actions vs GitLab CI
Section titled “GitHub Actions vs GitLab CI”| Feature | GitHub Actions | GitLab CI |
|---|---|---|
| Config file | .github/workflows/*.yml (multiple) | .gitlab-ci.yml (single) |
| Stages | Implicit via needs | Explicit stages: block |
| Reuse | Reusable workflows, composite actions | include, extends, templates |
| Marketplace | Large action marketplace | Smaller, but include from URLs |
| Runners | GitHub-hosted + self-hosted | Shared + group + project runners |
| Environments | Built-in with protection rules | Built-in with approval gates |
| OIDC | Native support | Native support |
| DAG | Via needs | Via needs keyword |
| Container registry | GitHub Packages (GHCR) | Built-in container registry |
| Best for | GitHub-hosted projects | GitLab-hosted projects, all-in-one platform |
Key Takeaways
Section titled “Key Takeaways”- 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
permissionsto apply least-privilege to theGITHUB_TOKEN.