5.3 KiB
Step 07: Server Mode and HTTP API
Prerequisites: steps 04, 05, 06.
Read README.md and 00-legacy-analysis.md first.
Goal
Implement the server subcommand: a persistent web server exposing build state as JSON and serving artifacts.
Design
Bootstrapping:
CLAUDE.mdnotes the context starts with web typenone; theserversubcommand must run a web context. Preferred approach:ServerCommandlaunches a secondSpringApplicationwithWebApplicationType.SERVLETand aserverprofile, then blocks until shutdown. Document the chosen mechanism in the code and, if it deviates, in an ADR.- Add
spring-boot-starter-webdependency. - The watcher (step 06) is active only in the
serverprofile. - New config keys
server.portandserver.bindAddress(defaults 18080 / 0.0.0.0, as legacy).
JSON API (package de.hoennig.gittally.server), replacing the legacy /control/* endpoints:
GET /api/builds/latest— latest build per branch.GET /api/builds/history— all builds, newest first.GET /api/builds/current— the list of running builds (there can be several, one per branch, up tobuilds.maxConcurrent), each with live status and log tail (?offset=for incremental log fetch, addressed by artifact key).GET /api/status/{commit}— effective status including Gitea lookup (replaces/control/status); must return an explicit error state on Gitea failure, never hang.POST /api/builds/{branch}/restart,POST /api/builds/{artifactKey}/cancel(cancel takes the artifact key because multiple builds can run concurrently),DELETE /api/builds/{artifactKey}— guarded by a simple token like the legacy cancel token; wire into executor/watcher/repository.GET /api/watcher— watcher health (last poll, last error).- Artifact serving:
GET /artifacts/{artifactKey}/**streaming from the artifact store, with no-cache headers for html/json/log.
Out of Scope
- HTML pages (step 08); JSON plus artifact files only.
- TLS/reverse proxy (documented in step 12).
Tests
@WebMvcTestslices with@MockkBeanper controller (seeCLAUDE.mdconventions).- Contract tests: JSON shapes, error states (Gitea down → explicit
unknownstatus, HTTP 200), cancel token rejection. - One
@SpringBootTeston the server profile proving the context boots with watcher and web enabled.
Acceptance Criteria
./gradlew ktlintFormatthen./gradlew buildis green.java -jar ... serverstarts,GET /api/builds/latestanswers, Ctrl-C shuts down cleanly (manual smoke test; document result in this file).
Implementation Notes (2026-07-07)
Bootstrapping was implemented as designed, with two additions.
application-server.yml switches spring.main.web-application-type to servlet, because spring.main.* properties override the programmatic SpringApplicationBuilder.web(...) setting.
CliRunner is excluded from the server profile so the second context does not run picocli again.
After Ctrl-C, ServerCommand parks the command thread while the JVM shuts down; otherwise main() races the shutdown hooks and SpringApplication.exit prints a stack trace for the already-closed CLI context.
Deviations and decisions:
- The live log tail is a sibling endpoint:
GET /api/builds/currentlists the running builds (withlogSize), andGET /api/builds/current/{artifactKey}/log?offset=fetches the log incrementally, addressed by artifact key as required. Responses are capped at 1 MiB per chunk. - The control token needs no config key. It is generated on first use and persisted to
.git/gittally/control-token(mode 600); operators can write their own token there, deleting the file rotates it. Requests pass it via theX-GitTally-Tokenheader or atokenparameter; mismatch answers 403 like legacy. DELETE /api/builds/{artifactKey}removes the result and then callsArtifactStore.prune(history), so no new store interface method was needed.GET /api/status/{commit}also accepts abbreviated hashes (7–40 hex like legacy) and resolves them against the local history. TheGiteaClient(step 03) gained 10s connect/read timeouts so the endpoint can never hang; a Gitea failure yields HTTP 200 withstatus: unknown(or the local status) plusgiteaError.POST /api/builds/{branch}/restartrebuilds the branch's last recorded commit. Branch names containing/would need an encoded slash, which Tomcat rejects by default — revisit in step 08 if the UI needs restart for such branches. (Resolved in step 08: the endpoint moved toPOST /api/builds/restart?branch=….)- Spring Boot 4 moved
@WebMvcTestinto the newspring-boot-starter-webmvc-testtest module (added as test dependency). - The server-profile
@SpringBootTestmocks theWatcherbean, so booting the test never fetches origin or enqueues builds; watcher wiring is proven by verifyingstart()was called.
Manual smoke test (2026-07-07): in a scratch repository, java -jar build/libs/gittally-0.1.0-SNAPSHOT.jar server started on the configured port 18981.
GET /api/builds/latest answered [] with HTTP 200, GET /api/watcher exposed the failing fetch of the origin-less repo as lastFetchError, GET /api/status/<sha> answered unknown with HTTP 200, and cancel without token answered 403.
SIGINT (Ctrl-C) shut the process down cleanly in about 2 seconds: port closed, no exceptions in the log, exit code 130.