Events / triggers:
push— code pushedpull_request— PR opened, updated, closedschedule— cron syntax for periodic runsworkflow_dispatch— manual trigger from UI or APIrepository_dispatch— external trigger via APIworkflow_call— called by another workflow (reusable workflows)
Runners:
- GitHub-hosted — ubuntu-latest, windows-latest, macos-latest. Free minutes on public repos, billed on private repos.
- Self-hosted — your own infrastructure. Required for accessing private networks or specialised hardware (GPU runners, ARM, etc.). You manage updates, security, isolation.
Jobs vs steps:
- Jobs run in parallel by default. Use
needs:to create dependencies. - Steps within a job run sequentially on the same runner.
- Each job gets a fresh runner — data doesn't persist between jobs unless you use artifacts or caching.
Secrets and variables:
- Secrets — encrypted, not visible in logs (
${{ secrets.MY_SECRET }}). Scoped to repo, organisation, or environment. - Variables — plaintext, visible in logs (
${{ vars.MY_VAR }}). Same scoping. - Environment variables — set with
env:at workflow, job, or step level.
Permissions:
The permissions: block controls what the workflow's GITHUB_TOKEN can do. Default is read-only for public repos, write for private. Always grant least privilege explicitly.
Caching:
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
Speeds up workflows by caching dependencies between runs.
Artifacts:
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
Passes files between jobs or downloads them after the workflow completes.
Environments: Named deployment targets (production, staging, dev) with:
- Required reviewers (manual approval before deploying)
- Wait timers
- Environment-specific secrets and variables
- Branch protection (only deploy from specific branches)