8.2 Jobs, Steps, Runners, Secrets, and Reusable Actions
Key Takeaways
- Jobs in a workflow run in parallel by default; add a `needs` key when a later job must wait for another job to succeed.
- Each job runs on a `runs-on` runner; steps either `run` a script or `uses` a reusable action, with optional `with` inputs and `env` variables.
- GitHub-hosted runners are a fresh Ubuntu, Windows, or macOS virtual machine for every job; self-hosted runners persist on hardware you maintain and do not need a clean instance per job.
- Store secrets at repository, organization, or environment scope and never echo them into logs; GitHub injects `GITHUB_TOKEN` automatically for each job.
- Included minutes for standard hosted runners on private repositories are 2,000 (Free), 3,000 (Pro and Team), and 50,000 (Enterprise Cloud) per month; public repositories using standard GitHub-hosted runners do not consume that quota.
Jobs, steps, and the YAML keys GH-900 expects
Section 8.1 put an event in front of a workflow file. This section opens the file. GitHub's component model is small enough to memorize: a workflow contains jobs; a job contains steps; a step either runs a script or runs an action; the job executes on a runner.
A job is a set of steps that execute on the same runner. Because they share that machine, step two can see files step one created: checkout, then install, then test. Steps run in order inside a job. Jobs run in parallel by default. If you declare test and lint with no relationship, GitHub starts both as soon as runners are free. That surprises people who read YAML top-to-bottom and assume sequence. To force sequence, set needs to the job id that must finish first:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
package:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm pack
lint and test still overlap. package waits until both succeed. If lint fails, package is skipped. Jobs do not share a filesystem: each job is a different runner (or at least a different job slot), so you pass files with artifacts if a later job needs a build output. GH-900 may not require artifact syntax, but it will punish the myth that job two can cd into job one's checkout.
A step is either:
run:— a shell script on the runner (bashon Linux and macOS, PowerShell on Windows unless you choose otherwise).uses:— a reusable action, which is packaged code that performs a common task (checkout the repo, set up Node.js, upload a release).
uses often takes with: inputs. env: sets environment variables for a step or a whole job. Map those words to YAML keys you will see on the exam:
| YAML key | Where it sits | Meaning |
|---|---|---|
name | Workflow (or job/step) | Display title in the Actions tab |
on | Workflow | Events that start a run |
jobs | Workflow | Map of job ids to job definitions |
runs-on | Job | Which runner label to request |
needs | Job | Other job ids that must complete first |
steps | Job | Ordered list of run or uses |
uses | Step | Reusable action (owner/repo@ref or a local path) |
with | Step | Inputs passed to that action |
env | Workflow, job, or step | Environment variables |
run | Step | Inline script |
The following teaching workflow uses those keys together—including a secret and a sequenced deploy—without turning into a Marketplace dump:
name: Build and deploy
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
- name: Install and test
run: |
npm ci
npm test
env:
NODE_ENV: test
deploy:
needs: test
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy
run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Read it the way the exam will: on is push-to-main or a manual run; test and deploy are jobs; deploy needs test; both runs-on Ubuntu; steps mix uses (checkout, setup-node) with run; with configures Node 20; env passes NODE_ENV and a mapped secret. ${{ }} is GitHub's expression syntax; secrets.DEPLOY_TOKEN is not written in the YAML as the real token.
Runners: GitHub-hosted versus self-hosted
A runner is the server that executes a job. Each runner runs one job at a time. You pick it with runs-on.
GitHub-hosted runners are virtual machines GitHub provides. Standard images cover Ubuntu Linux, Windows, and macOS (ubuntu-latest, windows-latest, macos-latest). GitHub maintains the image (tools update weekly) and, for typical hosted VMs, each job gets a fresh, newly provisioned virtual machine. When the job ends, that VM is gone. You cannot SSH in later and find yesterday's node_modules. That isolation is the security and reproducibility story: job B cannot read job A's leftover credentials off disk.
Self-hosted runners are machines you deploy—physical, virtual, container, on-premises, or in your cloud. You install GitHub's runner application and register the machine at repository, organization, or enterprise scope. GitHub's docs are explicit: self-hosted runners do not need a clean instance for every job. They persist. That is the feature (cached tools, GPU hardware, access to a private network) and the risk (leftover files, long-lived secrets on disk). You patch the OS. GitHub auto-updates the runner application unless you disable that. Self-hosted use of GitHub Actions is not billed as hosted minutes; you pay for the hardware. Standard hosted minutes still apply when the same workflow uses ubuntu-latest instead.
| GitHub-hosted | Self-hosted | |
|---|---|---|
| Who owns the machine | GitHub | You |
| Operating systems | Ubuntu, Windows, macOS images | Whatever you install |
| Freshness | New VM (or equivalent) per job | Persistent; jobs can see prior state |
| Maintenance | GitHub images and capacity | You patch OS, tools, and capacity |
Typical runs-on | ubuntu-latest | A label you assigned, for example [self-hosted, linux, x64] |
| Minutes | Consume plan quota on private repos for standard hosted runners | You pay infrastructure; not the hosted-minute SKU |
Larger hosted runners (more CPU, GPU, custom images) exist on Team and Enterprise Cloud. They are always billed, including on public repositories. If a question says "public repo, standard ubuntu-latest," minutes are free. If it says "public repo, 64-core larger runner," it is not free. Do not flatten those two.
Secrets, GITHUB_TOKEN, and reusable actions
Secrets are encrypted variables for sensitive values: deploy tokens, package registries, cloud keys. You create them at three scopes:
- Repository secrets — one repo.
- Organization secrets — many repos, optionally limited to selected repositories.
- Environment secrets — tied to a GitHub environment (
production,staging). You can require reviewers before a job that references that environment starts, so a production secret never reaches an unapproved run.
If the same name exists at more than one level, environment wins, then repository, then organization. Reference a secret as ${{ secrets.NAME }} and pass it through env or with. Never echo secrets. Do not echo ${{ secrets.DEPLOY_TOKEN }}, do not print them in curl -v, and do not write them to a public summary. GitHub redacts secrets that it knows about from logs, but redaction is not a license to print them, and obfuscation tricks can leak values GitHub does not recognize.
GITHUB_TOKEN is different from secrets you create. At the start of each job GitHub automatically creates a unique GITHUB_TOKEN (a GitHub App installation access token for that repository) and expires it when the job ends. You do not paste it in Settings. Use ${{ secrets.GITHUB_TOKEN }} (or github.token) to checkout extra refs, comment on a pull request, or open an issue from the workflow. Permissions are limited by repository/org defaults; least privilege is the secure default. Fork pull requests do not receive repository, organization, or environment secrets. They may use GITHUB_TOKEN with read-only permission. That is why "a first-time contributor's PR deploys with our cloud key" is a wrong mental model—GitHub withholds those secrets on purpose.
Reusable actions keep workflow YAML short. GitHub Marketplace lists actions you can uses: by owner/repo@ref, commonly actions/checkout@v4 or actions/setup-node@v4. You can also write a custom action in your repository (uses: ./path/to/action) or in another repository you control. Marketplace is convenience and community review; custom is private logic and org standards. Neither replaces the workflow file. A reusable workflow (workflow_call) is a further pattern: one YAML file that other workflows call. GH-900 needs the idea that you can reuse; it does not need you to author action.yml from scratch.
Included minutes
GitHub Actions billing states that standard GitHub-hosted runners are free for public repositories (and for GitHub Pages and Dependabot). Private repositories consume a monthly minute quota that resets each billing cycle, charged to the repository owner:
| Plan | Included minutes / month |
|---|---|
| GitHub Free (personal and Free for organizations) | 2,000 |
| GitHub Pro | 3,000 |
| GitHub Team | 3,000 |
| GitHub Enterprise Cloud | 50,000 |
Those numbers are for standard hosted runners. Windows and macOS hosted jobs cost more per minute than Linux if you exceed the quota; do not memorize every SKU for GH-900, but do not claim they are identical either. Artifact storage is a separate allowance (and is shared with GitHub Packages). If the account has no payment method, usage stops when the quota is gone.
Exam scenarios and traps
- Two jobs with no
needs→ parallel, two runners, no shared disk. - "Deploy only after tests pass" →
needs: teston the deploy job, not a comment in the YAML. - Fresh VM per GitHub-hosted job vs persistent self-hosted machine is the runner discriminator. "We need last week's cached SDK on the same box" points at self-hosted. "We need a clean Ubuntu every run" points at hosted.
- Secrets belong in Settings (repo, org, or environment), not committed as
env: TOKEN: supersecretin YAML. - Never echo.
GITHUB_TOKENis automatic; you still should not print it. - Fork PRs do not get your cloud secrets.
- Free = 2,000; Pro and Team = 3,000; Enterprise Cloud = 50,000. Public + standard hosted = free. Public + larger runner = paid.
uses+withis an action.run+envis a script. Both are steps.
If a GH-900 item mentions YAML, map the key (on, jobs, runs-on, steps, uses, with, env, needs) before you invent a product. If it mentions money, start from public-versus-private and standard-versus-larger, then apply 2,000 / 3,000 / 50,000.
By default, two jobs declared in the same GitHub Actions workflow do which of the following?
A private repository on GitHub Team uses standard GitHub-hosted runners. Which statement about included Actions minutes is correct?
Where can you store a deploy token as a GitHub Actions secret, and what must you avoid doing with it in a workflow?