Jobs, Steps, Matrices, Containers, Service Containers, and Dependency Graphs: Guided Hands-On Workflow and Core Operations
You will now make the topology visible in a disposable repository. One workflow will create a build dependency, expand a deliberately small matrix, run one job inside a container, attach a Redis service to another job, and then expose the graph through GitHub’s UI, CLI, and REST job metadata.
Learning objectives
- Create a build → matrix-test dependency graph with an explicit source-SHA contract.
-
Use
include/exclude,fail-fast, andmax-parallelin a deliberately bounded matrix. - Run one probe inside a job container and explain how that differs from the underlying Ubuntu runner.
-
Attach a Redis service container with health readiness and prove
host-runner networking through
127.0.0.1. -
Inspect job generation, duration, result, runner identity, and
logs through
ghand the versioned Actions REST API.
Lab assumptions: GitHub.com, GitHub Free,
repository owner/write access, GitHub CLI authenticated to
GitHub.com, Git installed locally, and a disposable public personal
repository. The lab uses standard ubuntu-latest runners
plus public Docker Hub images ubuntu:24.04 and
redis:7-alpine. Those image tags are intentionally
convenient for a disposable lab; production systems should review
provenance and pin container image digests when
repeatability/security requires it.
1. Preflight: create a fresh repository and prove the initial state
The repository is Atlas Topology Lab. It contains no valuable data and no secrets. Before creating automation, inspect authentication, repository identity, Actions settings, and current workflow list.
gh --version
gh auth status --active --hostname github.com
OWNER="$(gh api -H "X-GitHub-Api-Version: 2026-03-10" user --jq .login)"
REPO="$OWNER/atlas-c15-topology-lab"
gh repo view "$REPO" --json nameWithOwner,url,isArchived,visibility >/dev/null 2>&1 && {
echo "Repository already exists; choose a fresh lab name." >&2
exit 1
} || true
gh repo create "$REPO" --public --clone --add-readme
cd atlas-c15-topology-lab
DEFAULT_BRANCH="$(gh repo view "$REPO" --json defaultBranchRef --jq '.defaultBranchRef.name')"
gh workflow list -R "$REPO" --all --json id,name,path,state
printf 'default_branch=%s\n' "$DEFAULT_BRANCH"
Expected state: a public repository with one initial commit and no Actions workflows. If organization policy disables Actions or restricts Docker images, stop and use a personal disposable repository rather than weakening organization policy.
2. Create the execution graph
This workflow uses only read permission. The build job publishes the exact event SHA as a job output. Matrix jobs independently check out that SHA, which is the reproducible source boundary. A container probe demonstrates a job container; a Redis probe demonstrates service lifecycle and host networking.
name: Chapter 15 topology lab
on:
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
outputs:
source_sha: ${{ steps.source.outputs.sha }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- id: source
run: |
printf 'sha=%s\n' "$GITHUB_SHA" >> "$GITHUB_OUTPUT"
printf 'runner-local evidence\n' > runner-only.txt
test -f runner-only.txt
test:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 2
matrix:
mode: [fast, thorough]
feature: [off, on]
exclude:
- mode: thorough
feature: off
include:
- mode: compatibility
feature: on
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ needs.build.outputs.source_sha }}
- name: Prove fresh runner and exact source
env:
EXPECTED_SHA: ${{ needs.build.outputs.source_sha }}
MODE: ${{ matrix.mode }}
FEATURE: ${{ matrix.feature }}
run: |
test ! -e runner-only.txt
test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
printf 'matrix mode=%s feature=%s sha=%s\n' "$MODE" "$FEATURE" "$EXPECTED_SHA"
container_probe:
needs: build
runs-on: ubuntu-latest
container:
image: ubuntu:24.04
steps:
- name: Prove job-container user space
run: |
. /etc/os-release
printf 'container_os=%s\n' "$PRETTY_NAME"
printf 'workspace=%s\n' "$GITHUB_WORKSPACE"
service_probe:
needs: build
runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10
steps:
- name: Verify Redis protocol path
run: |
python - <<'PY2'
import socket
with socket.create_connection(('127.0.0.1', 6379), timeout=5) as s:
s.sendall(b'*1\r\n$4\r\nPING\r\n')
reply = s.recv(64)
print('redis_reply=', reply.decode().strip())
assert reply.startswith(b'+PONG')
PY2
summarize:
if: ${{ always() }}
needs: [build, test, container_probe, service_probe]
runs-on: ubuntu-latest
steps:
- env:
BUILD: ${{ needs.build.result }}
TEST: ${{ needs.test.result }}
CONTAINER: ${{ needs.container_probe.result }}
SERVICE: ${{ needs.service_probe.result }}
run: printf 'build=%s test=%s container=%s service=%s\n' "$BUILD" "$TEST" "$CONTAINER" "$SERVICE"
The matrix expands to four jobs: three surviving Cartesian
combinations plus one include row. With
max-parallel: 2, GitHub may schedule at most two of
those matrix jobs simultaneously even though other non-matrix jobs
can run when their own dependencies are ready.
3. Commit the workflow and bind the run to one SHA
mkdir -p .github/workflows
# Save the YAML above as .github/workflows/ch15-topology.yml
git add .github/workflows/ch15-topology.yml
git commit -m "ci: add Chapter 15 topology lab"
git push origin "$DEFAULT_BRANCH"
WORKFLOW_SHA="$(git rev-parse HEAD)"
printf 'workflow_sha=%s\n' "$WORKFLOW_SHA"
gh workflow view ch15-topology.yml -R "$REPO" --yaml
Prediction: pushing the file changes Git repository state but does not itself create a run because this workflow is manual-only. The next dispatch will select the workflow definition on the default branch and create hosted workflow/job records.
4. Dispatch once and capture the run identity
gh workflow run ch15-topology.yml -R "$REPO" --ref "$DEFAULT_BRANCH"
RUN_ID="$(gh run list -R "$REPO" --workflow ch15-topology.yml --event workflow_dispatch --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run watch "$RUN_ID" -R "$REPO" --exit-status
gh run view "$RUN_ID" -R "$REPO" --json event,headBranch,headSha,status,conclusion,jobs,url --jq '{event,headBranch,headSha,status,conclusion,jobs:[.jobs[]|{name,conclusion,startedAt,completedAt,databaseId}]}'
The run headSha should equal WORKFLOW_SHA.
Job names reveal the matrix expansion. Start/end timestamps make
parallelism visible; ordering is not guaranteed, so do not infer
semantic priority from which cell starts first.
5. Inspect job records and runner identity through REST
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/actions/runs/$RUN_ID/jobs?filter=latest&per_page=100" --jq '.jobs[] | {id,name,status,conclusion,runner_name,runner_group_name,started_at,completed_at}'
gh run view "$RUN_ID" -R "$REPO" --log
Do not expect a single runner name for the entire workflow. Each job is independently scheduled. The four matrix children are separate job records even though they came from one YAML job definition.
6. Prove the cross-job filesystem boundary
The build job created runner-only.txt. Every matrix
child asserts that the file does not exist before running
its test. That negative assertion is evidence of runner isolation.
The source commit itself is reproduced by checking out
needs.build.outputs.source_sha.
This is an important distinction: source identity is shared by an explicit value and a fresh checkout; build workspace state is not shared. If the test genuinely needed a generated binary instead, the correct mechanism would be a workflow artifact or package—not a hidden assumption about runner reuse.
7. Explain the matrix evidence before optimizing it
| Generated cell | Why it exists | Expected result |
|---|---|---|
| fast / off | Base product | success |
| fast / on | Base product | success |
| thorough / on | Base product; thorough/off excluded | success |
| compatibility / on | Added by include | success |
fail-fast: false means a failure would not immediately
cancel siblings. max-parallel: 2 bounds matrix
concurrency. These two settings solve different problems: evidence
completeness versus resource pressure.
8. Interpret the job-container result
The container_probe job is still scheduled by an Ubuntu
runner, but its ordinary steps run in the
ubuntu:24.04 job container. That is why the step
inspects the container’s /etc/os-release. The
underlying runner remains responsible for orchestration.
For production, treat the container image as a supply-chain dependency. A mutable tag can move. Use an approved registry and immutable digest/policy when reproducibility warrants it; do not put registry credentials in logs.
9. Interpret the service-container result
The Redis service exists only for service_probe. Its
health check is evaluated by Docker while the application-side
Python code proves the TCP/protocol path from the host runner
through the mapped port. Other jobs cannot assume that service or
its container filesystem exists.
10. Mental-model challenge: choose the surface, not a copied command
| Requirement | Choose | Reason |
|---|---|---|
| Pass one computed SHA to a dependent job | Job output + needs |
Small metadata contract. |
| Pass a 40 MB generated test fixture to another job | Artifact | File transfer across fresh runners. |
| Test three runtime modes with identical steps | Matrix | Same execution shape, bounded variable combinations. |
| Test one database-backed integration lane | Service container | Job-scoped disposable dependency. |
| Test against a production SaaS account | Not the disposable service pattern | External credentials, cost, rate limits, and production data require separate governance. |
Write down the choice before adding YAML. If you cannot explain the state boundary and ownership, the topology is not ready for automation.
11. Pause state safely for the next lessons
gh workflow list -R "$REPO" --all --json id,name,path,state
gh run view "$RUN_ID" -R "$REPO" --json headSha,conclusion,url
# Keep the repository active if proceeding directly to Lesson 4/5 exercises.
# Otherwise disable the workflow; no repository deletion is required.
gh workflow disable ch15-topology.yml -R "$REPO"
gh workflow list -R "$REPO" --all --json id,name,path,state
Disabling the workflow changes hosted workflow state, not Git history. The YAML remains in the repository and can be reviewed. A later fresh lab is preferable to reusing confusing historical state.
12. Lesson summary
You created a source-SHA dependency edge, four bounded matrix jobs, a containerized job, a healthy Redis service job, and an evidence aggregator. You proved that local files do not cross job runners, inspected actual job records instead of guessing from YAML, and separated matrix concurrency, failure policy, container user-space, and service networking into distinct controls.
Knowledge check
Why does each matrix child check out
needs.build.outputs.source_sha?
To bind every child to the same explicit source identity without depending on a shared runner filesystem.
Does max-parallel: 2 mean the entire workflow can
run only two jobs at once?
No. It limits simultaneous jobs generated by that matrix strategy. Other independent jobs are governed by runner availability and other concurrency limits.
Why does the host-runner service test use
127.0.0.1:6379?
Because the Redis service port is mapped to the runner host. If the job itself were containerized, the service label hostname would be the normal container-to-container address.
The build job creates a local file, but the test jobs do not see it. Is that a failure?
No. It is the expected fresh-runner boundary. Transfer generated files explicitly when they are part of the contract.
Why is fail-fast: false not the same as
continue-on-error: true?
Fail-fast controls whether sibling matrix jobs are canceled after a failure; continue-on-error changes how a specific job failure affects overall failure semantics.
Further reading — current official GitHub sources
- GitHub Docs — Workflow syntax
- GitHub Docs — Using jobs in a workflow
- GitHub Docs — Running variations of jobs
- GitHub Docs — GitHub-hosted runners
- GitHub Docs — Running jobs in a container
- GitHub Docs — Docker service containers
- GitHub Docs — Store and share workflow data
- GitHub CLI — gh run view
- GitHub REST — Workflow jobs
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.