# Synchronous build API `build_api` is the HTTP front of **coruna-lab-web**. It runs `frontend/tools/new_project.py` synchronously and publishes completed releases for a web server to serve directly from the local filesystem. Laravel lives in the sibling `coruna-lab` project and calls this API over the network. ## Storage layout ```text / ├── channel// # published web/, sync/, out/, manifest.json ├── staging// # private build workspaces └── locks/.lock # nonblocking advisory locks ``` Staging and published releases are children of the same artifact root and are checked to be on the same filesystem. A successful build is renamed into `channel/` only after its required output and absence of symlinks have been verified. Builder failures remove staging and leave an existing release untouched. Every release gets a `manifest.json` containing the normalized build input, its SHA-256 digest, request ID, build timestamp, and a digest of builder stdout. An identical input returns the existing manifest without running the builder. Different input for an existing channel returns `409`; `"force": true` replaces it after a new build succeeds. ## Configuration The service reads: - `BUILD_API_TOKEN` (required): exact Bearer token. - `BUILD_API_ARTIFACT_ROOT`: defaults to `artifacts/` in the repository. - `BUILD_API_PROJECT_SCRIPT`: defaults to `frontend/tools/new_project.py`. - `BUILD_API_PYTHON`: Python used to invoke the builder. - `BUILD_API_HOST` / `BUILD_API_PORT`: defaults to `127.0.0.1:8081`. - `BUILD_API_TIMEOUT`: synchronous build timeout in seconds, default `900`. Install API dependencies, then run from the repository root: ```bash python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt export BUILD_API_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(48))')" export BUILD_API_ARTIFACT_ROOT=/srv/coruna-artifacts python3 -m build_api ``` The HTTP layer is **FastAPI** served by **Uvicorn**. Route contracts, status codes, and Bearer auth are unchanged for Laravel clients. The builder contract is: ```text python new_project.py \ --root /staging/ \ --channel-id \ --deployment-domains ... \ --reporting-domains ... ``` It must create a channel-scoped tree below `--root`: ```text / ├── web/support.html ├── sync/daily.html ├── out/ └── manifest.json # includes channel_id, support_path, daily_path ``` The API always passes `--force` because it pre-creates the staging directory. It never writes into the Laravel `public/` directory. Published static URLs are: ```text /channel//web/support.html /channel//sync/daily.html ``` Run the API as a long-lived process (for example via a process manager in 宝塔) and put a reverse proxy in front of `BUILD_API_HOST`/`BUILD_API_PORT`. Point the public static site document root at the artifact root so only `channel//web/` and `channel//sync/` are reachable; keep `staging/`, `locks/`, manifests, and `out/` private. The web server user needs read access to published releases; it should not have write access. ## API Health is public: ```bash curl https://builds.example.com/health ``` Build: ```bash curl -i -X POST \ -H "Authorization: Bearer $BUILD_API_TOKEN" \ -H "Content-Type: application/json" \ https://builds.example.com/v1/channels/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/build \ -d '{ "deployment_domains": ["deploy.example"], "reporting_domains": ["report.example"] }' ``` The first successful build returns `201`; the same normalized input returns `200`. A busy channel or conflicting existing release returns `409`, and an invalid channel, JSON body, domain, or field returns `422`. Status and deletion: ```bash curl -H "Authorization: Bearer $BUILD_API_TOKEN" \ https://builds.example.com/v1/channels/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa curl -i -X DELETE -H "Authorization: Bearer $BUILD_API_TOKEN" \ https://builds.example.com/v1/channels/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ``` Deletion returns `204` whether or not the release exists. It rejects symbolic links and takes the same per-channel lock as builds. Public static URLs: ```text /channel//web/support.html /channel//sync/daily.html ``` Only `web/` and `sync/` under each channel should be exposed; manifests, `out/`, staging, and locks stay private. ## Migration checklist 1. Deploy `build_api` + artifact root + reverse proxy / static site via your panel. 2. Set Laravel `CORUNA_BUILD_SERVICE_*` and static-site domains. 3. Create a new channel from Admin and verify support/daily URLs on the static host. 4. Keep legacy `public/web` and `public/sync` read-only until old channels are rebuilt or retired. 5. Device validation (ops): capture that the implant requests `/channel//sync/daily.html` then `/channel//sync/`. ## Tests ```bash python3 -m unittest -v tests.test_build_api ```