skip to content

GitHub Actions

3 roadmaps32 questionsupdated

GitHub's built-in CI/CD: workflows triggered by repository events, jobs on hosted or self-hosted runners, reusable actions from the marketplace, secrets, and matrix builds. The most commonly asked platform, simply because it is where most repositories already live.

on this pageshow

guide

overview

~1 min

GitHub Actions is the CI/CD system built into GitHub. A YAML file under `.github/workflows` says which repository events start a run, which jobs that run contains, and which machine each job executes on. Interviewers ask about it because most teams already ship through it, and because its defaults carry real consequences: a trigger that fires on every unrelated commit, a token with more scope than the job needs, a cache that never refreshes. A good answer names the key, the default it changes, and what goes wrong without it. The hub follows a workflow file from top to bottom. [Workflow syntax and triggers](/topics/cloud-github-actions-workflow-syntax) decides when a run starts and which runs cancel each other. [Jobs and steps](/topics/cloud-github-actions-jobs-steps) shapes what runs, in what order and under which conditions. [Runners](/topics/cloud-github-actions-runners) is where the work physically executes. [Actions and the marketplace](/topics/cloud-github-actions-actions-marketplace) is the packaged code you pull into steps, and [secrets and environments](/topics/cloud-github-actions-secrets-environments) governs which credentials that code can reach. [Caching and matrix builds](/topics/cloud-github-actions-caching-matrices) makes the same pipeline faster and wider. Junior rounds check that you can read a workflow: triggers, `uses:` against `run:`, what `runs-on` picks. Senior and principal rounds turn to trust and cost: fork pull requests, third-party code, cloud credentials without stored keys, self-hosted machines, and matrices that outgrow their budget. Learn triggers and the job graph first; the security sections assume both.

primer

### Event, jobs, steps A workflow maps repository events to runs. A run holds jobs, a job holds steps, and each step either calls a packaged action or executes shell. Placing a key at the right level — `on:` on the workflow, `runs-on` and `needs` on the job, `if:` on a job or a step — is often half an answer. ### Every job is a separate machine Each job gets its own runner and a clean workspace. Jobs start in parallel unless a dependency says otherwise. Because they share no disk, anything that crosses a job boundary must travel explicitly, typically as an output, an artifact or a cache entry. That isolation makes splitting work into jobs cost something, and lets a job carry its own permissions and be re-run alone. ### Defaults are conditions By default, a step runs only while everything before it has gone well, and a job waiting on a failed dependency does not start. Trigger filters are allow-lists. Many "why didn't this run?" questions trace back to a default nobody overrode, and many "why did this run?" questions to a filter nobody wrote. ### Your credentials run someone else's code Every `uses:` brings code you did not write into a job that may hold secrets and a `GITHUB_TOKEN`, and every fork pull request proposes code for your pipeline to execute. Security in this hub is deciding what each run can reach: token scopes narrowed with `permissions`, secrets scoped to environments that can demand approval, actions pinned to refs that cannot move, and cloud access issued per run through OIDC instead of long-lived keys stored as secrets. ### Trust follows the trigger The event that started a run decides whose workflow file is used and which context it gets. Fork pull requests run with reduced privilege by design; triggers that run with the base repository's privileges hand them back and need care. "Who can cause this run, and what can it touch?" comes before any other design question. ### Speed and breadth cost minutes A cache trades correctness risk for time: its key has to change exactly when its inputs do. A matrix trades minutes for coverage: every dimension multiplies the job count. Senior questions here are budget questions — what must run on every pull request, and what can wait for a schedule.

