How GitHub Actions works
GitHub Actions runs CI/CD workflows on ephemeral VMs called runners. Learn how workflow triggers, runner registration, job execution, and artifact storage work, and the architectural difference between GitHub-hosted and self-hosted runners.
The Problem Statement
Interviewer: "Walk me through what happens when you push a commit to a GitHub repository that has a CI pipeline configured with GitHub Actions. Start from the git push and end at the test results appearing on the pull request."
This question comes up surprisingly often in platform engineering and DevOps interviews. It tests whether you understand event-driven architecture in practice, how distributed job scheduling works at scale, and where the security boundaries are in hosted CI systems. Most candidates describe the YAML syntax and move on. Strong candidates trace the actual system: what service generates the event, which component picks it up, how the runner is selected, and what happens when everything is done.
Clarifying the Scenario
Before answering, take 30 seconds to narrow the scope.
You: "A few quick questions. Are we talking about GitHub-hosted runners or self-hosted ones? And is this a simple single-job workflow or does it have matrix builds and job dependencies?"
Interviewer: "GitHub-hosted. And let's say it's a standard workflow with a build job and a test job that depends on the build."
You: "Got it. And should I cover the artifact passing between jobs, or just the job dispatch mechanism?"
Interviewer: "Cover the artifact passing too, that's an interesting part."
You: "Great. I'll structure this in four parts: how the event flows from the push to the Actions queue, how a runner picks up the job, how the job executes on the ephemeral VM, and how artifacts and results flow back."
This is the answer structure that covers the hidden rubric: event routing, distributed scheduling, ephemeral compute, and storage integration.
A Useful Mental Model
Think about GitHub Actions as three separate systems that talk to each other:
- The event pipeline: Git push triggers a webhook internally. GitHub's Actions service parses the workflow YAML and creates a workflow run record with individual job entries in a queue.
- The runner dispatch system: Runners long-poll a queue endpoint. When a job becomes available, an eligible runner claims it and receives a signed token granting access to the repository and secrets.
- The execution environment: A fresh virtual machine (or container) is provisioned, the repository is checked out, each step runs in sequence with isolated environment variables, and logs stream back to GitHub in real time.
- The artifact system: Jobs communicate through a temporary blob store. One job uploads, the next downloads. Final results are posted back as check suite annotations on the commit.
Understanding this separation matters because it explains why GitHub Actions has the failure modes it does (runner starvation, slow job pickup in burst conditions) and why the security model works the way it does.
Workflow Files and the Job Graph
The event loop is: a trigger selects a workflow, the Actions service parses its YAML, jobs whose dependencies are satisfied enter a queue, an eligible runner claims each job, and the runner reports logs and a final status. Jobs run in parallel by default; needs adds an edge to the job graph and makes one job wait for another. Steps within a job run sequentially on the same runner environment.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: build-output
- run: npm test
The second job still gets a fresh environment, so it explicitly checks out the repository and downloads the artifact. A workflow file describes the graph and the steps; it does not provide a shared filesystem or a long-lived worker.
The Architecture
Here is the full picture from git push to green checkmark:
Walk through each node in order.
The push hits GitHub's git receive-pack service, which accepts the packfile and updates the ref. This immediately fires an internal push event to the webhook dispatcher. The Actions service subscribes to these events, reads every workflow file in .github/workflows/ that has on: push or a matching branch filter, and creates one workflow run per matching file. Each job in the workflow becomes a separate entry in the job queue, with metadata recording the required runs-on label (for example, ubuntu-latest), the job's dependency graph (needs:), and the secrets it is allowed to access.
On the other side of the queue, GitHub runs a fleet of runner VMs. Each idle runner long-polls a job endpoint roughly every two seconds. When a job matches the runner's labels, the queue assigns it to that runner. The runner receives a short-lived ACTIONS_TOKEN that is scoped to only that workflow run, not the whole repository.
The runner provisions a fresh VM (or container), runs the steps in order, streams logs back over HTTPS, and then terminates the VM. The next job that runs on the same runner type gets a completely fresh machine with no filesystem state carried over.
Why ephemeral VMs matter
Ephemeral runners prevent state leakage between runs and between repositories. If a malicious dependency poisons the npm cache on a shared runner, every subsequent job on that machine is at risk. Fresh VMs eliminate this class of attack. GitHub Larger Runners and self-hosted runners can be persistent, which is why they require careful isolation.
Runner Architecture and Job Dispatch
This is the most architecturally interesting part of GitHub Actions, and the part most candidates skip over.
The core problem: how does a runner, sitting behind a NAT or firewall on GitHub's infrastructure (or on your own network for self-hosted), pick up jobs without GitHub needing to initiate an inbound connection to it?
The answer is long-polling over HTTPS. Runners maintain an outbound connection to a GitHub endpoint and wait for a response. When a job arrives, GitHub returns it in the poll response rather than pushing it. This means even a self-hosted runner inside a private VPC can receive jobs without any inbound firewall rules.
A few things here are worth highlighting. The ACTIONS_TOKEN that the runner receives is a JWT signed by GitHub with a short expiry (typically the duration of the job, capped at 6 hours). It uses the same permissions model as GitHub Apps: it can read the repository, write check results, and access the secrets that the workflow has been granted. When the job ends, the token is invalidated regardless of expiry. This is why you cannot store GITHUB_TOKEN and reuse it later.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.
Related Articles
Git stores everything as content-addressed objects: blobs, trees, commits, and tags. Understanding the object model, DAG structure, and ref mechanics explains why rebase, merge, and reset work the way they do.
How Kubernetes orchestrates containers: control plane components, the scheduler, Pod lifecycle, service discovery, rolling deployments, and how it self-heals on node failure.