GitHub Actions Study Guide
The GitHub Actions certification validates your ability to build and manage CI/CD automation using GitHub Actions, covering workflow syntax and events, jobs/steps/runners, secrets and security, and reuse/optimization patterns. It is aimed at developers, DevOps engineers, and platform engineers who author and maintain GitHub-native automation. The exam is 832-question-pool based with a passing score of 700 and a 120-minute duration.
Domain 1: Workflows and Events
- Workflows are defined in YAML files placed in the .github/workflows/ directory at the root of the repository; each file is one workflow.
- The top-level on: key declares triggers; common ones are push, pull_request, schedule (cron), and workflow_dispatch (manual). It accepts a single event, an array, or a map with filters.
- branches / branches-ignore filter which branches trigger push and pull_request; paths / paths-ignore filter by changed files (e.g. paths-ignore: ['**.md'] skips docs-only changes).
- branches and branches-ignore cannot be used together in the same event; likewise paths and paths-ignore are mutually exclusive.
- workflow_dispatch can define inputs (with required, type: string/boolean/choice/number, and default) that are entered when the workflow is manually run and read via inputs.<name> or github.event.inputs.<name>.
- schedule uses POSIX cron syntax in UTC (e.g. '0 2 * * *'); the shortest interval allowed is every 5 minutes, and scheduled runs can be delayed during high load.
- concurrency limits overlapping runs in a named group; cancel-in-progress: true cancels an in-flight run when a newer one starts. A common group is ${{ github.workflow }}-${{ github.ref }}.
- The github context exposes event metadata: github.event_name, github.ref, github.sha, github.repository, github.actor, and the raw payload under github.event.
- Step-level if: conditions gate execution, e.g. if: github.event_name == 'push' && github.ref == 'refs/heads/main'; expressions use the ${{ }} syntax (optional inside if:).
- Pass data between steps by writing key=value to the $GITHUB_OUTPUT file, then read it as steps.<id>.outputs.<key>; write to $GITHUB_ENV to set an environment variable for later steps.
- Set environment variables with env: at the workflow, job, or step level; more specific scopes override broader ones.
- Multi-line shell commands use the YAML block scalar with run: | so each line runs in the same shell step.
- Secrets are referenced as ${{ secrets.NAME }} and are automatically masked in logs; they are not passed to workflows triggered by pull requests from forks by default.
- Restrict push triggers to protected branches (e.g. main) and let pull_request handle PR validation; prefer a single off-peak cron schedule over multiple overlapping ones to reduce cost and contention.
Domain 2: Jobs, Steps, and Runners
- A job is a set of steps that runs on a single runner; by default all jobs in a workflow run in parallel unless ordered with needs.
- needs: [jobA] makes a job wait for the listed job(s) to finish successfully before it starts, creating a dependency graph.
- runs-on specifies the runner: GitHub-hosted labels like ubuntu-latest, windows-latest, macos-latest, or self-hosted labels for custom hardware.
- Self-hosted runners are used for specific hardware, on-prem network access, custom software, or compliance needs; you manage and secure them yourself.
- Steps either run a shell command (run:) or invoke a reusable action (uses:); the with: keyword passes inputs to an action.
- A matrix strategy (strategy.matrix) runs a job across combinations such as OS and language versions in parallel; fail-fast: false lets all combinations finish instead of cancelling siblings on first failure.
- Job-level outputs are declared under jobs.<id>.outputs (mapped from step outputs) and consumed downstream via needs.<id>.outputs.<name>.
- actions/cache stores and restores dependencies/build outputs keyed on a hash of the lockfile; a cache miss is non-fatal and steps simply rebuild and may save a new cache.
- Setup actions like actions/setup-node, setup-python, and setup-java install toolchains quickly and offer built-in dependency caching (e.g. setup-node's cache: 'npm').
- timeout-minutes caps maximum runtime at the job or step level; without it jobs are subject to GitHub's default 6-hour limit (35 days for the whole workflow run).
- Wrap a flaky step in a bounded retry (or a retry action) rather than re-running the entire workflow; gate optional steps with an if: expression based on changed paths or an earlier job's output.
- Each job's billed time is rounded UP to the nearest minute, so many tiny jobs accumulate rounding overhead; sharding tests across a matrix trades minutes for wall-clock speed.
- Environments can attach a manual approval gate (required reviewers) before a job that targets them runs, plus environment-scoped secrets and variables.
- Steps run sequentially within a job and share the same runner filesystem and workspace; a failed step stops the job unless if: always() / continue-on-error is set.
Domain 3: Secrets and Security
- An action is a packaged, reusable unit of automation referenced in a step via uses: (e.g. actions/checkout@v4); actions/checkout clones the repo into the runner workspace.
- Custom actions come in three flavors: JavaScript (runs via Node.js on the runner), Docker container (packages code plus its environment), and composite (bundles multiple steps/commands into one action).
- Pin third-party actions to a full-length commit SHA rather than a mutable tag; a SHA is immutable so the exact reviewed code runs even if the v4 tag is later moved by a maintainer or attacker.
- Store credentials as encrypted secrets and reference them as ${{ secrets.NAME }}; set one via the CLI with gh secret set API_KEY --body "value".
- The permissions: key sets GITHUB_TOKEN scopes at the workflow or job level; apply least privilege, e.g. permissions: { contents: read, packages: write }.
- Set permissions: { id-token: write } to allow the workflow to request an OIDC token for cloud authentication.
- Use OIDC to assume a cloud IAM role with short-lived credentials instead of storing long-lived access keys as secrets, reducing the standing-secret blast radius.
- pull_request_target runs in the BASE repository context with a write-capable token and secret access; checking out and running untrusted fork code under it is a dangerous supply-chain risk.
- Run untrusted fork PR code under the pull_request event (no secrets), and gate any secret-using steps behind a manually-approved environment or a maintainer-triggered workflow.
- Reusable workflows are invoked with on: workflow_call and called from another workflow's job via uses: to share standardized logic across repos.
- Secret scope hierarchy: repository secrets, environment secrets (with protection rules), and organization secrets that can be scoped to selected repositories; environment secrets are gated by required reviewers.
- Never echo secrets or pass them as command-line arguments where they can leak; secrets are masked in logs but not when transformed or printed in non-obvious encodings.
- In Actions settings you can require approval for workflows run by first-time or outside contributors and restrict which actions are allowed to run.
- Store a shared signing key as an organization secret scoped to selected repos; keep deploy keys as repository or environment secrets to limit exposure.
Domain 4: Reuse and Optimization
- GITHUB_TOKEN is an automatically provided, repo-scoped token created per run; its permissions are configurable and it expires when the job finishes.
- Reusable workflows (on: workflow_call) eliminate copy-pasted YAML by centralizing common pipelines; callers pass inputs and secrets and consume the workflow's outputs.
- actions/upload-artifact and actions/download-artifact move files between jobs and runs; set a short retention-days and upload only what is needed to control storage cost.
- actions/cache supports restore-keys as fallback prefixes so a partial hit from a similar older key still saves download time when the exact key misses.
- Caches are evicted least-recently-used once a repository exceeds the 10 GB total cache limit; caches are also scoped by branch with main-branch caches readable by other branches.
- GitHub-hosted minutes are billed by rounded-up wall-clock minutes per job at a per-minute rate; Windows runners cost 2x and macOS runners cost 10x the Linux rate.
- Larger runners with more vCPUs finish compute-heavy builds in fewer minutes but bill at a higher rate, so they only pay off when the speedup outweighs the multiplier.
- Self-hosted runners are not billed Actions minutes, so steady heavy workloads can be cheaper on hardware you already own (at the cost of maintenance and security).
- Use path filters and change-detection so workflows or matrix legs run only when relevant files change, avoiding wasted runs in monorepos.
- Combine concurrency groups with cancel-in-progress so a new push to a branch cancels the older in-flight run, freeing minutes immediately.
- Enable Docker layer caching (e.g. docker/build-push-action with buildx and a cache backend like gha) so unchanged image layers are reused between builds.
- Caching dependencies avoids repeated install/provisioning time every run, directly cutting the minutes spent setting up the toolchain.
- For multi-platform validation, run only the platform-specific step on costly macOS/Windows runners and keep heavy compute on cheaper Linux runners.
- Use organization policies to restrict which actions and runner types can be used, and combine SHA-pinning with OIDC to keep reuse both cost-efficient and secure.
GitHub Actions exam tips
- Memorize the canonical YAML keys and where they live: on:, jobs:, runs-on:, steps:, uses:/run:, with:, needs:, permissions:, concurrency:, strategy.matrix - many questions test exact placement.
- Know the difference between $GITHUB_OUTPUT (step outputs read via steps.<id>.outputs) and $GITHUB_ENV (env vars for later steps), and how job outputs flow through needs.<id>.outputs.
- Security questions hinge on least-privilege permissions:, SHA-pinning actions, OIDC vs long-lived keys, and the danger of pull_request_target with untrusted fork code - read those scenarios carefully.
- For cost questions, remember minutes round UP per job, Windows is 2x and macOS is 10x Linux, the cache limit is 10 GB with LRU eviction, and self-hosted runners are not billed minutes.
- When a question lists multiple plausible answers, pick the one matching GitHub's documented best practice (path filters, caching keyed on lockfiles, fail-fast: false for full results, concurrency with cancel-in-progress).
Study guide FAQ
What is the difference between $GITHUB_ENV and $GITHUB_OUTPUT?
Writing key=value to $GITHUB_ENV sets an environment variable that subsequent steps in the same job can read as $key. Writing to $GITHUB_OUTPUT defines a named step output read elsewhere as steps.<id>.outputs.<key>; to share across jobs you must also surface it as a job output and consume it via needs.<id>.outputs.
Why pin actions to a commit SHA instead of a tag like v4?
Tags are mutable, so a maintainer (or a compromised account) can move v4 to point at new, possibly malicious code. A full-length commit SHA is immutable, guaranteeing the exact code you reviewed runs every time - a key supply-chain safeguard. Use Dependabot to bump pinned SHAs deliberately.
When should I use OIDC instead of storing cloud credentials as secrets?
Use OIDC whenever you authenticate to a cloud provider (AWS, Azure, GCP). GitHub issues a short-lived, signed identity token per run that the cloud validates against a trust relationship, letting the job assume a role with temporary credentials. This removes long-lived keys from secrets, shrinking the blast radius if a workflow is compromised. It requires permissions: id-token: write.
Why is pull_request_target risky and what is the safe alternative?
pull_request_target runs in the base repository context with a privileged GITHUB_TOKEN and access to secrets. If it checks out and executes the forked PR's code, an untrusted contributor can run arbitrary commands with those privileges. The safe pattern is to validate untrusted code under the plain pull_request event (which has no secrets) and gate any secret-using or deploy steps behind a manually approved environment or a maintainer-triggered workflow.