Course outline

GitHub Actions / Events, execution, and data boundaries / Lesson 4

Runners, shells, and working directories

Make execution assumptions explicit instead of depending on a familiar label.

About 4 min readContent reviewed
On this page

Lesson goals

  • Separate hosted runner labels from toolchain identity.
  • Review shell and filesystem assumptions in a portable job.

runs-on selects execution capacity

A job’s runs-on value selects eligible runner capacity, which can be GitHub-hosted or self-hosted under repository and organization policy. Labels identify routing characteristics; they do not independently prove trust, a fixed image, or availability. A job can queue because no permitted runner matches its labels even when its script is correct. Parcel’s learning examples use hosted labels only as illustrations. Do not add a self-hosted label as a shortcut around queues without reviewing the host’s network, identities, persistence, and access policy.

A hosted image is not your lockfile

Hosted image contents and latest labels change over time. Record the relevant image and tool versions for diagnosis, and select an explicit application toolchain through reviewed setup when needed. An explicit operating-system label still does not pin every preinstalled package forever. Keep build inputs declared and avoid relying on incidental utilities found on one machine. If a job needs a particular executable, establish that requirement in its setup contract rather than copying a successful local terminal session into YAML.

Choose the interpreter intentionally

The default shell and command wrapper depend on platform and configuration. Bash syntax does not automatically work in PowerShell, and error propagation can differ. Specify shell when its semantics matter, then understand how the runner invokes it. A working-directory default also applies only to supported run steps; it does not reconfigure arbitrary action inputs. The following fragment chooses bash and an explicit relative working directory, assuming that directory and the fixture script already exist after a separate checkout or generation step.

yaml Declare a shell and relative directory
defaults:
  run:
    shell: bash
    working-directory: fixture
steps:
  - name: Check fixture
    run: python check_fixture.py

This job fragment is incomplete and non-runnable. It does not create fixture/check_fixture.py, select a Python version, or check out source. Those missing prerequisites are explicit rather than hidden behind an asserted successful run.

Use platform-aware path construction

Hardcoded path separators, case assumptions, and absolute home directories make jobs fragile across operating systems and execution contexts. Use declared workspace paths or language-native path operations. A Linux filesystem usually treats App.py and app.py as different names, while behavior on other systems can differ. Avoid using a developer’s local home path as a CI contract. The complete local program below constructs a fixture path without touching a repository or relying on the actual host’s separator convention.

python Reason about path components offline
from pathlib import PurePosixPath, PureWindowsPath
parts = ("fixture", "reports", "result.json")
print(PurePosixPath(*parts))
print(PureWindowsPath(*parts).as_posix())
Expected output
fixture/reports/result.json
fixture/reports/result.json

Both lines use an explicit logical path. Pure path objects perform no filesystem access and do not prove that a file exists on any runner. as_posix deliberately renders the Windows path with forward slashes for comparison.

Diagnose with narrow environment evidence

When a job differs from local behavior, collect the selected runner class, relevant tool versions, shell choice, working directory, and exact failing operation. Do not dump the full environment or recursively archive host directories, because they can contain secrets and unrelated data. Separate missing executable, wrong directory, permission denial, architecture mismatch, and queueing. Changing runner, interpreter, dependencies, and permissions simultaneously destroys diagnostic clarity. Fix the smallest incorrect assumption and retain enough evidence to confirm that the same revision now satisfies the intended contract.

Common errors and fixes

A script works in Bash but fails on a Windows runner.

Why it happens
The job relies on Bash syntax while the selected shell differs.
How to fix it
Select an available intended shell or write the script for the actual interpreter.
Prevent it next time
Review shell and quoting assumptions for every supported platform.

A job queues forever with an unfamiliar self-hosted label.

Why it happens
No eligible authorized runner matches the requested labels.
How to fix it
Inspect routing and capacity with the platform owner.
Prevent it next time
Treat runner labels and ownership as reviewed configuration.

A setup action still reads the wrong directory.

Why it happens
A run-step working-directory default was assumed to affect action-specific inputs.
How to fix it
Set the action’s documented path input separately.
Prevent it next time
Distinguish run defaults from uses action contracts.

Check your understanding

1. Does ubuntu-latest guarantee the same compiler next month?

Show worked solution

No. It selects a moving hosted environment, and installed tools can change. Record relevant versions and explicitly establish the application toolchain. Even a fixed OS label needs a reproducibility strategy for the inputs that affect release bytes.

2. A file exists locally as Report.json but a Linux job asks for report.json. What should change?

Show worked solution

Align the tracked filename and reference with the intended case, then verify the repository state. Do not fix this by broadening permissions or changing to a more privileged runner. The failure concerns path identity, not necessarily access authority.

Key takeaways

  • Runner routing, image contents, shell, and paths are separate assumptions.
  • Collect narrow environment evidence before changing execution boundaries.

Official references