GitHub Actions is a platform for running code in response to things that happen in a repository.
That is deliberately broader than “CI/CD”. Continuous integration is the most common use and it is one use among many: Actions also labels issues, publishes releases, runs scheduled maintenance, enforces policy, and deploys infrastructure. The primitive is event → code, and CI is one shape it takes.
The architecture
Section titled “The architecture”A vertical chain: an event occurs in the repository, GitHub matches it against workflow files, a workflow run is created containing jobs, each job is assigned a runner, and the runner executes the job's steps, which are either shell commands or actions.
Each layer has a job of its own, and knowing which one a problem belongs to is most of debugging.
An event is something that happened: a push, a pull request opening, a release publishing, a cron schedule firing, a person clicking a button. GitHub emits these regardless of whether anything is listening.
A workflow is a YAML file in .github/workflows/. Its on: block declares which events it
responds to. A repository can have many, and one event can start several.
A workflow run is one execution. It has an ID, logs, a conclusion, and it appears in the Actions
tab and in gh run list.
A job is a unit of work that gets its own runner. Jobs in a workflow run in parallel by
default — a fact that surprises people, and which needs: exists to constrain.
A runner is the machine. GitHub-hosted runners are fresh virtual machines created per job and destroyed afterwards. Self-hosted runners are machines you provide.
A step is one thing inside a job: either run: (a shell command) or uses: (an action).
An action is reusable code a step invokes. actions/checkout clones your repository;
actions/setup-python installs Python. Actions are the reuse mechanism, and they are also somebody
else’s code running in your job.
The smallest complete example
Section titled “The smallest complete example”name: CI
on: push: branches: [main]
permissions: contents: read
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - run: echo "Testing commit $GITHUB_SHA"Line by line:
| Line | Meaning |
|---|---|
name: | What the workflow is called in the interface |
on: | Which events start it — pushes to main |
permissions: | What GITHUB_TOKEN may do — read the repository, nothing more |
jobs: | The jobs this workflow contains |
test: | The job’s ID, used by needs: and as its status check name |
runs-on: | Which runner — a GitHub-hosted Ubuntu machine |
steps: | What the job does, in order |
uses: | Run an action — here, clone the repository |
run: | Run a shell command on the runner |
$GITHUB_SHA is one of the default environment variables GitHub sets on every runner. There are
many, and they are how a workflow learns what triggered it.
What Actions is used for
Section titled “What Actions is used for”CI is the majority case and it is worth seeing the range, because the platform is more general than its reputation.
Continuous integration. Build, lint, test and scan on every pull request. Covered in the CI cluster.
Continuous delivery and deployment. Package an artifact, deploy it to an environment, promote it to production. The CD cluster.
Repository automation. Labelling issues by content, welcoming first-time contributors, closing stale pull requests, syncing files across repositories. This overlaps with what GitHub Apps do, and the choice between them is discussed below.
Scheduled maintenance. Dependency audits, link checks, backups, reports. A schedule: trigger
plus a script.
Release automation. Building artefacts on a tag, generating notes, publishing a Release, attaching binaries and provenance.
Policy enforcement. Validating that a changed file meets a standard, that a commit message matches a convention, that a required label is present.
Runners
Section titled “Runners”A job needs a machine, and the two kinds differ in more than who pays.
GitHub-hosted runners are ephemeral virtual machines. GitHub creates one per job from a maintained image, runs the job, and destroys it. Nothing survives; nothing is shared between jobs.
Current labels span several operating systems and both architectures:
runs-on: ubuntu-latest # Linux x64runs-on: ubuntu-24.04-arm # Linux ARM64runs-on: windows-latest # Windows x64runs-on: windows-11-arm # Windows ARM64runs-on: macos-latest # macOS on Apple siliconSelf-hosted runners are machines you provide, register and maintain. They exist for three real reasons: hardware GitHub does not offer, software licensing that cannot move, and access to a private network. They are covered in Self-Hosted Runners and Secure Self-Hosted Runners, and the security responsibility they carry is substantial.
Do not self-host to reduce your bill. Self-host when hosted runners cannot do the job.
How Actions compares
Section titled “How Actions compares”Three comparisons come up constantly, and each clarifies something.
Actions versus Jenkins
Section titled “Actions versus Jenkins”Jenkins is a server you run. That is the whole difference and it propagates everywhere.
| GitHub Actions | Jenkins | |
|---|---|---|
| Infrastructure | Managed, or your runners | Yours, always |
| Configuration | YAML in the repository | Jobs, or a Jenkinsfile |
| Runner lifecycle | Ephemeral by default | Usually persistent agents |
| Reuse | Actions, reusable workflows | Plugins, shared libraries |
| Access control | Repository and organisation | Its own model |
| Upgrades and patching | GitHub | You |
Jenkins wins where you need total control, unusual hardware, or integration with systems that predate the cloud. Actions wins on operational cost and on proximity — the automation lives with the code it automates, and it is reviewed in the same pull request.
Neither is a general improvement on the other. Migrating a working Jenkins installation to Actions for its own sake is a large project with a modest payoff.
Actions versus a shell script
Section titled “Actions versus a shell script”A legitimate question: your CI is three commands, so why not a script?
You should still have the script. The distinction is that Actions provides what a script does not: a trigger (something must run it), a runner (something must own the machine), credentials (scoped, short-lived, not on someone’s laptop), reporting (a status check on the pull request), and history.
The good arrangement keeps the logic in the script and uses the workflow to invoke it:
- run: ./scripts/test.shThat is testable locally, portable if you leave GitHub, and reviewable by people who do not know workflow YAML. A workflow that inlines forty lines of shell is harder to test and harder to move.
Actions versus GitHub Apps
Section titled “Actions versus GitHub Apps”Both automate GitHub, and they suit different shapes of problem.
| GitHub Actions | GitHub App | |
|---|---|---|
| Trigger | Repository events, schedules | Webhooks it subscribes to |
| Runs on | GitHub’s runners, or yours | Your infrastructure |
| Scope | Its own repository | Every repository it is installed on |
| Identity | The workflow’s token | The App |
| Cost | Runner minutes | Your hosting |
| Best for | Anything touching the repository’s code | Cross-repository, multi-tenant integrations |
The dividing line is scope. A workflow acts on its own repository — the built-in token cannot reach another. Anything spanning repositories needs an App, or a workflow that mints an App token.
Limits worth knowing
Section titled “Limits worth knowing”Actions has quotas, and knowing they exist prevents surprises.
Minutes. Free for public repositories on standard runners. For private repositories, from an included allowance, then billed — with Windows and macOS costing multiples of Linux.
Storage. Artifacts and caches consume storage against an allowance, with retention policies.
Concurrency. How many jobs run simultaneously depends on plan and runner type.
Duration. Individual jobs and whole runs have maximum durations.
Common mistakes
Section titled “Common mistakes”Forgetting actions/checkout. The runner starts empty; your code is not there.
Assuming jobs share a filesystem. Each job gets its own runner. Passing files between them needs artifacts; passing values needs outputs.
Assuming jobs run in order. They are parallel unless needs: says otherwise.
Leaving permissions at the default. Declare them; one line.
Treating Actions as only CI. Scheduled and event-driven automation is often where it pays most.
Self-hosting for cost. The operational and security burden is the real price.
Exercise
Section titled “Exercise”- Create a disposable public repository.
- Add
.github/workflows/hello.ymlcontaining the minimal example above. - Commit and push to
main, then rungh run listandgh run view --log. - Delete the
actions/checkoutstep and push again. Observe thatlsstill works — the runner exists, your files do not. - Add a second job with no
needs:and confirm both start at once. - Add
needs:to the second job and confirm it now waits.
Steps 4 and 5 demonstrate the two facts that cause the most early confusion, and both are much more convincing observed than read.
The vocabulary, compared
Section titled “The vocabulary, compared”Terms from other CI systems, mapped:
| Elsewhere | GitHub Actions |
|---|---|
| Jenkins pipeline / GitLab pipeline | Workflow |
| Jenkins stage / GitLab stage | Job (parallel by default; use needs for order) |
| Jenkins agent / GitLab runner | Runner |
| Jenkins step / GitLab script line | Step |
| Jenkins shared library / GitLab include | Reusable workflow or composite action |
| Jenkins plugin | Action |
GitLab rules: | on: filters plus if: |
| CircleCI orb | Action, or reusable workflow |
The mapping that misleads most is stage → job. In most systems stages are sequential by
definition; in Actions, jobs are parallel by default and ordering is opt-in through needs. A
migrated pipeline that behaves unexpectedly is very often this — everything running at once because
nothing declared a dependency.
What you learned
Section titled “What you learned”- GitHub Actions runs code in response to repository events; CI is one use among several.
- The layers are event, workflow, run, job, runner, step, and command-or-action.
- The platform and a reusable action share a name and are different things.
- Runners are ephemeral and start empty —
actions/checkoutis what puts your code there. - Jobs are isolated and parallel by default.
- Self-hosting is for capability, not cost.
- Actions acts on its own repository; cross-repository automation needs an App.
Reading the Actions tab
Section titled “Reading the Actions tab”Every run has a structure the interface renders and the CLI exposes, and knowing the vocabulary makes debugging much faster.
gh run list --limit 10gh run view RUN_IDgh run view RUN_ID --log-failedgh run watch RUN_ID --exit-statusA run has a conclusion: success, failure, cancelled, skipped, timed_out,
action_required or neutral. Those are distinct states and conflating them causes confusion —
a skipped job did not fail, and a run waiting on a deployment approval is action_required rather
than stuck.
Within a run, each job has its own conclusion, and within a job each step does. A failed run means some job failed; a failed job usually means one step did.
gh run view RUN_ID --json jobs \ --jq '.jobs[] | {name, conclusion, steps: [.steps[] | select(.conclusion != "success") | .name]}'That prints only the steps that did not succeed, per job, which is the fastest way to locate a failure in a run with many jobs.
What a workflow can and cannot do
Section titled “What a workflow can and cannot do”Worth stating plainly, because the boundaries are not obvious.
It can read and write its own repository (subject to permissions), call the GitHub API as itself, reach the internet, install software on the runner, and store artifacts and caches.
It cannot reach another repository with the built-in token, persist anything on the runner between jobs, exceed its plan’s concurrency, or run longer than the platform’s maximum job duration.
It should not be given credentials it does not need, execute untrusted code with secrets in reach, or be the only copy of anything.
That last point is worth expanding. A workflow is not a backup: artifacts expire, caches are evicted, and logs are retained for a limited period. Anything you need to keep belongs in a Release or an artefact registry.
When not to use GitHub Actions
Section titled “When not to use GitHub Actions”An honest section, because “use Actions for everything” is a common failure.
When the work belongs on a schedule you control precisely. Scheduled workflows are queued and can be delayed; they are unsuitable for anything time-critical.
When it needs to run continuously. Actions runs jobs, not services. A long-running listener is a service, not a workflow.
When the compute is the point. Long-running builds on private repositories consume billed minutes quickly, and a dedicated build system may be cheaper at scale.
When it must not depend on GitHub’s availability. A deployment pipeline that cannot run during a GitHub incident is a real operational consideration.
When the logic belongs in the application. Business logic in a workflow is logic that cannot be tested locally and is invisible to anyone reading the codebase.
Where Actions fits in the pillar sequence
Section titled “Where Actions fits in the pillar sequence”The three previous pillars built a repository where changes arrive as pull requests and merge under policy. Actions is what makes that policy meaningful.
A required review says a human looked. A required check says the code builds, the tests pass and the scanner found nothing — and that check is a workflow. Without Actions, repository governance can only enforce process; with it, governance can enforce correctness.
That is the sense in which this pillar completes the sequence: Pillar 3 gave you the gate, and this one gives you something worth putting behind it.
Summary
Section titled “Summary”GitHub Actions runs code in response to repository events. Workflows contain jobs, jobs run on runners, and steps are either shell commands or reusable actions.
The two facts that cause the most early confusion are that runners start empty — nothing is checked
out until actions/checkout does it — and that jobs share nothing, including the filesystem, even
when one depends on another.
Everything else in this pillar is that model applied with increasing care.
Related lessons
Section titled “Related lessons”Check your understanding
3 questions — each one asks you to predict what Git or GitHub will do, not to recall a flag.