GitHub Actions Foundations: Workflows, Events, YAML, Permissions, and Execution Model: Configuration, Design Choices, and Tradeoffs
Once a workflow runs, the harder question is not “what YAML key exists?” but “which automation design gives the smallest reliable trust boundary?” This lesson turns trigger, permission, runner, and dependency choices into explicit engineering decisions.
Learning objectives
-
Choose
workflow_dispatch,push, orpull_requestaccording to trust and operational intent rather than convenience. - Decide whether permissions belong at workflow or job scope and explain why narrower job scope can reduce blast radius.
- Compare GitHub-hosted and self-hosted runners as security/reliability boundaries, not merely machine choices.
- Choose between shell steps, GitHub-maintained actions, third-party actions, and in-house actions with explicit supply-chain governance.
- Apply a decision table that weighs maintainability, security, reliability, compatibility, governance, and cost without requiring paid features.
Availability: The mandatory design exercise is plan-neutral and the hands-on examples stay on GitHub Free/public repositories. Standard GitHub-hosted runners are free for public repositories. Larger runners and some runner-networking/governance capabilities are organization/plan-dependent. GitHub Enterprise Server requires self-hosted execution and Actions must be enabled by a site administrator.
1. Manual dispatch versus push/pull_request: choose the evidence boundary first
workflow_dispatch is excellent for a first automation
because a human chooses when to create a run and the workflow can be
inspected before execution. push is appropriate when
every matching change must be validated.
pull_request is appropriate when proposed changes need
evidence before merge—but PR code and event data may be untrusted.
| Trigger | Best first use | Reliability/security tradeoff |
|---|---|---|
workflow_dispatch |
Operator-run smoke test, controlled rehearsal | Predictable timing; requires workflow on default branch; can still select a ref for execution. |
push |
Validate committed branch changes | Simple source identity; broad filters can cause noisy/expensive runs. |
pull_request |
Validate proposed integration | Good pre-merge evidence; fork code/event payload must be treated as untrusted. |
pull_request_target |
Privileged base-repository automation only when specifically needed | Security-sensitive; do not combine privileged context with checkout/execution of untrusted PR code. |
The chapter intentionally does not teach every event/filter syntax; Chapter 14 does that. Here, the decision is about who can cause execution and what code/data the run may consume.
2. Workflow-level versus job-level permissions
Top-level permissions establishes a default for all
jobs. Job-level permissions can narrow or specialize a
particular job. If only one job needs a write capability, do not
grant that capability to unrelated validation jobs.
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: ./scripts/test.sh
report:
permissions:
contents: read
issues: write
runs-on: ubuntu-latest
steps:
- run: ./scripts/report-result.sh
This design is still not automatically safe—the reporting script needs review—but a compromised test step does not inherit the reporting job’s issue-write access. Separate jobs can also use different environments/runners/secrets, creating clearer security domains.
3. GitHub-hosted versus self-hosted: who owns the machine risk?
For public repositories and beginner CI, GitHub-hosted runners are the safer default because normal hosted VM jobs start on fresh instances managed by GitHub. Self-hosted runners are valuable for custom hardware, private networks, specialized tooling, or GHES—but the operator owns patching, persistence, network reach, and compromise recovery.
| Question | Prefer GitHub-hosted | Consider self-hosted |
|---|---|---|
| Untrusted public PR code? | Yes | No — GitHub warns strongly against public repos on persistent self-hosted runners. |
| Private internal service required? | Maybe with supported private networking | Yes, if isolated and governed appropriately. |
| Custom hardware/software image? | Standard/larger runners if sufficient | Yes, when maintenance cost is justified. |
| GHES instance? | Not supported | Required execution model. |
| Need clean job environment? | Strong default for VM classes | You must design ephemeral cleanup/isolation. |
Do not treat “self-hosted is free” as “self-hosted has no cost.” Hardware, patching, monitoring, incident response, capacity, and network risk remain your responsibility.
4. Shell step versus action versus in-house component
A shell step is transparent when the operation is short and portable enough. A maintained action can encapsulate complex GitHub-specific behavior. A third-party action is also a software dependency with code execution inside your job. An in-house action centralizes behavior but creates a product you must maintain.
| Choice | Strength | Risk / governance need |
|---|---|---|
| Shell step | Visible implementation; no extra action dependency | Shell portability, quoting, error handling, tool availability. |
| GitHub-maintained action | Common integration with documented behavior | Still versioned executable code; repository policy may require full SHA pinning. |
| Third-party action | Fast reuse of specialized automation | Publisher/source compromise; pin full commit SHA and audit. |
| In-house action | Central policy/reuse under your ownership | Maintenance, release/versioning, testing, dependency supply chain. |
GitHub’s current security guidance says a full-length commit SHA is the only immutable way to reference an action. A mutable major-version tag is convenient, but you are trusting the action maintainer and tag integrity. Production policy can require full-SHA pinning.
5. Worked example: checkout convenience versus immutable dependency identity
GitHub’s current actions/checkout release line is v7.
For production immutability, this chapter’s checkpoint uses the
verified v7.0.1 commit rather than the mutable @v7 tag:
- name: Check out the exact source revision
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
The SHA is auditable and immutable. The tradeoff is update responsibility: you will not receive fixes automatically merely because the v7 tag moves. Dependabot can help propose action updates; reviewers must still validate the new immutable reference.
6. Do not blend Git, GitHub Actions, and external systems
Core Git decides what commits/refs exist. GitHub stores repository
and workflow configuration, creates events/runs, and authorizes
GITHUB_TOKEN. The runner executes commands. An external
registry/cloud/identity provider enforces its own authentication and
policy. A green GitHub job cannot prove that an external deployment
succeeded unless the job actually observed the external system’s
authoritative state.
7. Decision table: Atlas Relay grows from one workflow to a team service
| Scenario | Trigger / runner / building block | Decision rationale |
|---|---|---|
| Manual validation while learning |
workflow_dispatch / GitHub-hosted / shell
|
Low surprise, free public path, easy evidence inspection. |
Every change to src/ must pass tests |
filtered push + pull_request /
GitHub-hosted
|
Reliable pre-merge and branch evidence; untrusted PR boundary explicit. |
| Publish a comment only after tests | Separate reporting job with narrowly scoped write permission | Keeps write token away from test process. |
| Needs private on-prem dependency | Optional isolated self-hosted runner or supported private networking | Higher operational/security cost; justify against requirement. |
| Use a popular formatting action | Pin reviewed full commit SHA | Supply-chain immutability; schedule update review. |
Maintainability: keep the first workflow small. Security: minimize trigger reach, permission reach, and dependency trust. Reliability: bind evidence to exact SHAs and deterministic commands. Compatibility: account for shell/runner/GHES differences. Cost: public standard hosted use is free today, but avoid accidental trigger storms anyway.
8. A small production policy is better than a large YAML style guide
A useful starting policy can fit on one page: workflows must declare permissions; public-fork code runs only on GitHub-hosted isolated runners; third-party actions use reviewed full SHAs; event-provided strings never flow directly into shell syntax; workflow-file changes have CODEOWNERS review; logs avoid full contexts/secrets; and every release/build records run + source identity.
9. Lesson summary
Trigger choice defines who can cause execution; permission scope defines what a compromised job can do; runner choice defines machine/network blast radius; and action choice defines software-supply-chain trust. Treat those as architecture decisions before adding more YAML features.
Knowledge check
Only one job needs issues: write. Where should
that permission usually live?
At that job if possible, while the workflow default stays read-only. This reduces the token capability available to unrelated jobs.
Why is actions/example@v1 not immutable?
A tag can be moved. GitHub security guidance identifies a full-length commit SHA as the immutable action reference.
A self-hosted runner is idle and costs no Actions minutes. Is it automatically the cheaper choice?
No. You own machine/cloud cost, patching, monitoring, capacity, isolation, networking, and incident response.
What is the key security difference between
pull_request and a privileged
pull_request_target design?
The privileged pattern can run in the base-repository context with stronger access. It must not check out/execute untrusted PR code unless a carefully designed security model explicitly makes that safe.
Does a successful GitHub Actions deployment job prove the cloud service is healthy?
Only if the job actually verifies the cloud service’s authoritative post-deployment state. Actions job state and external-service state are separate systems.
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 — Choosing the runner for a job
- GitHub Docs — Secure use: pin actions to full SHA
- actions/checkout — official repository
- GitHub Docs — Actions settings/policies
- GHES 3.21 — Getting started with Actions
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.