GitHub Actions CI for Python — Lint, Test, Build
Think of sys.path as a set of treasure maps Python uses to find modules. Locally, running python tests/test_db.py might add tests/ to the map. In CI, pytest tests/test_db.py might keep the repository root as the primary map location. python -m pytest standardizes the starting point for the search, ensuring consistency.
The Setup
You write unit tests that import your application modules using relative imports. It works perfectly on your workstation, but in the GitHub Actions runner, Python throws a module not found error.
What Does This Print?
import sys
# Simulating CI path resolution mismatch
# In local development, running `python tests/test_db.py` prepends tests/ to sys.path
# In CI, running `pytest tests/test_db.py` keeps workspace root on sys.path.
try:
# A mock package import dependency mimicking actual service structure
if "tests" in sys.path[0]:
import models # Succeeds locally because 'tests/' is in sys.path
else:
raise ModuleNotFoundError("No module named 'models'")
print("Status: Local execution succeeded!")
except ModuleNotFoundError as err:
print(f"Status: CI Build Failed! Reason: {err}")
The Output
In local environments, developers often execute testing scripts from subdirectories or in a way that includes the script's folder in the Python path. However, typical automated CI runners execute pytest from the repository root, creating path mismatches where sibling dependencies cannot be imported, breaking unit test stages.
Why Python Does This
The CPython startup routine populates sys.path[0] with the directory containing the script used to invoke the interpreter. If you run python tests/test_db.py, the directory tests/ is prepended to the path, allowing relative or sibling imports inside that directory. When run inside automated CI runners using general orchestration (like pytest tests/test_db.py directly), the path resolution starts strictly from the root work directory. This discrepancy causes imported module searches to fail immediately unless the project root is explicitly added to sys.path or run via python -m pytest.
The Fix
import sys
import os
# Fix: Ensure the project root directory is added explicitly to sys.path
# This makes modules locatable regardless of the invocation command context
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if project_root not in sys.path:
sys.path.insert(0, project_root)
# Sibling/parent packages can now be imported consistently in CI/CD pipeline runs
print("Status: Import paths correctly structured. Tests running!")
Using python -m pytest or explicitly setting PYTHONPATH ensures that Python's module search path (sys.path) is consistently configured, typically adding the project root to the path. This guarantees that all imports resolve correctly, regardless of the current working directory from which the CI command is executed.
How This Fails in Real Systems
An e-commerce API experienced broken integration tests on master deployments. The pipeline suite passed on a developer's machine but failed continuously on the GitHub Actions runner because of dynamic module resolution path mismatches. This blocked hotfixes from building for three critical hours.