Domain 1: Author and manage workflows
- Workflows are defined in YAML files placed in the .github/workflows/ directory at the root of the repository; each file is one workflow with its own name:, on:, and jobs:.
- 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). Each pair is mutually exclusive and cannot be combined in one event.
- workflow_dispatch can define inputs (with required, type: string/boolean/choice/number/environment, and default) entered when the workflow is run manually 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.
- A job is a set of steps that runs on a single runner; jobs run in parallel by default unless ordered with needs, which makes a job wait for the listed job(s) and builds a dependency graph.
- runs-on selects the runner: GitHub-hosted labels like ubuntu-latest, windows-latest, macos-latest, or self-hosted labels for custom hardware.
- Steps either run a shell command (run:) or invoke a reusable action (uses:); the with: keyword passes inputs to an action, and run: | uses a YAML block scalar so multi-line commands run in one shell.
- A matrix strategy (strategy.matrix) runs a job across combinations such as OS and language versions in parallel; fail-fast: false lets all legs finish instead of cancelling siblings on first failure.
- 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 in the job.
- Job-level outputs are declared under jobs.<id>.outputs (mapped from step outputs) and consumed downstream via needs.<id>.outputs.<name>.
- Set environment variables with env: at the workflow, job, or step level; the more specific scope overrides the broader one.
- concurrency limits overlapping runs in a named group, and cancel-in-progress: true cancels an in-flight run when a newer one starts (a common group is ${{ github.workflow }}-${{ github.ref }}).
- Step- and job-level if: conditions gate execution using ${{ }} expressions and the github context (github.event_name, github.ref, github.sha, github.actor, github.repository).
- timeout-minutes caps runtime at the job or step level; without it a job is subject to the default 6-hour limit (35 days for the whole workflow run), and environments can require reviewer approval before a targeting job runs.
- Author reusable workflows with on: workflow_call, declaring inputs, secrets, and outputs so other workflows call them via uses: to share standardized logic across repositories.
Domain 2: Consume and troubleshoot workflows
- Monitor runs in the repository Actions tab: each run lists its jobs, per-step logs, timing, and final conclusion, and the visualization graph shows job dependencies and where a run failed.
- The gh CLI consumes runs: gh run list, gh run view <id> --log, gh run watch to follow a live run, and gh run download to pull artifacts.
- Enable step debug logging by setting the ACTIONS_STEP_DEBUG secret or variable to true (and ACTIONS_RUNNER_DEBUG for runner-level diagnostics) to get verbose logs when a step behaves unexpectedly.
- Re-run failed or all jobs from the run page or with gh run rerun <id> --failed; a re-run reuses the same commit SHA and the original inputs rather than picking up newer code.
- The REST and GraphQL APIs expose workflow runs, jobs, logs, and artifacts (e.g. GET /repos/{owner}/{repo}/actions/runs) for status dashboards and downstream automation.
- actions/upload-artifact and actions/download-artifact move files between jobs and after a run finishes; set retention-days (default 90, configurable) and upload only what is needed to limit storage.
- Download artifacts from the run summary UI, the API, or gh run download; artifacts persist for the retention window then are deleted automatically.
- actions/cache restores dependencies keyed on a hash of the lockfile; a cache miss is non-fatal (the step rebuilds and may save a new cache), and restore-keys supply fallback prefixes for a partial hit.
- 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').
- Consume Marketplace actions with uses: owner/repo@ref and read the action's README for its required inputs (with:) and outputs before adding it to a workflow.
- actions/checkout clones the repository into the runner workspace; it is usually the first step because the workspace starts empty and most build steps need the source.
- Workflow commands surface results as annotations: echo "::error::", "::warning::", and "::notice::" create annotations shown on the run and in pull requests, and $GITHUB_STEP_SUMMARY writes a Markdown job summary.
- Troubleshoot common failures: a non-zero step exit code, a missing secret or insufficient permissions (403), YAML syntax errors that stop a run from starting, expired or misnamed cache keys, and jobs skipped because their if: or needs conditions were not met.
- A failed step stops its job unless continue-on-error: true or if: always() is set; needs also short-circuits downstream jobs when an upstream job fails.
- The run conclusion (success, failure, cancelled, skipped) and the workflow status badge report overall health for consumers and dashboards.
Domain 3: Author and maintain actions
- A custom action is packaged, reusable automation defined by an action.yml (or action.yaml) metadata file at the action's root.
- action.yml declares name, description, author, inputs (each with description/required/default), outputs, branding (icon and color for the Marketplace listing), and the runs: block that specifies how the action executes.
- There are three action types selected by runs.using: JavaScript (using: node20 runs a bundled main.js on the runner), Docker container (using: docker packages code plus its OS/tooling), and composite (using: composite bundles multiple run/uses steps into one action).
- JavaScript actions use the @actions/core and @actions/github toolkit to read inputs (core.getInput), set outputs (core.setOutput), and log; because npm install does not run at execution time, dependencies must be committed or bundled (e.g. with @vercel/ncc).
- Docker container actions are the most flexible for custom OS and tooling but start slower (image pull or build) and run only on Linux runners.
- Composite actions reuse a sequence of steps without Node or Docker and reference their own inputs via ${{ inputs.<name> }}; a step with an id can write to $GITHUB_OUTPUT to produce the action's outputs.
- Callers pass inputs with with:, and the action exposes outputs consumed as steps.<id>.outputs.<name>; document each input and output in the README.
- Version actions with Git tags: publish an immutable full release tag like v1.2.3 and maintain a moving major-version tag like v1 that consumers reference so they receive backward-compatible updates.
- As an author, keep the major tag pointing at the latest compatible release and use releases and a changelog to signal breaking changes; consumers of third-party actions should pin to a full-length commit SHA for immutability.
- Publish an action to the GitHub Marketplace from a public repository that has an action.yml at its root and a published release, choosing a unique name and a category; the listing is drawn from the metadata and README.
- A repository can hold one action at its root or reference an action stored in a subdirectory via uses: owner/repo/path@ref.
- Test an action by referencing it locally with uses: ./ in a workflow in the same repository, adding unit tests for JavaScript logic, and validating across every runner OS the action claims to support.
- Keep an action backward compatible within a major version, bump the major tag only for breaking changes, and use Dependabot (which can update action versions in consuming repos) to keep the ecosystem current.
Domain 4: Manage GitHub Actions for the enterprise
- Self-hosted runners provide specific hardware, on-prem network access, custom software, or compliance control; you install, patch, and secure them yourself and they are not billed Actions minutes.
- Register a runner by downloading the runner application and running ./config.sh with the registration URL and token, then ./run.sh (or installing it as a service) so it listens for and executes jobs.
- Use --ephemeral so a runner accepts a single job then de-registers, giving a clean environment per job and a safer autoscaling model; remove a runner with ./config.sh remove.
- Jobs are routed to self-hosted runners by labels, e.g. runs-on: [self-hosted, linux, gpu]; custom labels match jobs to the right hardware.
- Runner groups organize self-hosted (and larger hosted) runners at the organization or enterprise level and control which repositories and workflows may use them.
- Autoscale with Actions Runner Controller (ARC) on Kubernetes to create ephemeral runners on demand and scale to zero when idle.
- Do not use self-hosted runners on public repositories: a forked pull request could execute untrusted code on your infrastructure, so reserve them for private or internal repos.
- At the organization or enterprise level, an Actions policy sets which repositories can run Actions and which actions are allowed: all actions, local (same-repo) actions only, or a selected list (optionally allowing verified-creator and specific owner/*@ actions).
- The default GITHUB_TOKEN permissions can be set org- or repo-wide to read-only, and policy can control whether workflows are allowed to create or approve pull requests.
- Required workflows let an organization enforce that specified workflows run and pass on pull requests across selected repositories before a merge is allowed.
- Share actions and reusable workflows across an org by hosting them in a repository whose Actions access is opened to other org repositories, avoiding per-repo copies.
- Enterprises can restrict which GitHub-hosted runner images and larger runners are available and expose them to teams through runner groups.
- Actions usage is metered as minutes and storage; organization owners set spending limits, can cap or alert on Actions and Packages spend, and review consumption in the billing dashboard.
- The Actions usage and billing reports break minutes down by runner OS and storage by repository, helping enforce budgets and spot runaway or misconfigured workflows.
Domain 5: Secure and optimize automation
- GITHUB_TOKEN is an automatically provided, repo-scoped token created per run; its permissions are configurable and it expires when the job finishes.
- The permissions: key sets GITHUB_TOKEN scopes at the workflow or job level; apply least privilege starting from read-only, e.g. permissions: { contents: read, packages: write }.
- Store credentials as encrypted secrets referenced via ${{ secrets.NAME }} (set one with gh secret set API_KEY --body "value"); secrets are masked in logs and are not passed to workflows triggered by fork pull requests by default.
- Secret scope hierarchy: repository secrets, environment secrets (gated by protection rules and required reviewers), and organization secrets that can be scoped to selected repositories.
- Never echo secrets or pass them as command-line arguments where they can leak; masking does not cover secrets that are transformed, encoded, or printed indirectly.
- Set permissions: { id-token: write } and use OIDC to exchange a short-lived, signed identity token for temporary cloud credentials (AWS, Azure, GCP) instead of storing long-lived access keys as secrets.
- pull_request_target runs in the BASE repository context with a write-capable GITHUB_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 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.
- 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. Use Dependabot to bump pins deliberately.
- GitHub-hosted minutes are billed by rounded-up wall-clock minutes per job; Windows runners cost 2x and macOS runners cost 10x the Linux rate, so keep heavy compute on Linux where possible.
- actions/cache is evicted least-recently-used once a repository exceeds the 10 GB cache limit; caches are branch-scoped with default-branch caches readable by other branches, so key them on a lockfile hash for high hit rates.
- Combine concurrency groups with cancel-in-progress so a new push to a branch cancels the older in-flight run and frees minutes immediately.
- Use path filters and change detection so workflows or matrix legs run only when relevant files change, avoiding wasted runs in monorepos.
- Cache dependencies and enable Docker layer caching (docker/build-push-action with buildx and the gha cache backend) so unchanged work is reused between runs.
- Larger or self-hosted runners can finish compute-heavy builds in fewer minutes but pay off only when the speedup outweighs the higher rate or maintenance cost; set short artifact retention-days to control storage spend.
- Enforce security at scale by combining least-privilege permissions:, SHA-pinning, OIDC, organization allowed-actions policies, and required reviewers on protected environments.
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.