Workflow Files, YAML Structure, Jobs, Steps, and Actions: Configuration, Design Patterns, and Trade-Offs
Once a workflow runs, the next question is whether its structure will remain understandable, secure, and maintainable. This lesson treats workflow shape as architecture. You will compare one job with multiple jobs, shell scripts with actions, broad defaults with narrow overrides, and direct configuration with abstraction—always asking what state is shared, what failure is isolated, what permission is required, and what evidence remains reviewable.
Learning objectives
- Choose between one job and multiple jobs based on isolation, parallelism, repeated setup, evidence, and failure boundaries rather than YAML aesthetics.
-
Choose
runversususesbased on ownership, portability, dependency trust, versioning, and reviewability. - Place permissions, environment, shell, and working-directory configuration at the narrowest maintainable scope.
- Explain the operational difference between an external action, a local action, a copied template, and a workflow that directly contains logic.
- Evaluate readability versus abstraction with a decision table and predict concrete runner, token, and evidence consequences.
1. The design question: what should be one schedulable unit?
A job is more than indentation. It is a runner allocation, environment boundary, permission scope, log group, retry unit, and potential parallelism unit. Splitting a workflow into jobs can improve isolation and make dependencies explicit, but every split also means another runner setup and an explicit strategy for moving data that cannot be recreated.
Conversely, putting everything into one giant job makes filesystem sharing easy but couples unrelated tasks to the same machine and failure path. The correct answer follows the delivery model, not a rule such as “one job per command” or “one job per workflow.”
2. One job versus multiple jobs
| Question | Favor one job when… | Favor multiple jobs when… |
|---|---|---|
| Filesystem | Later steps truly depend on ephemeral files from earlier steps. | Each job can recreate inputs or exchange them explicitly. |
| Isolation | The steps belong to one trust/failure unit. | Build, test, publish, or deploy need distinct privileges or blast radius. |
| Parallelism | Work is inherently sequential and short. | Independent work can run concurrently or on different runner types. |
| Setup cost | Repeated checkout/tool setup would dominate useful work. | Independent setup is acceptable for clearer boundaries. |
| Retry behavior | Repeating the whole sequence is safe. | Targeted rerun boundaries matter and side effects are isolated. |
| Permissions | All steps legitimately need the same narrow permissions. | Only one job needs mutation/deployment privilege. |
A useful heuristic is to split when trust, side effects, runner requirements, or failure ownership differ. Do not split only to make the YAML visually shorter.
3. run versus uses: ownership and
dependency trade-offs
run is appropriate when the logic is small,
repository-specific, and clearer as an explicit script.
uses is appropriate when a reviewed reusable action
owns a well-defined capability such as checkout or toolchain setup.
But reuse introduces an executable dependency, so it carries
version, provenance, runtime, and trust requirements.
For third-party code, full-SHA pinning is an immutable reference but not a substitute for source review. The repository owner, source, release process, requested credentials, network behavior, and update process still matter.
4. External action versus local action
An external action such as actions/checkout is fetched
as a dependency according to its reference. A local action
referenced as uses: ./.github/actions/example is part
of your repository tree. On a hosted runner, that local repository
content normally must be available in the workspace first, which
means checkout is commonly required before invoking the local
action.
Local actions can centralize repeated repository-specific logic, but they are still executable code and should have a clear interface. Chapter 17 will teach custom action implementation in depth; here the design lesson is simply that “local” changes the ownership/versioning boundary, not the need for review.
5. Workflow-level versus job-level versus step-level configuration
Put a setting at workflow scope only when it is genuinely a workflow-wide invariant. Put it at job scope when one job needs a different runner, permission set, environment, or default. Use step scope for exceptional shell/working-directory behavior.
permissions: {}
defaults:
run:
shell: bash
jobs:
verify:
permissions:
contents: read
runs-on: ubuntu-24.04
defaults:
run:
working-directory: ./tools
steps:
- name: One step needs repository root
working-directory: .
run: pwd
The principle is least scope: broad defaults reduce repetition only when they do not hide meaningful differences. A production workflow should make privilege escalation and target changes conspicuous in review.
6. Stability: job IDs and check names can become external interfaces
Branch protection, dashboards, API consumers, or human runbooks may come to depend on job/check names. Refactoring identifiers is therefore not always cosmetic. Stable IDs and clear display names reduce accidental breakage, especially as workflows become reusable or governed organization-wide.
Keep machine-facing IDs concise and semantic. Keep human-facing names specific enough that a failed check can be understood without opening the YAML. Avoid auto-generated-looking names that make incident evidence ambiguous.
7. Readability versus abstraction
| Pattern | Strength | Risk | Use when |
|---|---|---|---|
Inline run steps |
Maximum local visibility. | Duplication and shell portability drift. | Logic is small and repository-specific. |
Repository script called by run |
Testable outside Actions; clear ownership. | Still needs tool/runtime discipline. | Logic is substantial but belongs to the repository. |
| Local action | Reusable step interface in one repo. | Can hide behavior and adds action metadata/runtime concerns. | Repeated action-shaped capability deserves an interface. |
| External action pinned to SHA | Reusable maintained capability. | External executable dependency/supply-chain risk. | Benefit outweighs dependency risk and source/release are trusted. |
| Reusable workflow | Whole-job/platform contract. | Interface, permission, nesting, and secret propagation complexity. | Cross-repository pipeline policy belongs at workflow boundary; covered later. |
8. Worked scenario: build, test, and publish are not one trust boundary
Suppose a future pipeline has three concerns: compile source, run tests, and publish a release package. Compilation and tests need read-only repository access. Publication mutates an external registry and should run only for an authorized release event. A single job would make it easier to leak publication credentials into build steps and harder to rerun safely.
A stronger design is separate read-only build/test jobs plus a tightly gated publication job that depends on verified outputs. Chapter 02 does not implement package publication yet; the point is to recognize the future privilege boundary now. Job structure is how delivery policy becomes visible before credentials are added.
9. Decision checklist
- State: Does the next unit require the same ephemeral filesystem, or can inputs be recreated/transferred explicitly?
- Trust: Does one unit execute less-trusted code or need fewer privileges?
- Runner: Do units need different OS, architecture, hardware, or isolation?
- Failure: Should a reviewer be able to rerun one unit without repeating side effects?
- Evidence: Will logs/check names clearly identify the failed capability?
- Dependency: Is an action/reusable component worth its external code and update surface?
- Maintenance: Is configuration placed broadly because it is truly invariant, or only to save lines?
10. Production pattern and Chapter 02 boundary
The production pattern is not “more YAML.” It is a graph whose nodes align with trust, runner, side-effect, and recovery boundaries. Start explicit. Extract shared components only after their interface is understood. Grant permissions narrowly. Treat action references as dependencies. Keep stable names where external policy depends on them.
Knowledge check
What is the strongest reason to split publication from read-only build/test work?
It creates a separate trust and side-effect boundary so publication credentials/permissions are not exposed to every build step and retries can be reasoned about independently.
When is a repository script often clearer than a custom action?
When substantial logic belongs to one repository and should be testable locally, but does not need an action-specific reusable interface.
Why can changing a job name be operationally significant?
Required checks, automation, dashboards, or human runbooks may depend on stable check/job names; a rename can break those integrations.
Why is a workflow-wide working directory not always a good deduplication?
If only some jobs share it, the broad default hides meaningful differences and can redirect unrelated commands. Use the narrowest maintainable scope.
Does full-SHA pinning eliminate the need to review an external action?
No. It makes the selected version immutable, but the pinned code can still be malicious, over-privileged, or inappropriate. Review trust and behavior too.
Official references and version notes
- Understanding GitHub Actions — current definitions and execution relationships for workflows, jobs, steps, actions, and runners.
- Workflow syntax for GitHub Actions — authoritative workflow structure, jobs, steps, permissions, defaults, runners, and shell behavior.
-
Setting default shell and working directory
— current precedence and restrictions for workflow/job
defaults.run. - Using GitHub-hosted runners — job-to-runner isolation and filesystem sharing within a job.
- Secure use reference — current guidance to pin action dependencies to full-length commit SHAs and minimize privileges.
- Using pre-written building blocks — current action-reference options and immutable SHA guidance.
- Managing custom actions — current action release/reference practices; detailed custom-action construction is deferred to Chapter 17.
Version-sensitive behavior was rechecked against primary GitHub
documentation and GitHub-maintained action repositories on
2026-09-09. Executable examples use
ubuntu-24.04,
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
(upstream release v7.0.1), and where Python setup is needed
actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
(upstream release v7.0.0) with Python 3.13. At verification time
both actions declare a Node 24 runtime. Runner images, action
releases/runtimes, workflow keys, parser diagnostics, and
plan-dependent behavior can change; re-resolve current immutable
SHAs before copying these examples into long-lived production
workflows. The design exercise intentionally stops before
artifacts, reusable workflows, environments, OIDC, releases, or
external deployment; those later chapters add the data/credential
mechanisms needed to implement the architecture safely.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.