Workflow
A YAML file under .github/workflows that declares its triggers and jobs. Each triggering event that matches it creates one run.
Event trigger
A repository or external occurrence listed under on:, such as a push, a pull request, a schedule or a manual dispatch, optionally narrowed by branch, tag or path filters.
Job
A set of steps executed together on one runner. Jobs run in parallel unless needs: orders them, and they share no filesystem.
Action
Packaged, reusable step code — composite YAML, JavaScript or a Docker container — described by an action.yml file and referenced from a step.
Runner
The machine that executes a job, either a GitHub-hosted virtual machine or a self-hosted one you register, chosen by the labels in runs-on.
GITHUB_TOKEN
A short-lived token GitHub issues automatically for each job, used for calls against the repository; the permissions key decides its scopes.
Secret
An encrypted value stored at organization, repository or environment level and read through the secrets context. Its value is masked in logs.
Environment
A named deployment target that holds its own secrets and protection rules, such as required reviewers, a wait timer or allowed branches.
OIDC federation
Exchanging a per-run identity token issued by GitHub for short-lived cloud credentials, so no long-lived cloud key has to be stored as a secret.
Strategy matrix
A job setting that expands variables into one job per combination, adjusted with include and exclude and governed by fail-fast and max-parallel.
Cache key
The exact name under which a dependency cache is saved and first looked up; restore keys supply fallback prefixes when it misses.
Concurrency group
A named slot that limits runs or jobs sharing it to one at a time, optionally cancelling the older one when a newer one arrives.

Follow one push through the system. GitHub checks the event against each workflow's `on:` block and its filters; every workflow that matches starts a run. Jobs with no dependency are queued at once, and each is matched to a runner through the labels in `runs-on`. A job that names an environment first waits for that environment's protection rules. When a runner picks the job up it receives a `GITHUB_TOKEN` scoped by `permissions`, plus whichever secrets the steps reference, and executes the steps in order. Values reach later jobs through outputs, artifacts and caches. The results come back to the commit or pull request as checks, which branch protection can require before a merge. Each section of the hub owns one part of that path, and the security questions sit where they meet: ```yaml on: { push: { branches: [main] }, pull_request: {} } permissions: { contents: read } # every job starts read-only jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@<full-commit-sha> # immutable ref - run: make test deploy: needs: test # waits for test, skips if it fails if: github.event_name == 'push' # never from a pull request environment: production # reviewers gate it; env secrets permissions: { contents: read, id-token: write } # OIDC, this job only runs-on: ubuntu-latest steps: [ { run: ./deploy.sh } ] ``` No line carries the design alone. The environment gate protects the deploy only while nothing else can reach the same credentials, the pinned ref matters only because the token behind it can do something, and the job-level `permissions` block replaces the workflow's rather than adding to it, which is why it repeats `contents: read`. Runners, caches and matrices then decide how fast and how widely the same graph executes, not what it is allowed to do.

  1. Workflow Syntax & Triggers →

    Where every run begins: events, branch, tag and path filters, schedules, manual dispatch and concurrency.

  2. Jobs & Steps →

    The job graph, uses against run, outputs between jobs and the conditions that decide which steps execute.

  3. Secrets & Environments →

    Token scopes, secret precedence, environment gates and OIDC — the credential model senior rounds probe hardest.

  4. Actions & Marketplace →

    Action types, how uses references them, and why third-party code needs pinning and a narrow token.

  5. Runners →

    Hosted against self-hosted machines, and why a persistent shared runner is a security and flakiness risk.

  6. Caching & Matrix Builds →

    Speed and breadth: cache keys that hit and invalidate correctly, matrix shaping and the cost of every dimension.

  • Assuming a workflow skipped by a paths filter counts as passing: it reports nothing, so a required check waits indefinitely — see paths filters in a monorepo.

  • Treating log masking as protection: any code in a step that can read a secret can also send it elsewhere, masked output or not.

  • Pinning a third-party action to a version tag and calling it locked; tags can be moved, and a full commit SHA is what fixes the code you reviewed.

  • Writing a cleanup or notification step with no explicit if: and then wondering why it never runs after a failure.

  • Switching to pull_request_target to get secrets on fork pull requests, then checking out and executing the fork's code in that privileged context.

  • Relying on whatever default GITHUB_TOKEN scopes the repository or organization happens to have, instead of declaring permissions in the workflow.

  • Attaching a persistent self-hosted runner to a public repository, where a pull request from any fork can propose code for it to run.

  • Building a cache key from the operating system alone, so it hits on every run and never picks up changed dependencies.

  • Adding matrix dimensions without asking which failures each one has ever caught, while the job count multiplies with every new axis.

