GitHub Actions logs can look intimidating at first.
A failed CI run may show many lines of output, but usually only one step contains the important error.
This page explains how to read CI logs slowly and find the useful information.
Use this guide when:
- a pull request has a red CI check,
- a workflow failed after pushing to
main, - local checks pass but GitHub Actions fails,
- you do not know which command failed,
- the logs look too long to understand.
Do not read the whole log from top to bottom immediately.
Start by finding:
Which job failed?
Which step failed?
What was the first useful error message?
The first useful error is usually more important than the last line of the log.
When a pull request has a failing check:
- Open the pull request on GitHub.
- Scroll to the checks section.
- Click the failed check.
- Open the failed job.
- Open the failed step.
In this project, the job is called:
Quality checks
The workflow is called:
CI
This project uses a workflow similar to:
name: CI
jobs:
quality:
name: Quality checks
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- name: Install uv
- name: Set up Python
- name: Install dependencies
- name: Run Ruff linting
- name: Check formatting
- name: Run testsWhen CI fails, one of these steps usually failed.
Do not debug everything at once.
Start with the failed step.
This step downloads the repository files into the GitHub Actions runner.
Example:
- name: Checkout repository
uses: actions/checkout@v6This is uncommon.
Possible causes include:
- GitHub service issue,
- invalid workflow syntax,
- repository access problem.
Usually, this is not caused by Python code.
This step installs uv in the CI environment.
Example:
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0Possible causes include:
- wrong action name,
- invalid action version,
- temporary network issue,
- GitHub Actions service issue.
Check whether the uses: line is correct.
This step installs the Python version requested by the project.
Example:
- name: Set up Python
run: uv python installThe project uses:
.python-version
to define the selected Python version.
Possible causes include:
- invalid Python version in
.python-version, - Python version not available,
uvwas not installed correctly in the previous step.
Check:
.python-version
For this guide, it should contain something like:
3.12
This step installs project dependencies in CI.
Example:
- name: Install dependencies
run: uv sync --lockedCI uses:
uv sync --lockedbecause it should verify the committed lockfile, not silently update it.
A common cause is that pyproject.toml and uv.lock are out of sync.
This may happen after adding or removing a dependency without committing the updated lockfile.
Run locally:
uv syncor:
uv lockThen check:
git statusIf uv.lock changed, commit it:
git add pyproject.toml uv.lock
git commit -m "chore: update dependency lockfile"Then push again.
This step runs:
uv run ruff check .It checks the project for selected linting issues.
The log usually shows:
- file path,
- line number,
- rule code,
- message.
Example:
src/example_project/module.py:1:1: F401 `os` imported but unused
This means:
file: src/example_project/module.py
line: 1
column: 1
rule: F401
problem: os was imported but not used
Open the file and fix the issue.
Some Ruff issues can be fixed automatically:
uv run ruff check . --fixThen review the diff:
git diffDo not apply fixes blindly without reading the changes.
After fixing, run:
uv run ruff check .
uv run ruff format --check .
uv run pytestThis step runs:
uv run ruff format --check .It checks whether files are formatted correctly.
It does not modify files in CI.
The log may say that files would be reformatted.
That means formatting is different from Ruff's expected format.
Run locally:
uv run ruff format .Then run all checks again:
uv run ruff check .
uv run ruff format --check .
uv run pytestCommit the formatting changes:
git add .
git commit -m "style: format files"If the formatting change is part of another small documentation or code PR, it may be better to amend the existing commit instead of adding a separate formatting-only commit.
This step runs:
uv run pytestIt checks whether the code behaves as expected.
Pytest usually shows:
- failing test file,
- failing test name,
- assertion error,
- expected value,
- actual value.
Example:
FAILED tests/test_text_stats.py::test_count_words_handles_repeated_whitespace
This tells you which test failed.
Start by reading:
test file
test name
assertion error
Then run tests locally:
uv run pytestFor more detail:
uv run pytest -vTo run one test file:
uv run pytest tests/test_text_stats.pyFix the code or the test depending on what is actually wrong.
Then run:
uv run ruff check .
uv run ruff format --check .
uv run pytestThis can happen.
Common causes:
- uncommitted local files,
- missing
uv.lockupdate, - different Python version,
- branch is stale,
- workflow file changed,
- platform-specific behavior.
Start locally:
git status
uv sync
uv run ruff check .
uv run ruff format --check .
uv run pytestIf git status shows changes, check whether they should be committed.
If uv.lock changed, commit it.
If local checks still pass, inspect the failed CI step carefully.
If CI fails on main, do not ignore it.
The main branch should stay healthy.
Recommended response:
- Open the failed workflow run.
- Identify the failed job and step.
- Read the first useful error.
- Reproduce locally if possible.
- Create a small fix branch.
- Open a pull request with the fix.
Example branch:
git switch main
git pull
git switch -c fix/ci-failureExample commit:
git commit -m "fix: resolve CI failure"Long logs are easier to read if you search for keywords.
Useful words to search for:
error
failed
FAILED
Traceback
ModuleNotFoundError
AssertionError
would be reformatted
out of date
But be careful.
Some logs contain warnings or internal messages that are not the actual cause.
Focus on the failed step first.
The last line of a log may only say:
Error: Process completed with exit code 1.
That line tells you that the command failed.
It does not explain why.
The useful error is usually above it.
Look for the first message that explains the real problem.
When CI fails, avoid changing many unrelated things.
Bad approach:
format files, update dependencies, rewrite tests, change CI, and refactor code
Better approach:
fix the one issue that caused CI to fail
Then run checks and push again.
Small fixes are easier to review.
Check repository state:
git statusReview unstaged changes:
git diffReview staged changes:
git diff --stagedSync dependencies:
uv syncRun quality checks:
uv run ruff check .
uv run ruff format --check .
uv run pytestFormat files:
uv run ruff format .Run tests with more detail:
uv run pytest -vSymptom:
uv sync --locked
fails.
Likely fix:
uv lock
git add pyproject.toml uv.lock
git commit -m "chore: update lockfile"Symptom:
would be reformatted
Likely fix:
uv run ruff format .
git add .
git commit -m "style: format files"Symptom:
F401 imported but unused
Likely fix:
Remove the unused import.
Symptom:
AssertionError
Likely fix:
Check whether the implementation or the test expectation is wrong.
Symptom:
ModuleNotFoundError
Likely fix:
Check project structure and pytest configuration:
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]A green CI run means:
- dependencies installed,
- Ruff linting passed,
- formatting check passed,
- tests passed.
It does not mean:
- the design is perfect,
- the documentation is clear,
- the change is useful,
- the pull request should be merged without review.
CI checks repetitive things.
Humans still review meaning and quality.
When CI fails, ask:
Which step failed?
What command did that step run?
Can I run the same command locally?
What is the first useful error message?
Do not panic.
CI is not judging you.
It is giving feedback.