GitHub Actions Foundations: Workflows, Events, YAML, Permissions, and Execution Model: Concepts, Architecture, and Mental Model
A workflow file is small, but the thing it authorizes is not: GitHub may turn a repository event into code execution on a runner with a repository-scoped credential. Before memorizing YAML, you need to know exactly which revision, event payload, identity, permissions, and machine are involved.
Learning objectives
- Explain the hierarchy workflow file → event → workflow run → job → step → action/shell command → runner without confusing hosted Actions state with Git objects.
- Identify the run’s event, ref, source SHA, workflow-file ref/SHA, actor, runner, and logs, and explain why those values are event-dependent.
-
Describe
GITHUB_TOKENas a per-job GitHub App installation token and reason about explicit least-privilegepermissions. - Separate YAML parsing from GitHub expression/context evaluation and from shell interpolation on the runner.
- Model trusted versus untrusted code/data, GitHub-hosted versus self-hosted execution, and the boundary around secrets and repository credentials.
Availability: The mandatory path uses a public repository on GitHub.com with a standard GitHub-hosted runner. GitHub currently documents standard hosted-runner use as free for public repositories. Actions can still be disabled by repository, organization, or enterprise policy. GitHub Enterprise Server uses self-hosted runners; GitHub-hosted runners are not supported there.
1. The problem: repository events can become code execution
Chapters 1–12 treated GitHub mostly as hosted state: repositories, Issues, Projects, pull requests, rules, releases, and APIs. GitHub Actions changes the risk model because a repository event can cause code to execute on a machine. That execution may receive a repository-scoped credential, read repository contents, write logs, call APIs, download dependencies, or—if you grant it—mutate GitHub state.
A green check therefore means more than “some YAML parsed.” It means GitHub selected a workflow definition, created a run from an event, scheduled one or more jobs, acquired runners, issued credentials according to policy, executed steps, and recorded conclusions. A reliable operator can reconstruct each link in that chain.
2. Mental model: event → run → jobs → runners → evidence
flowchart TD
E["GitHub event"] --> W["Workflow definition"]
W --> R["Workflow run"]
R --> J1["Job: validate"]
R --> J2["Job: other"]
J1 --> H["GitHub-hosted runner"]
J2 --> S["Self-hosted runner optional"]
H --> L["Steps + logs + conclusion"]
S --> L
T["GITHUB_TOKEN per job"] --> J1
T2["GITHUB_TOKEN per job"] --> J2
The event determines whether a workflow is eligible. A workflow run contains jobs; each job executes on one selected runner and receives its own job-scoped token. Logs and conclusions are hosted evidence of what happened.
The nodes are different resource types. The workflow definition is a
YAML file committed in Git. A workflow run, job, step status,
annotation, and log are GitHub Actions objects. A runner is
execution infrastructure. GITHUB_TOKEN is a credential
minted for a job. The arrows are scheduling and authorization
relationships, not Git history edges.
| Term | What it is | Where its identity lives |
|---|---|---|
| Workflow | A configurable automated process defined by YAML |
.github/workflows/*.yml or
.yaml in Git
|
| Event | The GitHub activity that can trigger a run | Webhook-like event type + payload |
| Workflow run | One execution instance of a workflow | Actions UI / REST / gh run |
| Job | A schedulable unit of steps sharing one runner | Inside the workflow run |
| Step | A shell command or action invocation | Inside a job |
| Action | Reusable executable automation component | Referenced by uses: |
| Runner | Machine that executes a job | GitHub-hosted or self-hosted infrastructure |
3. Run identity: event, ref, source SHA, and workflow-file SHA
Do not reduce a run to “the commit that ran.” GitHub exposes several
related identities because event semantics differ. For example,
github.sha is event-dependent; on pull-request
workflows it is not safe to assume it always means the contributor
branch tip. The github.workflow_ref and
github.workflow_sha identify the workflow definition
itself.
| Evidence field | Question it answers | Important caution |
|---|---|---|
github.event_name |
Which trigger family created this run? |
A push and a pull_request describe
different refs/SHAs.
|
github.event |
What event payload did GitHub provide? | Treat payload strings as potentially untrusted input. |
github.ref |
Which event-defined ref is associated with the run? | Meaning depends on event; do not assume “branch name.” |
github.sha |
Which event-defined commit SHA is associated with the run? | For PR events, inspect PR head/base fields when you need those exact identities. |
github.workflow_ref |
Which workflow path + ref supplied the definition? | Useful when the same path differs across branches. |
github.workflow_sha |
Which commit contains the workflow file version? | This is distinct evidence from the event SHA. |
| Default branch | Where GitHub discovers some manually/repository-triggered workflows |
workflow_dispatch must exist on the default
branch to be manually triggered.
|
A production incident report should record at least the run ID,
event, headSha reported by GitHub, workflow path,
attempt number, and exact repository. For a critical build, also
record the commit that supplied the workflow definition and the
immutable versions of external actions.
4. Three languages in one file: YAML, Actions expressions, and the runner shell
Workflow YAML is parsed as configuration. GitHub expressions such as
${{ github.ref }} are evaluated by the Actions system
in supported fields. Shell syntax inside run: is
processed on the runner. Mixing these layers is a common source of
both bugs and security problems.
name: Layered example
on: workflow_dispatch
permissions:
contents: read
jobs:
explain:
if: ${{ github.repository_owner != '' }}
runs-on: ubuntu-latest
steps:
- name: Print a value safely
env:
EVENT_NAME: ${{ github.event_name }}
run: |
printf 'event=%s\n' "$EVENT_NAME"
Here YAML establishes structure. The if expression is
evaluated as an Actions condition. The event value is copied into an
environment variable, and Bash expands $EVENT_NAME at
runtime. This environment-variable pattern is also safer than
embedding attacker-controlled context strings directly into a shell
program.
5. Execution identity: GITHUB_TOKEN is job-scoped authorization
When Actions starts a job, GitHub creates a unique
GITHUB_TOKEN. GitHub documents it as a GitHub App
installation access token whose permissions are limited to the
workflow repository. It expires when the job ends (subject to the
documented maximum lifetime). The token is available as
secrets.GITHUB_TOKEN and through
github.token.
The security rule for this course is simple: declare
permissions explicitly. Do not depend on repository/org
defaults when a workflow’s intended access is known. A
validation-only workflow normally begins with:
permissions:
contents: read
When you specify permissions, omitted permission categories are treated as no access. That property makes the workflow file itself an authorization contract. Repository or organization policy can further reduce what is actually granted; YAML cannot override a stronger platform restriction.
Authentication is not authorization: The runner
may possess a valid GITHUB_TOKEN and still receive
403 Forbidden because the requested operation is
outside the token’s effective permissions. A 403 is often proof
that least privilege is working.
6. Trust boundaries: code, payload, secrets, dependencies, and runners
Anything an external contributor can influence should be treated as
untrusted. That includes pull-request titles/bodies, branch names,
commit messages, repository content checked out from a fork, and
artifacts produced by an untrusted run. GitHub explicitly warns that
strings from the github context can become
script-injection vectors if interpolated directly into shell code.
| Boundary | Safer default | Why |
|---|---|---|
| Event text → shell | Pass through an environment variable and quote it | Prevents the event string from becoming shell syntax during expression substitution. |
| Secrets → logs | Print only selected non-sensitive fields | Masking is defense-in-depth, not a license to dump contexts. |
| External action → job | Review source and pin to a full commit SHA for immutable use | An action executes with the job’s access to files, token, and secrets. |
| Public PR → self-hosted machine | Do not run public untrusted code on persistent self-hosted infrastructure | A compromised runner can expose local/network resources and persist across jobs. |
| Workflow permission → API | Grant only the categories the job needs | A stolen token can do only what the effective permissions allow. |
7. GitHub-hosted versus self-hosted runners
A GitHub-hosted job normally receives a fresh VM for that job. A self-hosted runner is infrastructure you deploy and maintain; it can retain state and network reachability between jobs. That is not merely a cost or performance choice—it is a trust decision.
| Dimension | GitHub-hosted standard runner | Self-hosted runner |
|---|---|---|
| Machine lifecycle | Fresh VM for a job in normal hosted VM classes | Your lifecycle; not inherently clean between jobs |
| Maintenance | GitHub maintains runner image/infrastructure | You patch OS, tools, network, hardening |
| Public-repository mandatory lab | Yes | No — this course keeps it optional |
| Network reach | Internet plus documented hosted connectivity | Whatever networks the machine can reach |
| GHES | Not supported | Required execution model for GHES Actions |
Later chapters cover runners deeply. For now remember the rule: execution environment is part of the security boundary. “The workflow file is safe” does not mean “the machine is safe.”
8. Read-only inspection before you create any workflow
Chapters 1–12 established an inspect-before-mutate habit. Keep it. On a repository that may or may not have Actions enabled, start with read-only CLI/API state.
REPO="OWNER/REPO"
gh workflow list -R "$REPO" --all \
--json id,name,path,state
gh run list -R "$REPO" --limit 10 \
--json databaseId,workflowName,event,status,conclusion,headBranch,headSha,url
gh api --paginate \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
"repos/$REPO/actions/runs?per_page=10" \
--jq '.workflow_runs[] | {id,event,status,conclusion,head_sha,html_url}'
An empty list can mean “no workflow/run exists”; an authorization failure is different; an absent Actions tab can reflect policy. Keep those states distinct rather than treating every empty screen as a workflow bug.
9. DevOps operating model
In a production GitHub operating model, a workflow run is evidence only when you can bind it to the repository, event, exact source identity, workflow definition, runner class, permissions, dependency versions, and logs. Those dimensions are the bridge from Chapters 7–12 (change control) into Chapters 13–20 (execution and delivery control).
10. Lesson summary
GitHub Actions is an event-driven execution system layered on repository state. Workflow YAML is versioned in Git, but runs/jobs/logs are hosted Actions objects. Event/ref/SHA semantics are event-specific; workflow-file identity is separately observable. Each job receives a repository-scoped GitHub App installation token whose access should be declared explicitly. Runner trust and untrusted input matter as much as YAML correctness.
Knowledge check
A workflow run is green. Does that prove the source commit you intended was tested?
No. First verify the run event and exact SHA/ref, and—when relevant—the PR head/base identity and workflow-file SHA. Green proves the executed run succeeded, not that you selected the right source.
Why is GITHUB_TOKEN not equivalent to your
personal gh login?
It is a job-scoped GitHub App installation access token minted for the workflow repository with effective permissions derived from workflow and repository/org policy. It is not your personal session.
Where must a workflow YAML file live for GitHub Actions to discover it?
Under .github/workflows/ with a
.yml or .yaml extension.
Why should a public pull request not execute on a persistent self-hosted runner by default?
Untrusted code can compromise the machine, steal job credentials/secrets, access local networks, and persist into later work. GitHub explicitly warns against self-hosted runners for public repositories.
What is wrong with putting
${{ github.event.pull_request.title }} directly
inside a shell command?
The title is attacker-controlled input. Expression substitution can turn crafted text into shell syntax. Pass it as an environment variable and quote it, or handle it in a non-shell action designed for structured input.
Further reading — current official GitHub sources
- GitHub Docs — Understanding GitHub Actions
- GitHub Docs — Workflow syntax
- GitHub Docs — Events that trigger workflows
- GitHub Docs — Contexts reference
- GitHub Docs — GITHUB_TOKEN
- GitHub Docs — Secure use reference
- GitHub CLI — gh run list
- GitHub CLI — gh run view
- GitHub REST API — workflow runs (2026-03-10)
- GitHub Docs — Script injections
- GitHub Docs — GitHub-hosted runners
- GitHub Docs — Self-hosted runners
- GitHub Docs — Actions billing
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.