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: Lint Markdown
- name: Build documentation site
- name: Run tests with coverage
- name: Run type checking
- name: Build distributionsWhen CI fails, one of these steps usually failed.
Do not debug everything at once.
Start with the failed step.
The separate Documentation workflow runs after relevant changes reach main.
It has two jobs:
Build documentation
Deploy documentation
The build job creates and uploads the site artifact. The deploy job publishes
that artifact to the github-pages environment.
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 --locked --group devCI uses:
uv sync --locked --group devbecause 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 rumdl check .
uv run mkdocs build --strict
uv run pytest --cov=text_toolkit --cov-report=term-missing
uv run mypy
uv build --no-sourcesThis 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 rumdl check .
uv run mkdocs build --strict
uv run pytest --cov=text_toolkit --cov-report=term-missing
uv run mypyCommit 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 rumdl check .It checks Markdown structure and consistency using the project configuration.
The log shows the file, line, rule identifier, and a short explanation. Read the rule before changing the document; some project-specific patterns may need a narrow configuration exception.
Run the check locally:
uv run rumdl check .Some issues can be fixed with uv run rumdl check --fix ., but always review
the resulting diff before committing it.
This step runs:
uv run mkdocs build --strictIt renders the documentation site and treats warnings about navigation, links, anchors, or configuration as failures.
Read the first warning in the log. Later messages may be consequences of the same missing page, invalid navigation entry, or broken link.
Run the strict build locally:
uv run mkdocs build --strictFor visual review, run uv run mkdocs serve and open the local address shown in
the terminal.
This step runs:
uv run pytest --cov=text_toolkit --cov-report=term-missingIt checks whether the code behaves as expected and reports statement and branch
coverage for text_toolkit. There is no minimum percentage gate.
Pytest usually shows:
- failing test file,
- failing test name,
- assertion error,
- expected value,
- actual value.
Example:
FAILED tests/test_text_stats.py::test_count_characters_includes_whitespace_by_default
This tells you which test failed.
Start by reading:
test file
test name
assertion error
Then run tests locally:
uv run pytest --cov=text_toolkit --cov-report=term-missingFor 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 rumdl check .
uv run mkdocs build --strict
uv run pytest --cov=text_toolkit --cov-report=term-missing
uv run mypyThis step runs:
uv run mypymypy checks whether code in src/ and tests/ follows the project's type
annotations and strict configuration.
The message usually includes:
- a file path,
- a line number,
- an explanation,
- an error code in square brackets.
Run the same command locally:
uv run mypyFix the code or its annotation. Avoid broad ignores that hide unrelated type errors. Then run the full local checks again.
This step runs:
uv build --no-sourcesIt builds a wheel and source distribution from the project's standard metadata.
Start with the first build backend error. Common causes include:
- invalid project metadata,
- a missing package directory,
- a package name that does not match the source layout,
- a required file missing from the source distribution.
Run the same command locally:
uv build --no-sourcesThe generated dist/ directory is ignored by Git and should not be committed.
This 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 rumdl check .
uv run mkdocs build --strict
uv run pytest --cov=text_toolkit --cov-report=term-missing
uv run mypy
uv build --no-sourcesIf 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 rumdl check .
uv run mkdocs build --strict
uv run pytest --cov=text_toolkit --cov-report=term-missing
uv run mypyFormat files:
uv run ruff format .Run tests with more detail:
uv run pytest -vSymptom:
uv sync --locked --group dev
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 the project structure and build system:
[build-system]
requires = ["uv_build>=0.12.19,<0.13"]
build-backend = "uv_build"Then confirm that the dependency installation step completed successfully.
A green CI run means:
- dependencies installed,
- Ruff linting passed,
- formatting check passed,
- Markdown linting passed,
- documentation site build passed,
- tests and coverage reporting passed,
- type checking passed,
- wheel and source distribution builds 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.
A green Documentation workflow means:
- the site was built strictly from locked dependencies,
- the generated
site/directory was uploaded as an artifact, - GitHub Pages accepted the deployment,
- GitHub reported a public deployment URL.
It does not prove that every page looks or reads as intended. Open the published site and inspect the changed page after deployment.
If Build documentation fails, reproduce the strict build locally. If Deploy documentation fails, inspect Pages settings, workflow permissions, the
github-pages environment, and the first useful deployment error.
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.