fix(api): boot when only services/api is deployed - #65
Merged
Conversation
EduPav
force-pushed
the
fix/api-boots-from-service-root
branch
from
August 13, 2026 15:06
cf9c65a to
a551d7b
Compare
The API crashed at import time — before serving a single request — whenever the deployed tree is the service directory rather than the whole repository. `_counter_path()` reached the repo root with `Path(__file__).resolve().parents[4]`, which assumes six levels (repo/services/api/app/repo/counter.py). Deploy only `services/api` — Railway with the service root directory set to it, or a Docker build that copies just this directory — and the tree is `/app/app/repo/counter.py`: four levels, so `parents[4]` indexes past the filesystem root and raises `IndexError`. That propagates through `app.repo.__init__` -> `app.service.files` -> `app.runtime.files` -> `main`, so uvicorn dies during `load_app()`. Observed on Railway as a successful build followed by a health check that never passes. Both call sites made the same assumption: - `app/repo/counter.py` now anchors at the repo root when those ancestors exist and at this service's own root otherwise. The reason for preferring the repo root is unchanged — `uvicorn --reload` watches services/api/, so runtime state written there causes reload noise — and it simply does not apply to a deployment with no reloader. - `main.py` resolved the repo-root `.env` three levels up, landing on `/.env` in a service-only tree. That failed silently rather than crashing (configuration comes from real environment variables there, and `load_dotenv` on a missing file is a no-op), but the comment claimed the path resolved correctly from anywhere, which was not true. `tests/test_counter_path.py` pins both layouts and the absolute-path bypass, so neither anchor can regress. Nothing existing covered this: the suite monkeypatches `download_count_file` to an absolute tmp path and never exercises the relative branch.
EduPav
force-pushed
the
fix/api-boots-from-service-root
branch
from
August 13, 2026 20:35
a551d7b to
7f8b11f
Compare
EduPav
marked this pull request as ready for review
August 13, 2026 20:37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The API crashed at import time, before serving a single request, whenever the deployed
tree is the service directory rather than the whole repository.
_counter_path()reached the repo root withPath(__file__).resolve().parents[4], whichassumes six levels:
repo/services/api/app/repo/counter.py. Deploy onlyservices/api—a platform with the service root directory set to it, or a Docker build that copies just
that directory — and the tree is
/app/app/repo/counter.py: four levels.parents[4]thenindexes past the filesystem root and raises
IndexError, which propagates throughapp.repo.__init__→app.service.files→app.runtime.files→main, so uvicorn diesinside
load_app().The failure signature is nasty: the build succeeds, the image is pushed, and then the
health check simply never passes —
1/1 replicas never became healthy. Nothing in the buildlog hints at a Python error, because the crash happens in the runtime container.
Both call sites shared the assumption:
app/repo/counter.pynow anchors at the repo root when those ancestors exist, and atthis service's own root otherwise. The reason for preferring the repo root is unchanged —
uvicorn --reloadwatchesservices/api/, so runtime state written there triggers reloadnoise — and it simply does not apply where there is no reloader.
main.pyresolved the repository-root dotenv file three levels up, landing on anonexistent path at the filesystem root in a service-only tree. That one failed silently
rather than crashing (configuration comes from real environment variables there, and
load_dotenvon a missing file is a no-op), but the comment claimed the path "resolvescorrectly regardless of where uvicorn is invoked from", which was not true.
Why it was never caught
The suite monkeypatches
download_count_fileto an absolute tmp path, so the relativebranch — the only one that anchors — was never exercised. And the deployment target that
exposes it is new: on Vercel the whole repository is uploaded and the entrypoint is
services/api/index.py, so all six levels exist andparents[4]resolves fine.tests/test_counter_path.pynow pins the full-checkout layout, the service-only layout, andthe absolute-path bypass, so neither anchor can regress to the other.
Checks run by Claude
pnpm verify:api— green: ruff, 187 passed (184 + the 3 added here), and all fourstructure tests including the file-size invariant
set to
services/api: buildSUCCESS, then health check failing six attempts over its100s window, with the
IndexErrortraceback in the runtime logs__file__/parents[N]anchor underservices/api; the only other two arein
scripts/(export_openapi.py,setup_b2_cors.py), which are developer tools run froma checkout and are correct as they stand
Manual checks
None outstanding — the fix is verified by the redeployed service booting and answering
/health, which is covered in the deployment evidence rather than left to a reviewer.