@rizom/ops 0.2.0-alpha.190 → 0.2.0-alpha.192

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rizom/ops",
3
- "version": "0.2.0-alpha.190",
3
+ "version": "0.2.0-alpha.192",
4
4
  "description": "Operator CLI for managing private brain fleet registry repos",
5
5
  "keywords": [
6
6
  "brains",
@@ -211,11 +211,16 @@ jobs:
211
211
 
212
212
  - name: Update Cloudflare DNS
213
213
  env:
214
+ CF_ZONE_ID: ${{ steps.user_config.outputs.cloudflare_zone_id }}
214
215
  BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
216
+ WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
215
217
  PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
216
218
  SERVER_IP: ${{ steps.provision.outputs.server_ip }}
217
219
  run: |
218
220
  bun deploy/scripts/update-dns.ts
221
+ if [ -n "$WWW_DOMAIN" ]; then
222
+ BRAIN_DOMAIN="$WWW_DOMAIN" bun deploy/scripts/update-dns.ts
223
+ fi
219
224
  BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
220
225
 
221
226
  - name: Validate SSH key
@@ -243,6 +248,7 @@ jobs:
243
248
  IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
244
249
  REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
245
250
  BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
251
+ WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
246
252
  PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
247
253
  BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
248
254
  run: kamal setup --skip-push -c deploy/kamal/deploy.yml
@@ -251,9 +257,13 @@ jobs:
251
257
  env:
252
258
  SERVER_IP: ${{ steps.provision.outputs.server_ip }}
253
259
  BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
260
+ WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
254
261
  PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
255
262
  run: |
256
263
  curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
264
+ if [ -n "$WWW_DOMAIN" ]; then
265
+ curl -I -k --max-time 20 --resolve "$WWW_DOMAIN:443:$SERVER_IP" "https://$WWW_DOMAIN/"
266
+ fi
257
267
  curl -I -k --max-time 20 --resolve "$PREVIEW_DOMAIN:443:$SERVER_IP" "https://$PREVIEW_DOMAIN/"
258
268
 
259
269
  - name: Upload generated config
@@ -28,9 +28,19 @@ writeSecretGitHubEnv("DISCORD_BOT_TOKEN", secrets["discordBotToken"]);
28
28
  // Per-user AT Protocol publishing credential (optional; from the user's
29
29
  // encrypted secrets file). The scaffold wires this so a pilot can publish its
30
30
  // brain's agent card to its PDS; a deployment that doesn't publish simply
31
- // leaves it unset. TLS material (CERTIFICATE_PEM/PRIVATE_KEY_PEM) is handled by
32
- // the kamal proxy block via shared env, not here.
31
+ // leaves it unset.
33
32
  writeSecretGitHubEnv("ATPROTO_APP_PASSWORD", secrets["atprotoAppPassword"]);
33
+ // Per-user custom-domain TLS overrides the shared fleet certificate when a
34
+ // complete certificate/key pair is present in the encrypted user secrets.
35
+ const certificatePem = decodeEscapedSecret(secrets["certificatePem"]);
36
+ const privateKeyPem = decodeEscapedSecret(secrets["privateKeyPem"]);
37
+ if (Boolean(certificatePem) !== Boolean(privateKeyPem)) {
38
+ throw new Error(
39
+ "Custom-domain TLS secrets require both certificatePem and privateKeyPem",
40
+ );
41
+ }
42
+ writeSecretGitHubEnv("CERTIFICATE_PEM", certificatePem);
43
+ writeSecretGitHubEnv("PRIVATE_KEY_PEM", privateKeyPem);
34
44
 
35
45
  writeGitHubOutput(
36
46
  "shared_ai_api_key_secret_name",
@@ -98,6 +108,10 @@ function parseFlatYaml(contents: string): Record<string, string> {
98
108
  return result;
99
109
  }
100
110
 
111
+ function decodeEscapedSecret(value: string | undefined): string | undefined {
112
+ return value?.replace(/\\n/g, "\n");
113
+ }
114
+
101
115
  function requireFlatValue(
102
116
  values: Record<string, string>,
103
117
  key: string,
@@ -25,14 +25,23 @@ if (!brainDomain) {
25
25
  throw new Error(`Missing domain in ${brainYamlPath}`);
26
26
  }
27
27
 
28
- const zone =
29
- brainDomain.startsWith(`${handle}.`) && brainDomain.length > handle.length + 1
30
- ? brainDomain.slice(handle.length + 1)
31
- : "";
32
- if (!zone) {
33
- throw new Error(`Could not derive preview domain from ${brainDomain}`);
28
+ const registry = await loadPilotRegistry(process.cwd());
29
+ const user = registry.users.find((entry) => entry.handle === handle);
30
+ if (!user) {
31
+ throw new Error(`Unknown user handle: ${handle}`);
34
32
  }
35
- const previewDomain = `${handle}-preview.${zone}`;
33
+ const previewDomain = resolvePreviewDomain(
34
+ handle,
35
+ brainDomain,
36
+ registry.pilot.domainSuffix,
37
+ );
38
+ const wwwDomain = isFleetDomain(
39
+ handle,
40
+ brainDomain,
41
+ registry.pilot.domainSuffix,
42
+ )
43
+ ? ""
44
+ : `www.${brainDomain}`;
36
45
 
37
46
  const brainVersion = envEntries["BRAIN_VERSION"] ?? "";
38
47
 
@@ -40,9 +49,7 @@ const brainVersion = envEntries["BRAIN_VERSION"] ?? "";
40
49
  // `brain-{version}` for a default instance, or its own `brain-{version}-sites-
41
50
  // {hash}` when it declares a siteOverride. Resolved through the same helper the
42
51
  // build uses so the tag we wait for and run matches exactly what was pushed.
43
- const registry = await loadPilotRegistry(process.cwd());
44
- const user = registry.users.find((entry) => entry.handle === handle);
45
- const sitePackages = sitePackagesFor(user?.siteOverride);
52
+ const sitePackages = sitePackagesFor(user.siteOverride);
46
53
  const imageTag = siteImageTag(brainVersion, sitePackages);
47
54
 
48
55
  const outputs: Record<string, string> = {
@@ -50,6 +57,8 @@ const outputs: Record<string, string> = {
50
57
  content_repo: envEntries["CONTENT_REPO"] ?? "",
51
58
  brain_domain: brainDomain,
52
59
  preview_domain: previewDomain,
60
+ www_domain: wwwDomain,
61
+ cloudflare_zone_id: user.cloudflareZoneId ?? process.env["CF_ZONE_ID"] ?? "",
53
62
  brain_yaml_path: brainYamlPath,
54
63
  instance_name: `rover-${handle}`,
55
64
  image_repository: `ghcr.io/${repository}`,
@@ -67,3 +76,28 @@ for (const key of required) {
67
76
  for (const [key, value] of Object.entries(outputs)) {
68
77
  writeGitHubOutput(key, value);
69
78
  }
79
+
80
+ function resolvePreviewDomain(
81
+ userHandle: string,
82
+ domain: string,
83
+ pilotDomainSuffix: string,
84
+ ): string {
85
+ if (!isFleetDomain(userHandle, domain, pilotDomainSuffix)) {
86
+ return `preview.${domain}`;
87
+ }
88
+
89
+ const fleetZone = pilotDomainSuffix.replace(/^\./, "");
90
+ if (!fleetZone) {
91
+ throw new Error(`Could not derive preview domain from ${domain}`);
92
+ }
93
+
94
+ return `${userHandle}-preview.${fleetZone}`;
95
+ }
96
+
97
+ function isFleetDomain(
98
+ userHandle: string,
99
+ domain: string,
100
+ pilotDomainSuffix: string,
101
+ ): boolean {
102
+ return domain === `${userHandle}${pilotDomainSuffix}`;
103
+ }