Skip to content

feat(contrail): wire contrail-community in-process + gate via CI#585

Merged
tompscanlan merged 14 commits into
mainfrom
fix/dockerfile-default-target-release
May 21, 2026
Merged

feat(contrail): wire contrail-community in-process + gate via CI#585
tompscanlan merged 14 commits into
mainfrom
fix/dockerfile-default-target-release

Conversation

@tompscanlan

Copy link
Copy Markdown
Contributor

What this does

Wires contrail-community (the PR #30 + PR #31 integration, vendored from our fork) into openmeet-api in-process — the same pattern as contrail-ingest, no new service and no main.ts change. The existing ContrailProvider is extended so that when both community secrets are configured, the community.* / space.* XRPC routes mount alongside the existing net.openmeet.* read routes.

This is Track D Phase 1, Step 2 (in-process wiring). Step 1 (vendoring @atmo-dev/contrail-community) landed earlier on this branch.

Changes

  • Vendor: add @atmo-dev/contrail-community tarball as a 6th file: dep; prepare-contrail-deps.sh builds/packs all 6 packages.
  • Config (contrail.config.ts): env-gated community block (CONTRAIL_COMMUNITY_ENCRYPTION_KEY, AES-GCM envelope key) and spaces.authority block (CONTRAIL_AUTHORITY_SIGNING_KEY, ES256 P-256 JWK). Community routes mount only when both are present — otherwise the provider behaves exactly as before.
  • Loader (contrail-loader.ts): import the 3 contrail ESM subpaths sequentially rather than via Promise.all. Under jest --experimental-vm-modules the concurrent cold link of the shared contrail-base/appview subgraph races and throws "module is not linked"; sequential import fixes it (and also fixed the pre-existing broken contrail-init-idempotency.spec.ts).
  • Provider: pass communityIntegration into the Contrail instance when configured; init() applies the spaces+community schema next to the ingest schema.
  • Env docs / dev compose: documented the Contrail block in env-example-relational and wired CONTRAIL_* into the api service in docker-compose-dev.yml (DB URL reuses local Postgres, schema-isolated; the keys interpolate from the shell at up time — no key material in git).
  • CI: added CONTRAIL_* (DB URL + throwaway test keys) to env-example-relational-ci so the contrail e2e actually runs in CI instead of skipping silently. Deleted a standalone loader spec that only asserted "an export is a function" and reproduced the jest vm-modules teardown race under --maxWorkers=2.

Test plan (e2e, not checkboxes)

  • test/contrail/contrail-xrpc.e2e-spec.ts — existing 4 read-route cases plus a new community-mount probe: GET /xrpc/net.openmeet.community.getHealth401 (mounted + service-auth gated), asserted explicitly not 404 (unmounted) or 503 (provider down). Gated on the 3 community env vars.
  • Verified in the full layered CI harness locally (docker-compose.relational.ci.yaml + atproto-devnet + docker-compose-ci-devnet.yml, api built from relational.e2e.Dockerfile): Tests: 5 passed, 5 total, EXIT_CODE=0. This PR makes GitHub Actions run that same harness.
  • src/contrail jest suite (config 3/3, wiring 2/2, idempotency 2/2) green under --maxWorkers=2 (CI's worker model).

Notes for reviewers

  • Branch history quirk: this branch previously carried the merged PR fix(docker): drop ingest target so release stays default build stage #583 (Dockerfile ingest-target fix). The first commit here (5e7622b) makes the same Dockerfile removal that already landed in main via fix(docker): drop ingest target so release stays default build stage #583's squash — so the Dockerfile diff shows in the file view but is a no-op against main (both sides made the identical removal; merges with no net change). The real payload is the 12 contrail commits on top.
  • Secrets: both contrail secrets stay as plaintext env vars for this release (the current OpenMeet posture). Hardening to sealed-secrets/sops is tracked separately under om-uwis + om-k55c, deferred post-release.
  • Community routes do not mount in any env that doesn't set both keys, so this is additive and safe to merge ahead of prod activation.

PR #582 appended `FROM release AS ingest` at the end of the Dockerfile.
deploy-to-dev.yml's `docker build` runs without `--target`, so Docker
defaults to the LAST stage in the Dockerfile — silently flipped the API
image to the ingest variant (CMD `npm run contrail:ingest`). New API
pods on b518567 crashloop because no HTTP server binds to port 3000 and
the readiness probe gets connection refused.

The `ingest` target was never load-bearing: the Phase E Deployment
manifest overrides `command: ["npm", "run", "contrail:ingest"]` on the
single-replica ingest pod, so the same `release` image works for both
roles. Dropping the extra target restores `release` as the default and
unblocks the dev rollout.
Rebuild the contrail tarballs from the fork's feat/pr30-pr31-integration
branch and add @atmo-dev/contrail-community as a sixth vendored package.
prepare-contrail-deps.sh now builds and packs contrail-community alongside
the other five; package.json pins it as a file: dependency. Verified at
runtime against both sqlite (in-memory) and postgres: contrail + community
schemas apply and createApp wires the spaces/community routes.
Add CredentialKeyMaterial, AuthorityConfig, SpacesConfig, and CommunityIntegration
types to the @atmo-dev/contrail ambient module. Extend ContrailConfig with spaces?
and community?, ContrailOptions with communityIntegration?, and add
generateAuthoritySigningKey() function declaration. Add new
@atmo-dev/contrail-community module declaration with createCommunityIntegration().
Adds two opt-in sections to buildContrailConfig(): spaces.authority
(enabled by CONTRAIL_AUTHORITY_SIGNING_KEY) and community (enabled by
CONTRAIL_COMMUNITY_MASTER_KEY), both absent by default so existing
ingest-only deployments are unaffected.
…ion key

Keep the vendor 'masterKey' config property (contrail-community API);
feed it from CONTRAIL_COMMUNITY_ENCRYPTION_KEY. Avoids 'master' terminology
in our env vars, locals, and docs.
Append a Contrail block to env-example-relational covering
CONTRAIL_DATABASE_URL, CONTRAIL_COMMUNITY_ENCRYPTION_KEY,
CONTRAIL_AUTHORITY_SIGNING_KEY and the optional network vars, noting
that community XRPC routes mount only when both the community block and
spaces.authority are configured.

Wire the same CONTRAIL_* vars into the api service in
docker-compose-dev.yml: CONTRAIL_DATABASE_URL reuses the local Postgres
(schema-isolated), the two keys interpolate from the shell at `up` time
so no key material lands in git. SERVICE_DID is intentionally omitted —
contrail.config defaults it and it is a shared var read elsewhere.
Assert GET /xrpc/net.openmeet.community.getHealth returns 401
(auth-gated, mounted) and explicitly not 404 (unmounted) or 503
(provider down). Gated on the community env vars since the routes only
register when both community and spaces.authority are configured.
- Add CONTRAIL_* (DB URL on the CI Postgres, schema-isolated, plus two
  throwaway CI test keys) to env-example-relational-ci so the
  contrail-xrpc e2e suite runs in CI instead of skipping. Verified end to
  end in the layered CI stack: 5/5 pass, including community.getHealth.
- Delete contrail-community-loader.spec.ts: it only asserted an export is
  a function (covered by the wiring spec + e2e + runtime) and triggered a
  jest vm-modules teardown race when sharing a worker with sibling contrail
  specs (om-mcv0) — a real risk to the maxWorkers=2 unit job.
Address PR #585 review feedback:

- buildContrailConfig now assembles the community block only when BOTH
  CONTRAIL_COMMUNITY_ENCRYPTION_KEY and CONTRAIL_AUTHORITY_SIGNING_KEY are
  set. Community routes are service-auth gated against credentials the
  authority signs/verifies, so an encryption key alone is a half-configured
  mount the vendor integration can't serve. Previously the partial-config
  path (encryption key, no authority) reached createCommunityIntegration at
  startup untested; now it is dropped with a warning. This matches the
  documented "both keys" behavior in env-example-relational.
- Remove unused ContrailProvider.isCommunityReady() (no callers; the /xrpc
  middleware forwards through handle() unconditionally). communityEnabled
  is retained for the init log line.
- Mark the committed CI test keys as DO-NOT-REUSE / inert outside CI.
- Add trailing newline to env-example-relational.

Tests: src/contrail config suite 4/4 (adds a partial-config drop+warn case);
wiring/idempotency unchanged.
@tompscanlan

Copy link
Copy Markdown
Contributor Author

Addressed review feedback in 7f53929:

  • Both-keys invariant now enforced in code (was the main concern). buildContrailConfig assembles the community block only when both CONTRAIL_COMMUNITY_ENCRYPTION_KEY and CONTRAIL_AUTHORITY_SIGNING_KEY are set — matching the documented behavior in env-example-relational. The encryption-key-only path (which previously reached createCommunityIntegration at startup, untested) is now dropped with a warning. Added a config-spec case (should drop community (and warn)...); suite is 4/4.
  • Dropped dead isCommunityReady() — no callers; the /xrpc middleware forwards through handle() unconditionally. Kept communityEnabled for the init log line.
  • CI test keys marked DO NOT REUSE / inert-outside-CI, pointing at the secret-store tracking issues.
  • Trailing newline on env-example-relational.

Still open (not code): the three rebuilt vendor tarballs (contrail, -base, -appview) and the dropped integrity fields in package-lock.json for the file: deps — worth a one-line note in the PR description on why those three rebuilt.

@tompscanlan tompscanlan merged commit 45956d4 into main May 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant