GitHub Actions
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 pageshowhide
guide
overview
~1 minGitHub 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.
- Workflow Syntax & Triggers →
Where every run begins: events, branch, tag and path filters, schedules, manual dispatch and concurrency.
- Jobs & Steps →
The job graph, uses against run, outputs between jobs and the conditions that decide which steps execute.
- Secrets & Environments →
Token scopes, secret precedence, environment gates and OIDC — the credential model senior rounds probe hardest.
- Actions & Marketplace →
Action types, how uses references them, and why third-party code needs pinning and a narrow token.
- Runners →
Hosted against self-hosted machines, and why a persistent shared runner is a security and flakiness risk.
- 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
pathsfilter 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_targetto get secrets on fork pull requests, then checking out and executing the fork's code in that privileged context.Relying on whatever default
GITHUB_TOKENscopes the repository or organization happens to have, instead of declaringpermissionsin 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
- Workflow Syntax & Triggers6 questions
- Jobs & Steps6 questions
- Runners4 questions
- Actions & Marketplace4 questions
- Secrets & Environments6 questions
- Caching & Matrix Builds6 questions
questions
page 2 of 2What are ephemeral and just-in-time GitHub Actions runners, and when would you use them?
basics
~20 sAn ephemeral GitHub Actions runner accepts exactly one job and then deregisters, so nothing carries over. A just-in-time runner goes further: GitHub's API issues a single-use configuration for a runner that has not existed before, so autoscalers can create one per queued job.
Your GitHub Actions matrix has grown to 60 jobs per pull request. How do you decide what to keep?
basics
~20 sRank each dimension by the failures it has actually caught, keep on every pull request only the legs that catch defects a merge would ship, and move the rest to a scheduled or pre-release run. Then attack the per-leg cost with caching and change detection.
showing 31–32 of 32