Repository navigation
feat(graphql-server): serve the operator-injected release tuple at /.well-known/constructive-release - #1917
feat(graphql-server): serve the operator-injected release tuple at /.well-known/constructive-release#1917pyramation wants to merge 2 commits into
Conversation
…well-known/constructive-release
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
Review complete. 🟡 1 medium 💬 Inline comments (1)
This change introduces a configurable "release" identity tuple (version, sha, constructiveDb) sourced from
Reviewed commit: a9cd3af |
There was a problem hiding this comment.
Adds a versioned release middleware exposing CONSTRUCTIVE_RELEASE_* metadata at a well-known server endpoint, with shared env parsing in graphql/env and exported types in graphql/types.
Key findings
- 🟡 Partial runtime release tuple served with HTTP 200 — release.ts:6
| export const createReleaseHandler = (release: ReleaseOptions): RequestHandler => { | ||
| const tuple: ReleaseOptions = { | ||
| hub: release.hub, | ||
| constructive: release.constructive, | ||
| constructiveDb: release.constructiveDb, | ||
| dashboard: release.dashboard, | ||
| functions: release.functions | ||
| }; |
There was a problem hiding this comment.
🟡 bug · medium
Partial runtime release tuple served with HTTP 200
The all-or-nothing validation for the release tuple exists only on the env path (getReleaseEnv, graphql/env/src/release.ts:20-27). createReleaseHandler copies its five fields with no completeness check (graphql/server/src/middleware/release.ts:6-13), and getEnvOptions deepmerges runtime overrides unvalidated (graphql/env/src/merge.ts:51), so a runtime-supplied partial release option reaches the handler intact. Since JSON.stringify drops keys whose values are undefined, the endpoint returns 200 with missing fields, silently breaking the RELEASE_FIELDS contract (exact 5-key body) that the acceptance tests and workflow preflight rely on.
📋 Prompt for AI Agents
In graphql/server/src/middleware/release.ts lines 6-13, inside createReleaseHandler, validate that every field of the passed release object (hub, constructive, constructiveDb, dashboard, functions) is a non-empty string and throw a descriptive Error otherwise, mirroring the all-or-nothing invariant enforced by getReleaseEnv (graphql/env/src/release.ts:15-31) so a runtime-supplied partial release option cannot produce a 200 response missing keys required by the RELEASE_FIELDS contract.
Summary
Fixes constructive-io/constructive-planning#2289. Hub's
live.ymlpreflight andtests/acceptance/graphql.tsbothGET https://api.launchql.dev/.well-known/constructive-releaseand need a non-empty JSON release tuple. Nothing served that path, so devnet answers 404 and live acceptance cannot pass.The GraphQL server now serves the tuple the cloud operator already renders from
ConstructiveApp.spec.release:graphql/envgetReleaseEnv: readsCONSTRUCTIVE_RELEASE_{HUB,CONSTRUCTIVE,CONSTRUCTIVE_DB,DASHBOARD,FUNCTIONS}intoopts.release(newReleaseOptionsingraphql-types). It is all-or-nothing. If none of the variables are set, there is no tuple and no route. If any of them is set, all five must be non-empty, orgetEnvOptionsthrows and names the bad variables. The server will not start with a partial tuple.graphql/servercreateReleaseHandlerchecks the same invariant again for runtime options that skip the env parser (new Server({ release })goes through an unvalidated deepmerge). If any of the five fields is missing or empty, it throws and names them, so the server never answers 200 with a partial tuple.graphql/server: whenopts.releaseis set,app.get(RELEASE_PATH, createReleaseHandler(...))is mounted next to/healthz. It sits before domain/API routing, so it answers on any host, includingapi.launchql.dev.ReleaseSpec(hub, constructive, constructiveDb, dashboard, functions). These are also hub'sRELEASE_FIELDS, whichlive.ymlcompares key by key. The largerconstructive.cloud/release/v1candidate artifact from constructive-hub#892 is a different object: cloud promotion turns it intospec.release. It is not what this endpoint serves.Why here and not a new operator-rendered static route
The issue proposes having the operator render
spec.releaseas a static JSON route. The operator already rendersspec.releaseinto every server container asCONSTRUCTIVE_RELEASE_*, and those values take precedence over anything in the app's own env (operator/internal/resources/release.go,release_env_test.go). The only missing piece was an HTTP responder on the API host, andapi.launchql.devalready routes toconstructive-public-server. Traefik has no static-response primitive, so an operator-only fix would need a new Deployment, a new pinned image, a Service, NetworkPolicy floor entries, and Traefik path matching (traefikMatchignorespathtoday). Serving it from the server:spec.releaseas the single source of truth, with no second hand-kept copy;Reproduce before fix
The tests are in the normal package jest flow (
graphql/env,graphql/server). For the "before" run, I reverted thesrcchanges (types/env/server), rebuilt thegraphql-typesandgraphql-envdist, and ran the tests:After the fix (dist rebuilt):
Partial runtime
releaseoption (raised in review). The tests were added first and run against the previouscreateReleaseHandler:eslintandtsc --noEmit(graphql/server) are clean.Rollout for devnet
docker-constructive.yamlthen builds theconstructiveserver image frommain.deploy/dev/constructiveapp.yaml, andrelease.constructive/deploy/release.yamlwith it, to that commit. Then roll it out through the normal devnet deploy. No operator or CRD change is needed: the operator already injectsCONSTRUCTIVE_RELEASE_*.curl -fsS https://api.launchql.dev/.well-known/constructive-release | jq -cS .Link to Devin session: https://app.devin.ai/sessions/c7c0041938a3463eb27ccf6a67a7b67e
Open in Devin Desktop: https://app.devin.ai/desktop/session/c7c0041938a3463eb27ccf6a67a7b67e?variant=devin
Requested by: @pyramation