GitHub Actions has no release numbers of its own; the platform changes continuously, and this guide describes it as of 2026. A few changes still decide which answer is current: - **Step outputs** were once set by printing a `set-output` workflow command. It was deprecated in 2022 in favour of appending to the file named by `$GITHUB_OUTPUT`, and old workflows still carry the earlier form. - **Default `GITHUB_TOKEN` scopes** became read-only for new repositories and organizations in 2023. Older ones can still issue a broadly writable token, which is why declaring `permissions` remains the expected answer. - **JavaScript actions** depend on Node runtimes that GitHub retires on a schedule, so an action pinned long ago can break when its runtime leaves the runners. - **The `-latest` runner labels** move to newer images over time, so a workflow that pins nothing can change underneath you.

Interviewers expect you to place GitHub Actions, not only write it. Its advantage is proximity: it lives beside the code, pull requests and permissions it acts on. GitLab CI/CD offers the same built-in model for GitLab. Jenkins is the self-hosted, plugin-driven option teams keep when they need full control of the build server or carry years of existing pipelines. For delivery, Actions is push-based: the pipeline reaches out and deploys. GitOps controllers such as Argo CD and Flux instead pull desired state from a repository into a cluster, and many teams combine the two — Actions builds, tests and updates manifests, the controller applies them. Around the workflow file sit Dependabot or Renovate to keep pinned action references current, and linters such as actionlint to catch errors before a run. [CI/CD and GitOps](/topics/cloud-cicd) covers the platform-neutral ideas.

explore

report an issue with this guide →

questions

page 1 of 2

In a GitHub Actions step, what does uses: reference, and what forms can that reference take?

level: juniorimportance: must knowfreq 75%

answer

  1. The alternative to run:
  2. A ref is never optional
  3. One form points inside your own repository
  4. There is a container form too
  5. Inputs live under a sibling key

basics

~10 s

