TL;DR: A push fires a webhook, GitHub matches it against workflow triggers and queues each job, then assigns jobs to runners advertising a matching label. The runner executes configured steps, including checkout and artifact upload only if requested by the workflow. Runner persistence depends on the selected hosting and lifecycle model.
How to approach it
Split the system into control plane and workers in your first sentence, then walk the lifecycle in order. Expect follow-ups on hosted versus self-hosted runners and artifacts versus cache, so leave threads for both.
A strong answer
The push delivers a webhook event, and GitHub evaluates every workflow file on the pushed ref against its triggers: the on: block filters by event type, branch and path. Matching workflows start a run. GitHub then parses the job graph, the needs: keys, and evaluates dependencies and job conditions before scheduling eligible jobs; failed dependencies normally skip downstream jobs unless conditions change that behaviour. Jobs are the unit of scheduling. Steps within a job use the assigned runner, with working-directory and container settings controlling their execution context, while two jobs may land on entirely different machines, which is why passing data between them requires declared artifacts rather than files on disk.
Runner assignment goes through labels. Hosted pools advertise labels such as ubuntu-latest; a job asking for runs-on: [self-hosted, gpu] waits until a runner carrying both labels is idle. Self-hosted runners register against the repo or organisation and poll for work, so a job can wait without starting when no permitted runner matches. Check the queue reason and documented queue limits.
The runner then materialises the environment: checkout, container or toolchain setup, and actions prepared according to their source, including repository actions, local actions and container actions. Careful organisations pin third-party actions to a commit SHA, since a mutable tag is remote code execution with a friendly name. Repository secrets become available according to the event and protection rules; a workflow must explicitly reference them as action inputs or environment variables. Keep them out of runner images.
Two consequences of disposability matter in practice. For an ephemeral runner, local state is discarded after the job, so anything worth keeping is uploaded explicitly: actions/upload-artifact writes to run-scoped storage, and a dependent job pulls it down with download-artifact once needs: resolves. Caches are opt-in and use workflow-defined keys; actions/cache does not automatically derive a complete content-addressed build key. Include the dependency lockfile and relevant runtime/platform versions. Without a usable hit, a cold runner installs dependencies again.
Hosted versus self-hosted is the trade interviewers want you to reason through:
| Concern | Hosted runners | Self-hosted runners |
|---|---|---|
| Capacity | Managed pool; quota and capacity can queue jobs | You own provisioning and queueing |
| Cache warmth | Cold each job, pay restore cost | Warm disk possible |
| Trust boundary | Managed isolation; verify runner type | Your network, your credentials nearby |
| Maintenance | Images rotated for you | Patching, disk cleanup, zombie processes |
| Cost shape | Per-minute billing | Owned capacity plus operations cost |
Security settles it for public repositories: prefer hosted runners for untrusted public-fork builds. Checkout alone fetches data; install, build and test steps execute that data as code. A self-hosted design needs single-use isolation, restricted network access and no trusted credentials; ephemerality alone does not protect reachable internal services.
Worked diagnosis: eligible hardware, ineligible job
Consider a fictional job requesting self-hosted and linux-arm64. The organisation has six idle x86 runners and one ARM runner in a group that this repository cannot use. Adding more x86 capacity will not help. Compare the requested labels with runner registration, then check runner-group access and whether the required dependency jobs and environment approvals have completed. A blocked dependency and a queued runnable job are different states.
After correcting eligibility, the build job writes release.tar. A second job reports that the file is missing. needs: build establishes ordering; it does not transfer the first job's filesystem. Upload the intended output, record its digest and have the dependent job download and verify it. Use small job outputs for metadata such as that digest, not for a whole archive.
Finally, repeat the run on a cold runner. If success depends on an undeclared preinstalled tool or a persistent workspace, record that dependency and either provision it explicitly or remove it. A cache should shorten preparation; it should not be the only surviving copy of a required build input.
What interviewers probe next
"Your job sat queued for twenty minutes. Where do you look?" Runner availability against the requested labels first, then organisation-level concurrency limits, then whether a job earlier in the needs: chain is stuck.
"Artifacts versus cache?" Artifacts move outputs between jobs and to humans, and expire on retention policy. Cache speeds up repeated work and evicts by inactivity and size. They solve different problems, and swapping them produces confusing pipelines.
"How do you harden workflows triggered by fork PRs?" Read-only token, no secret exposure, actions pinned by SHA, and untrusted code confined to an isolated runner without trusted network or credential access.
Common mistakes
Reciting YAML keywords without the runtime underneath, which reads as documentation recall rather than operating experience.
Assuming state persists between jobs in the same run, then shipping a pipeline that fails only in CI because a file never travelled.
Connecting a persistent self-hosted runner to a public repository and hoping. Untrusted code can persist on that host or reach adjacent services unless the design provides isolation.