Chapter 15Lesson 02~200 minutes

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.

Hands-on workflowmax-parallelJob containerRedis service

Learning objectives

  • Create a build → matrix-test dependency graph with an explicit source-SHA contract.
  • Use include/exclude, fail-fast, and max-parallel in 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 gh and 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?

Does max-parallel: 2 mean the entire workflow can run only two jobs at once?

Why does the host-runner service test use 127.0.0.1:6379?

The build job creates a local file, but the test jobs do not see it. Is that a failure?

Why is fail-fast: false not the same as continue-on-error: true?

Next lesson

Next: Jobs, Steps, Matrices, Containers, Service Containers, and Dependency Graphs: Configuration, Design Choices, and Tradeoffs

Further reading — current official GitHub sources

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.