What GitHub Actions is
GitHub Actions is GitHub's built-in automation platform. You describe automation as YAML workflow files committed to the repository, and GitHub runs them on runners in response to events — a push, a pull request, a schedule, a button press, an API call or almost any activity on GitHub. CI/CD is the obvious use, but the same machinery labels issues, publishes packages, rotates dependencies, deploys Pages sites and answers webhooks from other systems.
Because workflows live in the repository, they are versioned, reviewed and branched like code. That is the root of
many exam answers: a workflow change goes through a pull request, a workflow on a branch runs that branch's
version of the file, and protecting .github/workflows/ with CODEOWNERS is how you control who may change
automation.
The building blocks
flowchart LR EV["Event"] --> WF["Workflow run"] WF --> J1["Job A"] WF --> J2["Job B"] J1 --> R1["Runner<br/>fresh VM or container"] J2 --> R2["Runner<br/>fresh VM or container"] R1 --> S1["Steps<br/>run: scripts · uses: actions"] R2 --> S2["Steps<br/>run: scripts · uses: actions"]
| Term | What it is | Remember |
|---|---|---|
| Event | Activity that triggers a run: push, pull_request, schedule, workflow_dispatch, repository_dispatch, workflow_call, issues… | Declared under on:; many events have types and filters |
| Workflow | A YAML file in .github/workflows/ with one or more jobs | Only .yml or .yaml files in that exact directory run |
| Workflow run | One execution of a workflow, with a run ID, run number and attempt number | Re-running increments github.run_attempt, not run_number |
| Job | A set of steps that runs on one runner | Jobs run in parallel by default; needs: makes them sequential |
| Step | One run: script or one uses: action | Steps run in order; a failing step fails the job unless continue-on-error |
| Action | A reusable unit of steps: JavaScript, Docker container or composite | Referenced as owner/repo@ref, ./path or docker://image |
| Runner | The machine that executes a job — GitHub-hosted, larger, or self-hosted | Selected by runs-on labels, groups or both |
| Reusable workflow | A workflow with on: workflow_call, called as a job with jobs.<id>.uses | Called at job level; a composite action is used at step level |
| Environment | A named deployment target with protection rules, secrets and variables | A job opts in with environment: |
Anatomy of a workflow
Every workflow you meet on the exam is some arrangement of the same few top-level keys. Learn to read them in this
order: when does it run (on), with what power
(permissions), how many at once (concurrency), and then the
jobs and their dependencies.
name: CI # shown in the Actions tab
run-name: CI for ${{ github.ref_name }} by @${{ github.actor }}
on: # 1 · the events that start a run
push:
branches: [main]
pull_request:
paths: ['src/**', 'package*.json']
workflow_dispatch: # adds a "Run workflow" button
permissions: # 2 · the GITHUB_TOKEN's scopes
contents: read
concurrency: # 3 · one run per branch at a time
group: ci-${{ github.ref }}
cancel-in-progress: true
env: # 4 · variables for every job
NODE_ENV: test
jobs:
test: # 5 · a job: one runner, many steps
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7 # an action
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci # a script, in the default shell
- run: npm test
build:
needs: test # 6 · waits for test to succeed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: npm ci && npm run build
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
The highlighted ${{ … }} spans are expressions: GitHub evaluates them before the
text reaches the runner. Everything else in a run: line is handed to the shell as-is. Keeping that
boundary in mind is half of the security skill — an expression that injects untrusted text into a script is the
classic script-injection bug.
Two styles of pinning in these notes
First-party actions/* samples use major tags such as actions/checkout@v7 for readability.
Third-party actions are pinned to a full commit SHA with the version in a trailing comment, as the
security page recommends.
Where files live
| Path | What it holds |
|---|---|
.github/workflows/*.yml | Workflows, including reusable ones (on: workflow_call). Subdirectories are not scanned. |
.github/workflows/actions.lock | The workflow lockfile that records the exact commit of every action a workflow uses (generally available since 28 October 2026) |
action.yml / action.yaml | Action metadata. At the repository root for a published action; Marketplace listing requires it there. |
.github/actions/<name>/action.yml | A conventional home for a local action, used with uses: ./.github/actions/<name> after checkout |
.github repository → workflow-templates/ | Organisation workflow templates: a .yml plus a matching .properties.json (and an optional icon) |
.github/dependabot.yml | Dependabot version updates, including package-ecosystem: github-actions to keep action versions current |
.github/CODEOWNERS | Code owners — commonly used to require review for /.github/workflows/ |
When each part is evaluated
Many questions reduce to timing. A workflow is not one script run top to bottom: different parts are read at different moments, by different components, and some contexts simply do not exist yet when a key is evaluated.
flowchart LR A["Event fires<br/>workflow file read<br/>from its commit"] --> B["Run queued<br/>repo and org<br/>secrets read"] B --> C["Job starts<br/>environment rules,<br/>environment secrets,<br/>GITHUB_TOKEN minted"] C --> D["Job ends<br/>token revoked"]
| Moment | What happens | Consequence |
|---|---|---|
| Parse time | YAML parsed, anchors expanded, on: filters checked | Invalid YAML fails the run before any job starts, with an annotation on the run page |
| Run queued | run-name, workflow-level concurrency, repo and org secrets | run-name can use github and inputs, not env |
| Job scheduled | jobs.<id>.if, strategy, runs-on, job concurrency | These can read needs, vars, inputs, matrix — but not env or steps |
| Job starts | Environment rules, environment secrets, GITHUB_TOKEN minted, services started | A failed protection rule stops the job before any step runs |
| Each step | steps[*].if, with, env, then the step runs | Only here can you read steps.<id>.outputs and values written to $GITHUB_ENV earlier |
The Syntax reference has the full table of which contexts each key can see.
The Actions tab
The exam expects you to know where things are in the UI, not just in YAML. The Actions tab of a repository lists workflows on the left and their runs on the right, filterable by event, status, branch and actor. Opening a run shows its summary: the trigger, status, duration, a graph of jobs, annotations, job summaries and artifacts. Opening a job shows its steps and their logs.
| Status | What it means |
|---|---|
| Success | Every job finished without a failure (a job that was skipped does not fail the run) |
| Failure | A step exited non-zero and nothing caught it; downstream needs: jobs are skipped |
| Skipped | A job's if: was false, or a job it needs did not succeed |
| Cancelled | Cancelled by a person, the API, concurrency with cancel-in-progress, or a timeout |
| In progress | Running — or queued, waiting for a runner, a concurrency slot or an environment approval |
A job's log is grouped by step. Here is the start of a checkout step as the log viewer shows it:
Run actions/checkout@v7
Getting Git version info
Each step's output is folded into groups, secrets are replaced by ***, and annotations such as
::notice:: are lifted into the run summary. Skill 2
covers reading logs, re-running and downloading them in depth.
Runners in brief
| Runner | Who manages it | Notes |
|---|---|---|
| GitHub-hosted (standard) | GitHub | A fresh VM per job; Ubuntu, Windows and macOS; free for public repositories |
| Larger runners | GitHub | More CPU and memory, GPU, Arm, static IP ranges, Azure private networking, custom images; Team and Enterprise Cloud |
| Self-hosted | You | Your own machine or container running the runner application; persistent unless ephemeral |
| Actions Runner Controller | You, on Kubernetes | Autoscaling ephemeral self-hosted runners as runner scale sets |
The current GitHub-hosted labels, checked on 5 November 2026: ubuntu-latest is moving from Ubuntu
24.04 to Ubuntu 26.04 in a gradual rollout between 19 October and 19 November 2026;
windows-latest is Windows Server 2025; macos-latest is
macOS 26 on Arm64. Skill 4
covers runner management in full.
Renames and retired syntax
Older Learn units, blog posts, forum answers and practice questions are full of names and commands that have moved. The exam follows the current product, but you will read both.
| You may read | Current name or syntax | Notes |
|---|---|---|
| Starter workflows | Workflow templates | The study guide still says "starter workflows"; the docs and the UI say workflow templates |
echo "::set-output name=x::y" | echo "x=y" >> "$GITHUB_OUTPUT" | Retired; the runner now ignores the command |
::save-state | echo "x=y" >> "$GITHUB_STATE" | Retired alongside set-output |
::set-env, ::add-path | $GITHUB_ENV, $GITHUB_PATH | Disabled since 2020 for security |
actions/upload-artifact@v3 | actions/upload-artifact@v7, download-artifact@v8 | v3 stopped working in January 2025; v4+ artifacts are immutable and need matching majors |
runs.using: node16 / node20 | runs.using: node24 | Node 24 became the default on 16 June 2026; Node 20 was removed on 23 September 2026, so older declarations now run on Node 24 |
ubuntu-20.04, windows-2019, macos-13 | ubuntu-24.04 / 26.04, windows-2025, macos-15 / 26 | Retired images fail to find a runner |
| "Required workflows" (organisation setting) | Rulesets → Require workflows to pass before merging | Required workflows moved into repository rulesets |
| Immutable actions (OCI packages on ghcr.io) | Immutable releases and workflow lockfiles | Removed from GitHub's roadmap in December 2025; see Skill 3 |
pull_request_target running the base branch's workflow | Always the default branch's workflow | Changed on 8 December 2025, along with how environment branch rules match PR events |
Retired commands still look plausible
An answer option that uses ::set-output is wrong even though it looks like valid workflow-command
syntax. If a question shows it in a log or script, the right reading is "this output will be empty". The same goes
for actions/upload-artifact@v3, which fails today, and for node16 or node20 actions, which the runner now runs on Node 24 whatever they declare — untested, and liable to break.