Hands-on: sharifndlovu123/github-actions-course, the companion repo for the DevOps Directive "GitHub Actions: Beginner to Pro" course (workflows, authoring actions, best practices, capstone).
GitHub Actions is CI/CD built into GitHub. Workflows are YAML files in .github/workflows/ that run on events in your repository. Concepts are in CI-CD Pipelines.
Anatomy
| Term | Meaning |
|---|---|
| Workflow | A YAML file defining an automated process |
| Event | What triggers it (push, pull_request, schedule, workflow_dispatch, release) |
| Job | A set of steps run on one runner. Jobs run in parallel unless needs is set |
| Step | A shell command (run) or a reusable action (uses) |
| Action | A reusable unit from the Marketplace or your repo |
| Runner | The machine: GitHub-hosted (ubuntu-latest, windows-latest, macos-latest) or self-hosted |
A Basic CI Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip
- run: pip install -r requirements.txt
- run: pytest
matrixruns the job once per combination (versions, OSes)cache: piprestores dependencies between runspermissionslimits the automaticGITHUB_TOKEN; always set it explicitly (least privilege)
Triggers (on)
on:
push:
branches: [main]
paths: ["src/**"]
tags: ["v*"]
pull_request:
types: [opened, synchronize]
schedule:
- cron: "0 2 * * *" # nightly, UTC
workflow_dispatch: # manual button, with inputs
inputs:
environment:
type: choice
options: [staging, production]
workflow_call: # reusable workflow
Build and Push a Docker Image
name: Docker
on:
push:
branches: [main]
jobs:
image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- 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 }}
cache-from: type=gha
cache-to: type=gha,mode=max
See 1. Docker.
Secrets, Variables and Contexts
- Secrets: encrypted, set at repo / environment / org level, used as
${{ secrets.NAME }}; masked in logs, not passed to workflows from forks - Variables: non-sensitive config,
${{ vars.NAME }} - Contexts:
github(event, ref, sha, actor),env,job,steps,runner,needs,matrix - Pass data between steps with
$GITHUB_OUTPUT, and between jobs with joboutputs
steps:
- id: ver
run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT"
- run: echo "Building ${{ steps.ver.outputs.version }}"
Job Dependencies and Environments
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./build.sh
- uses: actions/upload-artifact@v4
with: { name: dist, path: dist/ }
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/download-artifact@v4
with: { name: dist }
- run: ./deploy.sh staging
deploy-prod:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # can require reviewers / wait timer
steps:
- uses: actions/download-artifact@v4
with: { name: dist }
- run: ./deploy.sh production
Environments hold their own secrets and protection rules (required reviewers, branch restrictions), which gives manual approval gates.
if: conditions control steps and jobs, for example if: github.ref == 'refs/heads/main'.
Deploying to the Cloud with OIDC (no stored keys)
GitHub issues a short-lived OIDC token; the cloud trusts it and hands back temporary credentials.
AWS
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: af-south-1
- run: aws s3 sync ./site s3://my-unique-bucket-12345 --delete
The role's trust policy restricts it to your repo and branch (token.actions.githubusercontent.com). See 2. AWS IAM and Security.
Azure
permissions:
id-token: write
contents: read
steps:
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- uses: azure/webapps-deploy@v3
with:
app-name: my-unique-webapp
package: .
Uses an Entra federated credential on an app registration. See 5. Azure Identity and Security.
Reuse
- Reusable workflows (
workflow_call): call a workflow from another withuses: org/repo/.github/workflows/build.yml@v1 - Composite actions: bundle several steps in an
action.yml - JavaScript / Docker actions: custom logic with inputs and outputs
- Starter workflows in the organisation
.githubrepo
Security Practices
- Set
permissions:to the minimum; defaultcontents: read - Pin third-party actions to a full commit SHA, not a moving tag
- Never echo secrets; avoid
pull_request_targetwith untrusted code - Use OIDC instead of long-lived cloud keys
- Dependabot for actions and dependencies; CodeQL code scanning; secret scanning and push protection
- Self-hosted runners: use ephemeral runners, never on public repos
Operations
- Caching (
actions/cache, built-incache:in setup actions), concurrency groups to cancel superseded runs:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
- Re-run failed jobs, enable debug logging with
ACTIONS_STEP_DEBUG - Hosted runner minutes are metered (free tier for public repos)
- Use
timeout-minutes:on jobs to avoid runaway builds