uses: runs a packaged action instead of a shell command. It points at a public repository plus a ref (actions/checkout@v4), a path inside your own repository (./.github/actions/setup), or a Docker image (docker://alpine:3.19).

solid answer

~40 s

A step either runs a command with `run:` or invokes a packaged action with `uses:`. The reference has three shapes: **`owner/repo@ref`** for an action in another repository, where `ref` is a tag, branch or commit SHA and is mandatory (`actions/checkout@v4`); **`owner/repo/path@ref`** when the action lives in a subdirectory of that repository; and **`./path`** for an action in the current repository, which requires that the repository has already been checked out. `docker://image:tag` runs a published container image directly as the action. Parameters go under `with:` and are declared by the action's `action.yml`. Whatever the form, `uses:` means "run someone's packaged code in my job" — which is why the ref you choose is a supply-chain decision, not cosmetic.

code

yaml · 9 lines
yaml
steps:
  - uses: actions/checkout@v4              # owner/repo@ref
  - uses: acme/tools/setup-cli@v2          # subdirectory of a repo
  - uses: ./.github/actions/configure      # local, needs checkout first
    with:
      environment: staging
  - uses: docker://alpine:3.19             # published container image
    with:
      args: echo hello

go deeper

for a junior

Be able to write both step kinds and recall the owner/repo@ref form plus the fact that the ref is mandatory.

for a middle

Explain all the reference forms including local and docker://, how with: maps to declared inputs, and how step outputs are read back via an id.

for a senior

Frame uses: as executing third-party code inside your job and connect the ref choice to supply-chain exposure and reproducibility.

for a principal

Own the organisational convention: where shared actions live, whether they are vendored locally or referenced across repositories, and how references are pinned and updated at scale.

## Two kinds of step GitHub Actions steps come in exactly two flavours. `run:` executes a shell command on the runner. `uses:` invokes an **action** — a reusable unit of code packaged with an `action.yml` metadata file that declares its inputs, outputs and how it executes. ```yaml steps: - uses: actions/checkout@v4 - run: ./gradlew build ``` A single step cannot have both keys. ## The reference forms **Public or private repository:** `owner/repo@ref`. The ref is required — there is no implicit default branch — and may be a release tag (`@v4`), a branch (`@main`), or a full commit SHA. GitHub fetches that repository at that ref and executes the action defined by the `action.yml` at its root. **Subdirectory:** `owner/repo/path/to/action@ref`. Many organisations keep several actions in one repository; the path selects which one, and the ref still applies to the whole repository. **Local:** `./.github/actions/setup-toolchain`. The path is relative to the repository root of the *workspace*, so the step only works after `actions/checkout` has run. This is the cheapest way to factor repeated steps out of one repository's workflows without publishing anything. **Docker image:** `docker://alpine:3.19` runs a published image directly, with `with.args` becoming the container arguments. Docker-based steps only work on Linux runners. ## Passing data Inputs go under `with:`, keyed by the names the action's `action.yml` declares; unknown keys are ignored rather than rejected, which is a classic source of "my setting had no effect". Environment variables go under `env:`. Outputs come back via the step's `id`: ```yaml - id: meta uses: acme/build-meta@v2 with: prefix: release - run: echo "tag is ${{ steps.meta.outputs.tag }}" ``` ## Why the ref matters The reference is the entire security boundary. `uses:` downloads and executes code from another repository *inside your job*, with access to the job's environment, the workspace, the network, and whatever secrets you have passed in. A mutable ref like `@main`, or a major tag like `@v4` that its owner re-points on each release, means the code you run today is not necessarily the code you reviewed. Pinning to a full commit SHA freezes it. That is why security-conscious repositories pin third-party actions by SHA and keep the human-readable version in a trailing comment. ## Common mistakes - Omitting the ref (`uses: actions/checkout`) — the workflow fails to parse the step. - Using a local `./` action before `actions/checkout`, so the path does not exist yet. - Assuming `with:` keys are validated: a typo'd input is silently dropped unless the action itself checks. - Believing an action is "just configuration". It is arbitrary code — JavaScript, a container, or a bundle of shell steps.

  • What happens if you reference a local action with ./ before running actions/checkout?
    The step fails because the path does not exist: the workspace is empty until checkout populates it. Local actions are read from the checked-out working tree, not fetched from GitHub, so any job using one must check out the repository first — including jobs that otherwise need no source code.
  • If you pass a with: key the action does not declare, what happens?
    Nothing visible. Undeclared inputs are simply not surfaced to the action, so a misspelled key silently takes no effect and the step runs with the default. The failure mode is a configuration that appears applied but is not, which is why reading the action's action.yml beats guessing input names.

saying these in an interview costs you the question

  • Omitting the ref after owner/repo
  • Treating an action as inert configuration
  • Using a local ./ action without checking out first
  • Assuming misspelled with: keys raise an error
  • Thinking run: and uses: can share one step

context

open as a page

In a GitHub Actions workflow step, what is the difference between uses: and run:?

level: juniorimportance: must knowfreq 78%

basics

~20 s

A step either invokes packaged code with uses:, which points at an action by repository, local path, or Docker image and takes inputs via with:, or executes shell commands with run:. One step cannot have both keys.

open as a page

In GitHub Actions, what does runs-on select, and what is a hosted ubuntu-latest runner?

level: juniorimportance: must knowfreq 80%

basics

~10 s

runs-on picks the machine a GitHub Actions job executes on, by label. ubuntu-latest requests a GitHub-hosted Linux virtual machine that is created fresh for that one job and destroyed when the job finishes.

open as a page

In GitHub Actions, how do you pass a repository secret into a step safely?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Reference it through the secrets context, as ${{ secrets.NAME }}, and map it into an env variable or an action input instead of splicing it into a shell command. GitHub masks the value in logs, but masking is redaction, not access control.

open as a page

In GitHub Actions, how do branches: and tags: filters on push behave?

level: juniorimportance: must knowfreq 68%

basics

~20 s

They are independent allow-lists over the pushed ref. A branches filter alone means tag pushes never trigger the workflow, and a tags filter alone means branch pushes never do; declaring both matches either. Patterns are globs, not regexes.

open as a page

In GitHub Actions, how do actions/cache's key and restore-keys differ?

level: middleimportance: must knowfreq 72%

basics

~20 s

key is the exact identifier looked up first and the name any new cache is saved under. restore-keys is an ordered list of prefixes tried only when key misses, restoring the newest partial match so the job starts from a warm but stale cache.

open as a page

In a GitHub Actions strategy matrix, what do include and exclude actually do?

level: middleimportance: must knowfreq 68%

basics

~20 s

The matrix's array variables expand to the cross product of every combination, one job each. exclude removes combinations from that product, and include then adds extra values to matching combinations or appends whole new ones. Exclude is processed first, so include can add a combination back.

open as a page

How does one GitHub Actions job pass a computed value to a later job?

level: middleimportance: must knowfreq 66%

basics

~10 s

A step appends name=value to the file at $GITHUB_OUTPUT and carries an id. The job republishes it under its outputs: map, and any job that lists it in needs: reads it as needs.<job_id>.outputs.<name>.

open as a page

In a GitHub Actions workflow, what does a job's needs: key change about execution?

level: middleimportance: must knowfreq 80%

basics

~20 s

Jobs in a GitHub Actions workflow run in parallel by default. Adding needs: makes a job wait for the listed jobs and, by default, skip entirely if any of them fails, turning the job list into a dependency graph.

open as a page

In GitHub Actions, what does the permissions key do to the GITHUB_TOKEN?

level: middleimportance: must knowfreq 70%

basics

~20 s

It sets the API scopes the job's GITHUB_TOKEN carries. Listing any scope drops every scope you did not list to none, so permissions: contents: read yields a token that can clone but cannot write issues, packages, or code.

open as a page

In GitHub Actions, what does a concurrency group with cancel-in-progress do?

level: middleimportance: must knowfreq 66%

basics

~20 s

A concurrency group allows only one run of that group at a time. With cancel-in-progress true, a new run cancels the one already running; with it false or absent, the new run waits, and only the newest waiting run survives.

open as a page

In GitHub Actions, what does adding workflow_dispatch to on: enable?

level: juniorimportance: should knowfreq 58%

basics

~20 s

It makes the workflow runnable on demand — from the Actions tab, the REST API, or gh workflow run — against any branch or tag you choose, with optional typed inputs you declare in the workflow file.

open as a page

What are the three types of GitHub Action you can author, and how do they differ?

level: middleimportance: should knowfreq 58%

basics

~20 s

Composite actions bundle a sequence of workflow steps in YAML. JavaScript actions run a Node script directly on the runner. Docker container actions run your code inside an image. The action.yml runs.using key picks which, and Docker actions run only on Linux runners.

open as a page

Why pin a third-party GitHub Action to a full commit SHA instead of a version tag?

level: middleimportance: should knowfreq 58%

basics

~20 s

Git tags are mutable and major tags like v4 are deliberately re-pointed on each release, so uses: acme/action@v4 can execute different code tomorrow. A full commit SHA is immutable, so the code you reviewed is the code that runs.

open as a page

In a GitHub Actions matrix build, what does setting fail-fast: false change?

level: middleimportance: should knowfreq 54%

basics

~20 s

By default a failing matrix job cancels every other in-progress and queued job in that matrix. fail-fast: false lets all legs run to completion, so one pull request shows every platform or version that is broken instead of only the first one to fail.

open as a page

What does timeout-minutes do in a GitHub Actions job, and what is its default?

level: middleimportance: should knowfreq 45%

basics

~20 s

timeout-minutes caps how long a GitHub Actions job may run before the runner terminates it and the job is reported as failed. The default is 360 minutes, and the key can also be set on an individual step.

open as a page

How do you register a self-hosted GitHub Actions runner, and how does it receive jobs?

level: middleimportance: should knowfreq 62%

basics

~20 s

You download GitHub's runner package onto the machine and run config.sh with the target URL and a short-lived registration token, then run.sh or the service installer. The runner then polls GitHub over outbound HTTPS for jobs; GitHub never connects inbound.

open as a page

In GitHub Actions, which secret wins when org, repo, and environment names collide?

level: middleimportance: should knowfreq 62%

basics

~20 s

The most specific scope wins: an environment secret overrides a repository secret, which overrides an organization secret of the same name. The environment value only applies to jobs that declare that environment; other jobs still see the repository or organization value.

open as a page

In GitHub Actions, what are the rules for a schedule: cron trigger?

level: middleimportance: should knowfreq 52%

basics

~20 s

Five-field POSIX cron, always interpreted in UTC, with a shortest usable interval of five minutes. Scheduled runs always use the workflow file and latest commit on the default branch, and start times are best-effort, not guaranteed.

open as a page

What can a malicious third-party GitHub Action do inside your job, and how do you contain it?

level: seniorimportance: should knowfreq 47%

basics

~20 s

An action runs as arbitrary code in your job: it can read the workspace, every environment variable including secrets you expose, use the GITHUB_TOKEN against your repository, and send data anywhere the runner can reach. Containment means least-privilege token, minimal secret exposure, pinned refs, and job isolation.

open as a page

How do you build a GitHub Actions matrix at runtime from a previous job's output?

level: seniorimportance: should knowfreq 46%

basics

~20 s

A setup job computes a JSON array, writes it to GITHUB_OUTPUT, and declares it as a job output. The downstream job depends on it with needs and sets strategy.matrix to fromJSON of that output, which parses the string into the matrix structure.

open as a page

A GitHub Actions cache reports cache-hit true but builds use stale dependencies. Why?

level: seniorimportance: should knowfreq 48%

basics

~20 s

The actions/cache key contains nothing that changes when dependencies change, so it matches forever. Cache entries are immutable, so the first archive saved under that key is served indefinitely and no new one can replace it.

open as a page

In GitHub Actions, why is a final cleanup step skipped when an earlier step fails?

level: seniorimportance: should knowfreq 62%

basics

~20 s

Every GitHub Actions step carries an implicit condition that all previous steps succeeded, so one failure skips the rest of the job. A cleanup step must opt out with an explicit if:, such as if: always() or if: !cancelled().

open as a page

Why are self-hosted GitHub Actions runners risky for public repositories, and what limits the exposure?

level: seniorimportance: should knowfreq 55%

basics

~20 s

A pull request from any fork can propose workflow code that executes on your self-hosted runner, and a persistent runner keeps state between jobs — so one hostile run can steal or poison what the next job uses. GitHub's own guidance is not to use self-hosted runners on public repositories.

open as a page

How do you configure a GitHub Actions job to assume a cloud role through OIDC?

level: seniorimportance: should knowfreq 56%

basics

~20 s

Grant the job permissions: id-token: write, use the provider's login action to exchange the GitHub-issued token for temporary credentials, and on the cloud side trust GitHub's issuer while pinning the sub claim to the specific repository, branch, or environment.

open as a page

What do GitHub Actions environment protection rules gate, and how does the job behave?

level: seniorimportance: should knowfreq 58%

basics

~20 s

They gate the start of any job that declares that environment. Required reviewers, a wait timer, and deployment branch or tag policies must all pass before the job is dispatched, and only then are the environment's secrets issued to it.

open as a page

How does pull_request_target differ from pull_request in GitHub Actions?

level: seniorimportance: should knowfreq 60%

basics

~20 s

pull_request_target runs the base branch's workflow file in the base repository's context, so repository secrets and a writable GITHUB_TOKEN are available even for fork pull requests. pull_request runs from a fork with no secrets and a read-only token.

open as a page

When would you split a GitHub Actions pipeline into more parallel jobs rather than more steps?

level: principalimportance: should knowfreq 42%

basics

~20 s

Split when the work is genuinely independent and long enough to repay a new runner, or when a stage needs its own permissions or re-run granularity. Keep work in one job when steps share a large workspace, because separate jobs share no filesystem.

open as a page

How would you keep production credentials unreachable from GitHub Actions runs on fork pull requests?

level: principalimportance: should knowfreq 40%

basics

~20 s

Rely on the default that fork pull requests get no secrets and a read-only token, keep privileged work out of pull-request-triggered workflows entirely, and put real credentials behind environments with reviewers so any privileged job must be approved before it starts.

open as a page

In a GitHub Actions monorepo, how would you design paths filters without blocking merges?

level: principalimportance: should knowfreq 40%

basics

~20 s

A workflow skipped by a paths filter reports no status, so a required check stays pending and the pull request cannot merge. Either pair it with a same-named stub workflow on the inverse filter, or run always and skip inside with job conditions.

open as a page

showing 1–30 of 32