All checks were successful
CD / Build and push images (push) Successful in 2m36s
CI / Lint, typecheck, test (push) Successful in 1m50s
CI / Auth e2e pack (push) Successful in 1m58s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
Bootstrap apps/collab as a Hocuspocus WebSocket server (ADR 0003): - pino JSON logging (service=collab) and shared Zod env validation (collabEnvSchema); structured connection open/close logs. - /healthz endpoint (process liveness + PostgreSQL ping) served via the onRequest hook, matching the container-internal path and the proxied /collab/healthz path; any WebSocket handshake is accepted for now (authentication arrives with #34, persistence with #35). - Dockerfile (ESM workspace build) and a compose service on the frontend and internal networks with a healthcheck; dev overlay service and a new COLLAB_PORT variable. - CD builds, pushes, and promotes the collab image; CI builds it on PRs; the smoke suite asserts /collab/healthz through the reverse proxy. - deployment.md/stages.md: proxy routing, per-stage COLLAB_PORT, checklist. Closes #33 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
import type { HealthResponse } from '@dorfteich/shared';
|
|
|
|
/** Result of a single dependency probe. */
|
|
export interface DatabaseProbe {
|
|
ok: boolean;
|
|
detail?: string;
|
|
}
|
|
|
|
/**
|
|
* Liveness + dependency report for the collab service. Unlike the api, which
|
|
* splits pure liveness (`/healthz`) from readiness (`/readyz`), the collab
|
|
* skeleton only needs the one endpoint the compose healthcheck and pipeline
|
|
* smoke test hit, so `/healthz` also carries the database probe (issue #33).
|
|
*/
|
|
export interface CollabHealthReport {
|
|
status: 'ok' | 'unhealthy';
|
|
service: HealthResponse['service'];
|
|
version: string;
|
|
time: string;
|
|
checks: { name: 'database'; status: 'ok' | 'failed'; detail?: string }[];
|
|
}
|
|
|
|
/** Build the health payload and the HTTP status a monitor should see. */
|
|
export function buildHealthReport(
|
|
version: string,
|
|
database: DatabaseProbe,
|
|
): { httpStatus: 200 | 503; body: CollabHealthReport } {
|
|
const body: CollabHealthReport = {
|
|
status: database.ok ? 'ok' : 'unhealthy',
|
|
service: 'collab',
|
|
version,
|
|
time: new Date().toISOString(),
|
|
checks: [
|
|
{
|
|
name: 'database',
|
|
status: database.ok ? 'ok' : 'failed',
|
|
...(database.detail ? { detail: database.detail } : {}),
|
|
},
|
|
],
|
|
};
|
|
return { httpStatus: database.ok ? 200 : 503, body };
|
|
}
|
|
|
|
/**
|
|
* Whether a plain HTTP request is a health probe. Matches both the container-
|
|
* internal path (`/healthz`, used by the Docker healthcheck) and the path seen
|
|
* through the host reverse proxy, which routes `/collab*` to this service
|
|
* without stripping the prefix (deployment.md), i.e. `/collab/healthz`.
|
|
*/
|
|
export function isHealthRequest(method: string | undefined, url: string | undefined): boolean {
|
|
if (method !== undefined && method !== 'GET' && method !== 'HEAD') {
|
|
return false;
|
|
}
|
|
const pathname = pathnameOf(url);
|
|
return pathname === '/healthz' || pathname === '/collab/healthz';
|
|
}
|
|
|
|
function pathnameOf(url: string | undefined): string {
|
|
if (!url) {
|
|
return '';
|
|
}
|
|
// The base only matters to parse a path-only request-target; it is discarded.
|
|
return new URL(url, 'http://collab.internal').pathname.replace(/\/+$/, '') || '/';
|
|
}
|