gitea-saas-core#38: assemble one create_app() + a real entrypoint #43

Merged
pbwraith merged 1 commits from issue-38 into main 2026-09-03 11:04:30 +02:00
Owner

Summary

Assembly only (issue #38): one create_app() that composes the 15 modules on main into a single runnable Flask app, plus the real entrypoints. No behaviour changes to billing, factory, dashboard or review.

Files this issue owns, all new or touched here:

  • gitea_saas/app.py (new) — create_app(config=None) -> Flask:
    • builds config via the existing config.py loader (no new env vars; SAAS_API_KEY/SAAS_PORT/SAAS_DB_PATH are the only knobs),
    • opens the DB via db.py (through BillingCore, which owns the connection + idempotent schema bootstrap) and constructs BillingCore,
    • registers, in one place: the billing routes from server.py (server.create_app), register_factory_routes (factory), mount_dashboard (dashboard),
    • fail-closed: with SAAS_API_KEY unset the app still starts but every authed route returns 503 SERVICE_UNAVAILABLE — not 500, not 200. No second auth path: key validation stays in gitea_saas.auth (key_is_valid), and the front-ends' own canonical-401 guards apply unchanged once a key IS configured (covered by tests below). The 503 gate is installed only when no key is configured, only at the assembler layer, ahead of the front-ends' guards; liveness routes in auth.PUBLIC_PATHS (/health, /healthz, /api/factory/health) stay public.
    • when SAAS_DB_PATH is unset the assembler falls back to an ephemeral file in a fresh temp dir so billing/factory/dashboard share one coherent dataset (config.py's in-memory default can't be shared by the factory/dashboard, which open their own connections).
  • wsgi.py (new, repo root) — application = create_app() for gunicorn.
  • pyproject.toml — [project.scripts] gitea-saas = "gitea_saas.app:main" plus runtime dependencies = ["flask>=3.0"] (a real entrypoint implies declaring the runtime dep; matches requirements.txt).
  • tests/test_app.py (new) — acceptance 1 + 2 + configured-key/401 regression + entrypoint wiring.
  • gitea_saas/__main__.py (new) — one extra file beyond the four listed: acceptance 3 requires python -m gitea_saas to serve /health, and the issue's Outcome section lists __main__.py among the four missing pieces. It delegates to gitea_saas.app:main.

No symbols needed exporting: server.create_app, register_factory_routes and mount_dashboard were already importable; server.py, intake_factory.py and dashboard.py are untouched beyond importing.

Note on 503 vs the #24 consolidation: standalone front-ends return canonical 401 for an unset key (pre-#24 they returned 503; #24 consolidated to 401). Acceptance 2 of this issue explicitly requires the ASSEMBLED app to report 503 for the unconfigured deployment — implemented as an unconfigured-state gate at the assembly layer only, so existing per-front-end 401 behaviour and tests are untouched.

Test evidence

  • python3 -m pytest (full suite): 244 passed, 1 skipped (34s).
  • git diff --check: clean.
  • Acceptance 3 verified live: env -u SAAS_API_KEY -u SAAS_DB_PATH SAAS_PORT=8137 python3 -m gitea_saas starts with zero config; then
    • GET /health → 200
    • GET /healthz → 200
    • GET /api/factory/health → 200
    • GET /api/clients/1 (authed route, no key) → 503 {"error":{"code":"SERVICE_UNAVAILABLE", ...}}
    • server stopped afterwards.

Closes #38

## Summary Assembly only (issue #38): one `create_app()` that composes the 15 modules on `main` into a single runnable Flask app, plus the real entrypoints. No behaviour changes to billing, factory, dashboard or review. Files this issue owns, all new or touched here: - `gitea_saas/app.py` (new) — `create_app(config=None) -> Flask`: - builds config via the existing `config.py` loader (no new env vars; `SAAS_API_KEY`/`SAAS_PORT`/`SAAS_DB_PATH` are the only knobs), - opens the DB via `db.py` (through `BillingCore`, which owns the connection + idempotent schema bootstrap) and constructs `BillingCore`, - registers, in one place: the billing routes from `server.py` (`server.create_app`), `register_factory_routes` (factory), `mount_dashboard` (dashboard), - fail-closed: with `SAAS_API_KEY` unset the app still starts but every authed route returns `503 SERVICE_UNAVAILABLE` — not 500, not 200. No second auth path: key validation stays in `gitea_saas.auth` (`key_is_valid`), and the front-ends' own canonical-401 guards apply unchanged once a key IS configured (covered by tests below). The 503 gate is installed only when no key is configured, only at the assembler layer, ahead of the front-ends' guards; liveness routes in `auth.PUBLIC_PATHS` (`/health`, `/healthz`, `/api/factory/health`) stay public. - when `SAAS_DB_PATH` is unset the assembler falls back to an ephemeral file in a fresh temp dir so billing/factory/dashboard share one coherent dataset (config.py's in-memory default can't be shared by the factory/dashboard, which open their own connections). - `wsgi.py` (new, repo root) — `application = create_app()` for gunicorn. - `pyproject.toml` — `[project.scripts] gitea-saas = "gitea_saas.app:main"` plus runtime `dependencies = ["flask>=3.0"]` (a real entrypoint implies declaring the runtime dep; matches `requirements.txt`). - `tests/test_app.py` (new) — acceptance 1 + 2 + configured-key/401 regression + entrypoint wiring. - `gitea_saas/__main__.py` (new) — one extra file beyond the four listed: acceptance 3 requires `python -m gitea_saas` to serve `/health`, and the issue's Outcome section lists `__main__.py` among the four missing pieces. It delegates to `gitea_saas.app:main`. No symbols needed exporting: `server.create_app`, `register_factory_routes` and `mount_dashboard` were already importable; `server.py`, `intake_factory.py` and `dashboard.py` are untouched beyond importing. Note on 503 vs the #24 consolidation: standalone front-ends return canonical 401 for an unset key (pre-#24 they returned 503; #24 consolidated to 401). Acceptance 2 of this issue explicitly requires the ASSEMBLED app to report 503 for the unconfigured deployment — implemented as an unconfigured-state gate at the assembly layer only, so existing per-front-end 401 behaviour and tests are untouched. ## Test evidence - `python3 -m pytest` (full suite): **244 passed, 1 skipped** (34s). - `git diff --check`: clean. - Acceptance 3 verified live: `env -u SAAS_API_KEY -u SAAS_DB_PATH SAAS_PORT=8137 python3 -m gitea_saas` starts with zero config; then - `GET /health` → 200 - `GET /healthz` → 200 - `GET /api/factory/health` → 200 - `GET /api/clients/1` (authed route, no key) → **503** `{"error":{"code":"SERVICE_UNAVAILABLE", ...}}` - server stopped afterwards. Closes #38
pbwraith added 1 commit 2026-09-01 10:49:33 +02:00
Author
Owner

[worker-done] gitea-saas-core#38 assembled: one create_app() + real entrypoints.

Commit: 0f9d2a9 (branch issue-38, PR #43).

Test evidence:

  • Full suite: 244 passed, 1 skipped (python3 -m pytest).
  • git diff --check: clean.
  • Acceptance 1 & 2 in tests/test_app.py (URL-map composition incl. billing + /api/factory/* + /dashboard + /healthz; no SAAS_API_KEY -> authed routes 503, liveness 200).
  • Acceptance 3 verified live: python3 -m gitea_saas with zero SAAS env started on 127.0.0.1:8137; /health 200, /healthz 200, /api/factory/health 200, authed GET /api/clients/1 -> 503 SERVICE_UNAVAILABLE (not 500/200); server stopped after.

Ready for manual review (PR #43, branch only, Closes #38).

[worker-done] gitea-saas-core#38 assembled: one create_app() + real entrypoints. Commit: 0f9d2a9 (branch issue-38, PR #43). Test evidence: - Full suite: 244 passed, 1 skipped (`python3 -m pytest`). - git diff --check: clean. - Acceptance 1 & 2 in tests/test_app.py (URL-map composition incl. billing + /api/factory/* + /dashboard + /healthz; no SAAS_API_KEY -> authed routes 503, liveness 200). - Acceptance 3 verified live: `python3 -m gitea_saas` with zero SAAS env started on 127.0.0.1:8137; /health 200, /healthz 200, /api/factory/health 200, authed GET /api/clients/1 -> 503 SERVICE_UNAVAILABLE (not 500/200); server stopped after. Ready for manual review (PR #43, branch only, Closes #38).
Author
Owner

Held — main is red, and it is not this PR's fault

Gated against the real merge result. The comparison that matters:

tree pytest
origin/main @ 71cc263 (no PR) 10 failed, 225 passed
main + this PR 10 failed, 234 passed

The same 10 failures, and this PR adds 9 passing tests and zero new
failures.
Every failure is in tests/test_review_workspace.py /
tests/test_reviewer.py — files this PR does not touch.

So this is not a bounce and nothing is being asked of you. The work is good; it
is queued behind a pre-existing defect filed as #48: on Windows the shell
eats the ^ in git rev-parse --verify <sha>^{commit}, so ReviewWorkspace
cannot build a synthetic merge on the review box at all.

I will not merge onto a red main — with the baseline broken there is no way to
tell a genuine regression from the existing noise, which is exactly how
botmanager shipped a broken master last week. Once #48 is green I will
re-gate and merge.

## Held — `main` is red, and it is not this PR's fault Gated against the real merge result. The comparison that matters: | tree | pytest | |---|---| | `origin/main` @ `71cc263` (**no PR**) | **10 failed**, 225 passed | | `main` + this PR | **10 failed**, 234 passed | **The same 10 failures, and this PR adds 9 passing tests and zero new failures.** Every failure is in `tests/test_review_workspace.py` / `tests/test_reviewer.py` — files this PR does not touch. So this is not a bounce and nothing is being asked of you. The work is good; it is queued behind a pre-existing defect filed as #48: on Windows the shell eats the `^` in `git rev-parse --verify <sha>^{commit}`, so `ReviewWorkspace` cannot build a synthetic merge on the review box at all. I will not merge onto a red `main` — with the baseline broken there is no way to tell a genuine regression from the existing noise, which is exactly how botmanager shipped a broken `master` last week. Once #48 is green I will re-gate and merge.
Author
Owner

Merged — verified against green main

tree pytest
origin/main (after !49) 236 passed
main + this PR 245 passed, 1 skipped, 0 failed

0 conflicts. This PR was held, never bounced — it was queued behind the Windows caret defect (#48), which !49 fixed. The service can finally be started by someone who does not know its internals — create_app(), wsgi.py and a console entrypoint. 15 working modules had no ignition.

## Merged — verified against green `main` | tree | pytest | |---|---| | `origin/main` (after !49) | 236 passed | | `main` + this PR | **245 passed, 1 skipped, 0 failed** | 0 conflicts. This PR was *held*, never bounced — it was queued behind the Windows caret defect (#48), which !49 fixed. The service can finally be started by someone who does not know its internals — `create_app()`, `wsgi.py` and a console entrypoint. 15 working modules had no ignition.
pbwraith merged commit a66f88ea10 into main 2026-09-03 11:04:30 +02:00
Sign in to join this conversation.
No reviewers
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: pbwraith/gitea-saas-core#43
No description provided.