- devops
- l1
- topic-pack
- github-actions
- cicd --- Portal | Level: L1: Foundations | Topics: GitHub Actions, CI/CD | Domain: DevOps & Tooling
GitHub Actions - Primer¶
Why This Matters¶
Timeline: GitHub Actions launched in October 2018 (originally HCL-based syntax) and switched to YAML in August 2019. By 2023 it became the most popular CI/CD platform, overtaking Jenkins. The "Actions" marketplace has over 20,000 community-contributed actions.
GitHub Actions is the most widely used CI/CD platform, integrated directly into the world's largest code hosting service. Every push, pull request, issue, or schedule trigger can launch automated workflows — build, test, deploy, release, scan, notify. For DevOps teams, understanding GitHub Actions is not optional: it is where most open-source projects run CI, where most startups build their deployment pipelines, and where most enterprise teams are consolidating their automation. The security model (secrets, OIDC, permissions), the caching system, and the reusable workflow patterns are the difference between a fast, secure pipeline and a slow, vulnerable one.
Core Concepts¶
1. Workflow Syntax Fundamentals¶
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main]
paths:
- 'src/**'
- 'tests/**'
- '.github/workflows/ci.yml'
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Monday 6am UTC
workflow_dispatch: # manual trigger
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
type: choice
options: [staging, production]
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest --cov=app --cov-fail-under=90 -v
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: htmlcov/
retention-days: 7
2. Matrix Builds¶
jobs:
test:
strategy:
fail-fast: false # don't cancel other jobs if one fails
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ['3.10', '3.11', '3.12']
exclude:
- os: macos-latest
python-version: '3.10'
include:
- os: ubuntu-latest
python-version: '3.12'
coverage: true # extra variable for one combination
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r requirements.txt
- run: pytest -v
- if: matrix.coverage
run: pytest --cov=app --cov-report=xml
3. Secrets and Security¶
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # requires environment approval rules
permissions:
id-token: write # needed for OIDC
contents: read
steps:
# Use repository secrets
- name: Deploy
env:
API_KEY: ${{ secrets.DEPLOY_API_KEY }}
run: ./deploy.sh
# OIDC authentication to AWS (no static credentials)
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: us-east-1
# Never echo secrets — they're masked in logs, but be careful
# with indirect exposure (error messages, log files, artifacts)
Security hardening:
# Pin actions to SHA (not tags — tags can be moved)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
# Remember: tags are pointers that can be moved after you audit them.
# A malicious maintainer can update v4 to inject code. SHAs are immutable.
# Restrict permissions (principle of least privilege)
permissions:
contents: read # never give write unless needed
# Limit environment access
environment:
name: production
url: https://prod.example.com
War story: In January 2024, a popular third-party GitHub Action (
tj-actions/changed-files) was compromised via a supply chain attack. The attacker modified the tag to inject code that exfiltrated secrets from CI runs. Organizations that pinned to SHA were unaffected. This is why security teams mandate SHA pinning for all third-party actions.Gotcha: GitHub-hosted runners have a 6-hour maximum job timeout and 10 GB of storage. If your build produces large artifacts or runs long integration tests, you will hit these limits. Self-hosted runners have no such limits but require you to manage security (runners execute arbitrary code from PRs by default on public repos).
4. Caching and Artifacts¶
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Cache dependencies (restored across runs)
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
node_modules
key: deps-${{ runner.os }}-${{ hashFiles('**/requirements.txt', '**/package-lock.json') }}
restore-keys: |
deps-${{ runner.os }}-
# Upload build artifacts (shared between jobs)
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 5
deploy:
needs: build
runs-on: ubuntu-latest
steps:
# Download artifacts from build job
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
5. Reusable Workflows¶
# .github/workflows/reusable-deploy.yml (the reusable workflow)
name: Deploy
on:
workflow_call:
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
- name: Deploy to ${{ inputs.environment }}
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
echo "Deploying ${{ inputs.image_tag }} to ${{ inputs.environment }}"
helm upgrade myapp ./chart \
--set image.tag=${{ inputs.image_tag }} \
-f values-${{ inputs.environment }}.yaml
# .github/workflows/ci.yml (the caller)
name: CI
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make test
deploy-staging:
needs: test
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
image_tag: ${{ github.sha }}
secrets:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
deploy-production:
needs: deploy-staging
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
image_tag: ${{ github.sha }}
secrets:
DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
6. Docker Build and Push¶
jobs:
build-image:
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/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=semver,pattern={{version}}
- 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
7. Composite Actions (Custom Actions)¶
# .github/actions/setup-and-test/action.yml
name: 'Setup and Test'
description: 'Install deps and run tests'
inputs:
python-version:
description: 'Python version'
required: false
default: '3.11'
outputs:
coverage:
description: 'Test coverage percentage'
value: ${{ steps.test.outputs.coverage }}
runs:
using: 'composite'
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: 'pip'
- run: pip install -r requirements.txt
shell: bash
- id: test
run: |
COV=$(pytest --cov=app --cov-report=term | grep TOTAL | awk '{print $4}')
echo "coverage=$COV" >> "$GITHUB_OUTPUT"
shell: bash
8. Job Dependencies and Conditionals¶
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ruff check .
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pytest
deploy:
needs: [lint, test] # both must pass
if: github.ref == 'refs/heads/main' # only on main
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
notify:
needs: [deploy]
if: always() # run even if deploy fails
runs-on: ubuntu-latest
steps:
- if: needs.deploy.result == 'failure'
run: echo "Deploy failed!"
Quick Reference¶
# Trigger types
on: [push, pull_request] # basic
on: { push: { branches: [main] } } # filtered
on: { schedule: [{ cron: '0 0 * * *' }] } # scheduled
on: workflow_dispatch # manual
on: workflow_call # reusable workflow
# Key expressions
${{ github.sha }} # commit SHA
${{ github.ref_name }} # branch name
${{ github.event.pull_request.number }} # PR number
${{ secrets.MY_SECRET }} # repository secret
${{ vars.MY_VARIABLE }} # repository variable
${{ needs.job_id.outputs.key }} # output from another job
${{ hashFiles('**/lockfile') }} # file content hash (for caching)
# Useful gh CLI commands for debugging
gh run list # list recent runs
gh run view <id> # view run details
gh run view <id> --log # view full logs
gh run rerun <id> # rerun a failed run
gh run watch <id> # live tail a running workflow
Wiki Navigation¶
Prerequisites¶
- Git for DevOps (Topic Pack, L0)
Related Content¶
- Deep Dive: CI/CD Pipeline Architecture (deep_dive, L2) — CI/CD, GitHub Actions
- Adversarial Interview Gauntlet (30 sequences) (Scenario, L2) — CI/CD
- CI Pipeline Documentation (Reference, L1) — CI/CD
- CI/CD Drills (Drill, L1) — CI/CD
- CI/CD Flashcards (CLI) (flashcard_deck, L1) — CI/CD
- CI/CD Pipelines & Patterns (Topic Pack, L1) — CI/CD
- Circleci Flashcards (CLI) (flashcard_deck, L1) — CI/CD
- Dagger / CI as Code (Topic Pack, L2) — CI/CD
- Interview: CI Vuln Scan Failed (Scenario, L2) — CI/CD
- Jenkins Flashcards (CLI) (flashcard_deck, L1) — CI/CD
Pages that link here¶
- Anti-Primer: Github Actions
- CI Pipeline
- CI/CD Drills
- CI/CD Pipeline Architecture
- Comparison: CI Platforms
- Dagger
- Git for DevOps
- Github Actions
- How We Got Here: CI/CD Evolution
- Production Readiness Review: Answer Key
- Production Readiness Review: Study Plans
- Scenario: CI Failed Due to Vulnerability Scan
- Thinking Out Loud: GitHub Actions