@rizom/ops 0.2.0-alpha.14 → 0.2.0-alpha.141

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.
Files changed (38) hide show
  1. package/README.md +3 -1
  2. package/dist/brains-ops.js +211 -194
  3. package/dist/cert-bootstrap.d.ts +1 -1
  4. package/dist/content-repo.d.ts +1 -0
  5. package/dist/deploy.js +3 -170
  6. package/dist/entries/deploy.d.ts +2 -2
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +211 -194
  9. package/dist/load-registry.d.ts +13 -2
  10. package/dist/observed-status.d.ts +1 -1
  11. package/dist/origin-ca.d.ts +1 -1
  12. package/dist/parse-args.d.ts +2 -0
  13. package/dist/push-secrets.d.ts +2 -9
  14. package/dist/push-target.d.ts +1 -2
  15. package/dist/run-command.d.ts +1 -1
  16. package/dist/run-subprocess.d.ts +1 -6
  17. package/dist/schema.d.ts +54 -18
  18. package/dist/secrets-encrypt.d.ts +4 -4
  19. package/dist/ssh-key-bootstrap.d.ts +1 -26
  20. package/dist/user-add.d.ts +15 -0
  21. package/dist/verify-user.d.ts +19 -0
  22. package/package.json +6 -4
  23. package/templates/rover-pilot/.env.schema +17 -3
  24. package/templates/rover-pilot/.github/workflows/build.yml +13 -5
  25. package/templates/rover-pilot/.github/workflows/deploy.yml +31 -10
  26. package/templates/rover-pilot/.github/workflows/reconcile.yml +16 -2
  27. package/templates/rover-pilot/README.md +3 -1
  28. package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +1 -5
  29. package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +12 -3
  30. package/templates/rover-pilot/deploy/scripts/update-dns.ts +14 -4
  31. package/templates/rover-pilot/docs/onboarding-checklist.md +32 -12
  32. package/templates/rover-pilot/docs/operator-playbook.md +117 -6
  33. package/templates/rover-pilot/docs/user-onboarding.md +81 -346
  34. package/templates/rover-pilot/pilot.yaml +1 -1
  35. package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
  36. package/templates/rover-pilot/deploy/Caddyfile +0 -72
  37. package/templates/rover-pilot/deploy/Dockerfile +0 -38
  38. package/templates/rover-pilot/deploy/kamal/deploy.yml +0 -40
@@ -14,7 +14,6 @@ export interface ResolvedCohort {
14
14
  presetOverride?: PilotPreset;
15
15
  aiApiKeyOverride?: string;
16
16
  gitSyncTokenOverride?: string;
17
- mcpAuthTokenOverride?: string;
18
17
  }
19
18
  export interface ResolvedAnchorProfileSocialLink {
20
19
  platform: "github" | "instagram" | "linkedin" | "email" | "website";
@@ -29,6 +28,16 @@ export interface ResolvedAnchorProfile {
29
28
  story?: string;
30
29
  socialLinks?: ResolvedAnchorProfileSocialLink[];
31
30
  }
31
+ export interface ResolvedSetupDelivery {
32
+ delivery: "email";
33
+ email: string;
34
+ }
35
+ export interface ResolvedAtprotoConfig {
36
+ identifier: string;
37
+ }
38
+ export interface ResolvedPlaybooksConfig {
39
+ onboarding?: boolean | undefined;
40
+ }
32
41
  export interface ResolvedUserIdentity {
33
42
  handle: string;
34
43
  cohort: string;
@@ -41,7 +50,9 @@ export interface ResolvedUserIdentity {
41
50
  discordAnchorUserId?: string;
42
51
  effectiveAiApiKey: string;
43
52
  effectiveGitSyncToken: string;
44
- effectiveMcpAuthToken: string;
53
+ setup?: ResolvedSetupDelivery;
54
+ atproto?: ResolvedAtprotoConfig;
55
+ playbooks?: ResolvedPlaybooksConfig;
45
56
  anchorProfile: ResolvedAnchorProfile;
46
57
  snapshotStatus: SnapshotStatus;
47
58
  }
@@ -1,4 +1,4 @@
1
- import type { FetchLike } from "@brains/utils/origin-ca";
1
+ import type { FetchLike } from "@brains/deploy-support/origin-ca";
2
2
  import type { ObservedUserStatus, ResolvedUserIdentity } from "./load-registry";
3
3
  export interface LookupResult {
4
4
  address: string;
@@ -1 +1 @@
1
- export { createOriginCertificateRequest, generateOriginKeyPair, issueCloudflareOriginCertificate, setCloudflareZoneSslStrict, type CloudflareOriginCaResult, type FetchLike, type OriginCertificateRequest, type OriginKeyPair, } from "@brains/utils/origin-ca";
1
+ export { createOriginCertificateRequest, generateOriginKeyPair, issueCloudflareOriginCertificate, setCloudflareZoneSslStrict, type CloudflareOriginCaResult, type FetchLike, type OriginCertificateRequest, type OriginKeyPair, } from "@brains/deploy-support/origin-ca";
@@ -6,6 +6,8 @@ export interface ParsedArgs {
6
6
  version?: boolean | undefined;
7
7
  dryRun?: boolean | undefined;
8
8
  pushTo?: string | undefined;
9
+ cohort?: string | undefined;
10
+ anchorId?: string | undefined;
9
11
  };
10
12
  }
11
13
  export declare function parseArgs(argv: string[]): ParsedArgs;
@@ -1,9 +1,2 @@
1
- import { normalizePushTarget, type PushTarget } from "./push-target";
2
- import { type RunCommand } from "./run-subprocess";
3
- export type SecretPair = readonly [name: string, value: string];
4
- export interface PushSecretsOptions {
5
- runCommand?: RunCommand | undefined;
6
- logger?: ((message: string) => void) | undefined;
7
- }
8
- export declare function pushSecretsToBackend(_target: PushTarget, secrets: readonly SecretPair[], options?: PushSecretsOptions): Promise<void>;
9
- export { normalizePushTarget };
1
+ export { pushSecretsToBackend, type PushSecretsOptions, type SecretPair, } from "@brains/deploy-support/push-secrets";
2
+ export { normalizePushTarget } from "@brains/deploy-support/push-target";
@@ -1,2 +1 @@
1
- export type PushTarget = "gh";
2
- export declare function normalizePushTarget(value?: string): PushTarget | undefined;
1
+ export { normalizePushTarget, type PushTarget, } from "@brains/deploy-support/push-target";
@@ -1,4 +1,4 @@
1
- import type { FetchLike } from "@brains/utils/origin-ca";
1
+ import type { FetchLike } from "@brains/deploy-support/origin-ca";
2
2
  import type { LoadPilotRegistryOptions } from "./load-registry";
3
3
  import { type LookupHost } from "./observed-status";
4
4
  import type { ParsedArgs } from "./parse-args";
@@ -1,6 +1 @@
1
- export type RunCommand = (command: string, args: string[], options?: {
2
- stdin?: string;
3
- env?: NodeJS.ProcessEnv;
4
- cwd?: string;
5
- }) => Promise<void>;
6
- export declare const runSubprocess: RunCommand;
1
+ export { runSubprocess, type RunCommand, } from "@brains/deploy-support/run-subprocess";
package/dist/schema.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { z } from "@brains/utils";
1
+ import { z } from "@brains/utils/zod";
2
2
  export declare const presetSchema: z.ZodEnum<["core", "default", "pro"]>;
3
3
  export declare const exactVersionSchema: z.ZodString;
4
4
  export declare const handleSchema: z.ZodString;
@@ -14,7 +14,7 @@ export declare const pilotSchema: z.ZodObject<{
14
14
  preset: z.ZodEnum<["core", "default", "pro"]>;
15
15
  aiApiKey: z.ZodString;
16
16
  gitSyncToken: z.ZodString;
17
- mcpAuthToken: z.ZodString;
17
+ contentRepoAdminToken: z.ZodString;
18
18
  agePublicKey: z.ZodString;
19
19
  }, "strict", z.ZodTypeAny, {
20
20
  agePublicKey: string;
@@ -27,7 +27,7 @@ export declare const pilotSchema: z.ZodObject<{
27
27
  preset: "default" | "core" | "pro";
28
28
  aiApiKey: string;
29
29
  gitSyncToken: string;
30
- mcpAuthToken: string;
30
+ contentRepoAdminToken: string;
31
31
  }, {
32
32
  agePublicKey: string;
33
33
  schemaVersion: 1;
@@ -39,7 +39,7 @@ export declare const pilotSchema: z.ZodObject<{
39
39
  preset: "default" | "core" | "pro";
40
40
  aiApiKey: string;
41
41
  gitSyncToken: string;
42
- mcpAuthToken: string;
42
+ contentRepoAdminToken: string;
43
43
  }>;
44
44
  export declare const userSchema: z.ZodObject<{
45
45
  handle: z.ZodString;
@@ -55,7 +55,30 @@ export declare const userSchema: z.ZodObject<{
55
55
  }>;
56
56
  aiApiKeyOverride: z.ZodOptional<z.ZodString>;
57
57
  gitSyncTokenOverride: z.ZodOptional<z.ZodString>;
58
- mcpAuthTokenOverride: z.ZodOptional<z.ZodString>;
58
+ setup: z.ZodOptional<z.ZodObject<{
59
+ delivery: z.ZodLiteral<"email">;
60
+ email: z.ZodString;
61
+ }, "strict", z.ZodTypeAny, {
62
+ email: string;
63
+ delivery: "email";
64
+ }, {
65
+ email: string;
66
+ delivery: "email";
67
+ }>>;
68
+ atproto: z.ZodOptional<z.ZodObject<{
69
+ identifier: z.ZodString;
70
+ }, "strict", z.ZodTypeAny, {
71
+ identifier: string;
72
+ }, {
73
+ identifier: string;
74
+ }>>;
75
+ playbooks: z.ZodOptional<z.ZodObject<{
76
+ onboarding: z.ZodOptional<z.ZodBoolean>;
77
+ }, "strict", z.ZodTypeAny, {
78
+ onboarding?: boolean | undefined;
79
+ }, {
80
+ onboarding?: boolean | undefined;
81
+ }>>;
59
82
  anchorProfile: z.ZodOptional<z.ZodObject<{
60
83
  name: z.ZodOptional<z.ZodString>;
61
84
  description: z.ZodOptional<z.ZodString>;
@@ -67,12 +90,12 @@ export declare const userSchema: z.ZodObject<{
67
90
  url: z.ZodString;
68
91
  label: z.ZodOptional<z.ZodString>;
69
92
  }, "strict", z.ZodTypeAny, {
70
- url: string;
71
93
  platform: "github" | "instagram" | "linkedin" | "email" | "website";
94
+ url: string;
72
95
  label?: string | undefined;
73
96
  }, {
74
- url: string;
75
97
  platform: "github" | "instagram" | "linkedin" | "email" | "website";
98
+ url: string;
76
99
  label?: string | undefined;
77
100
  }>, "many">>;
78
101
  }, "strict", z.ZodTypeAny, {
@@ -82,8 +105,8 @@ export declare const userSchema: z.ZodObject<{
82
105
  description?: string | undefined;
83
106
  story?: string | undefined;
84
107
  socialLinks?: {
85
- url: string;
86
108
  platform: "github" | "instagram" | "linkedin" | "email" | "website";
109
+ url: string;
87
110
  label?: string | undefined;
88
111
  }[] | undefined;
89
112
  }, {
@@ -93,8 +116,8 @@ export declare const userSchema: z.ZodObject<{
93
116
  description?: string | undefined;
94
117
  story?: string | undefined;
95
118
  socialLinks?: {
96
- url: string;
97
119
  platform: "github" | "instagram" | "linkedin" | "email" | "website";
120
+ url: string;
98
121
  label?: string | undefined;
99
122
  }[] | undefined;
100
123
  }>>;
@@ -106,7 +129,16 @@ export declare const userSchema: z.ZodObject<{
106
129
  };
107
130
  aiApiKeyOverride?: string | undefined;
108
131
  gitSyncTokenOverride?: string | undefined;
109
- mcpAuthTokenOverride?: string | undefined;
132
+ setup?: {
133
+ email: string;
134
+ delivery: "email";
135
+ } | undefined;
136
+ atproto?: {
137
+ identifier: string;
138
+ } | undefined;
139
+ playbooks?: {
140
+ onboarding?: boolean | undefined;
141
+ } | undefined;
110
142
  anchorProfile?: {
111
143
  name?: string | undefined;
112
144
  email?: string | undefined;
@@ -114,8 +146,8 @@ export declare const userSchema: z.ZodObject<{
114
146
  description?: string | undefined;
115
147
  story?: string | undefined;
116
148
  socialLinks?: {
117
- url: string;
118
149
  platform: "github" | "instagram" | "linkedin" | "email" | "website";
150
+ url: string;
119
151
  label?: string | undefined;
120
152
  }[] | undefined;
121
153
  } | undefined;
@@ -127,7 +159,16 @@ export declare const userSchema: z.ZodObject<{
127
159
  };
128
160
  aiApiKeyOverride?: string | undefined;
129
161
  gitSyncTokenOverride?: string | undefined;
130
- mcpAuthTokenOverride?: string | undefined;
162
+ setup?: {
163
+ email: string;
164
+ delivery: "email";
165
+ } | undefined;
166
+ atproto?: {
167
+ identifier: string;
168
+ } | undefined;
169
+ playbooks?: {
170
+ onboarding?: boolean | undefined;
171
+ } | undefined;
131
172
  anchorProfile?: {
132
173
  name?: string | undefined;
133
174
  email?: string | undefined;
@@ -135,8 +176,8 @@ export declare const userSchema: z.ZodObject<{
135
176
  description?: string | undefined;
136
177
  story?: string | undefined;
137
178
  socialLinks?: {
138
- url: string;
139
179
  platform: "github" | "instagram" | "linkedin" | "email" | "website";
180
+ url: string;
140
181
  label?: string | undefined;
141
182
  }[] | undefined;
142
183
  } | undefined;
@@ -147,33 +188,28 @@ export declare const cohortSchema: z.ZodEffects<z.ZodObject<{
147
188
  presetOverride: z.ZodOptional<z.ZodEnum<["core", "default", "pro"]>>;
148
189
  aiApiKeyOverride: z.ZodOptional<z.ZodString>;
149
190
  gitSyncTokenOverride: z.ZodOptional<z.ZodString>;
150
- mcpAuthTokenOverride: z.ZodOptional<z.ZodString>;
151
191
  }, "strict", z.ZodTypeAny, {
152
192
  members: string[];
153
193
  aiApiKeyOverride?: string | undefined;
154
194
  gitSyncTokenOverride?: string | undefined;
155
- mcpAuthTokenOverride?: string | undefined;
156
195
  brainVersionOverride?: string | undefined;
157
196
  presetOverride?: "default" | "core" | "pro" | undefined;
158
197
  }, {
159
198
  members: string[];
160
199
  aiApiKeyOverride?: string | undefined;
161
200
  gitSyncTokenOverride?: string | undefined;
162
- mcpAuthTokenOverride?: string | undefined;
163
201
  brainVersionOverride?: string | undefined;
164
202
  presetOverride?: "default" | "core" | "pro" | undefined;
165
203
  }>, {
166
204
  members: string[];
167
205
  aiApiKeyOverride?: string | undefined;
168
206
  gitSyncTokenOverride?: string | undefined;
169
- mcpAuthTokenOverride?: string | undefined;
170
207
  brainVersionOverride?: string | undefined;
171
208
  presetOverride?: "default" | "core" | "pro" | undefined;
172
209
  }, {
173
210
  members: string[];
174
211
  aiApiKeyOverride?: string | undefined;
175
212
  gitSyncTokenOverride?: string | undefined;
176
- mcpAuthTokenOverride?: string | undefined;
177
213
  brainVersionOverride?: string | undefined;
178
214
  presetOverride?: "default" | "core" | "pro" | undefined;
179
215
  }>;
@@ -1,19 +1,19 @@
1
- import { z } from "@brains/utils";
1
+ import { z } from "@brains/utils/zod";
2
2
  declare const encryptedUserSecretsSchema: z.ZodObject<{
3
3
  gitSyncToken: z.ZodOptional<z.ZodString>;
4
- mcpAuthToken: z.ZodOptional<z.ZodString>;
5
4
  discordBotToken: z.ZodOptional<z.ZodString>;
6
5
  aiApiKey: z.ZodOptional<z.ZodString>;
6
+ atprotoAppPassword: z.ZodOptional<z.ZodString>;
7
7
  }, "strict", z.ZodTypeAny, {
8
8
  aiApiKey?: string | undefined;
9
9
  gitSyncToken?: string | undefined;
10
- mcpAuthToken?: string | undefined;
11
10
  discordBotToken?: string | undefined;
11
+ atprotoAppPassword?: string | undefined;
12
12
  }, {
13
13
  aiApiKey?: string | undefined;
14
14
  gitSyncToken?: string | undefined;
15
- mcpAuthToken?: string | undefined;
16
15
  discordBotToken?: string | undefined;
16
+ atprotoAppPassword?: string | undefined;
17
17
  }>;
18
18
  export type EncryptedUserSecrets = z.infer<typeof encryptedUserSecretsSchema>;
19
19
  export interface SecretsEncryptOptions {
@@ -1,26 +1 @@
1
- import { type FetchLike } from "@brains/utils/origin-ca";
2
- import { type RunCommand } from "./run-subprocess";
3
- export interface SshKeyBootstrapOptions {
4
- env?: NodeJS.ProcessEnv | undefined;
5
- fetchImpl?: FetchLike | undefined;
6
- logger?: (message: string) => void;
7
- pushTo?: string | undefined;
8
- runCommand?: RunCommand | undefined;
9
- sshKeygen?: SshKeygen | undefined;
10
- }
11
- export interface SshKeyBootstrapResult {
12
- createdHetznerKey: boolean;
13
- createdLocalKey: boolean;
14
- privateKeyPath: string;
15
- publicKeyPath: string;
16
- sshKeyName: string;
17
- }
18
- export interface SshKeygen {
19
- createEd25519KeyPair: (privateKeyPath: string, comment: string) => void;
20
- derivePublicKey: (privateKeyPath: string) => string;
21
- }
22
- export declare function runPilotSshKeyBootstrap(rootDir: string, options?: SshKeyBootstrapOptions): Promise<{
23
- success: boolean;
24
- message?: string;
25
- }>;
26
- export declare function bootstrapPilotSshKey(rootDir: string, options?: SshKeyBootstrapOptions): Promise<SshKeyBootstrapResult>;
1
+ export { bootstrapSshKey as bootstrapPilotSshKey, runSshKeyBootstrap as runPilotSshKeyBootstrap, type SshKeyBootstrapOptions, type SshKeyBootstrapResult, type SshKeygen, } from "@brains/deploy-support/ssh-key-bootstrap";
@@ -0,0 +1,15 @@
1
+ export interface AddPilotUserOptions {
2
+ cohort: string;
3
+ anchorId?: string | undefined;
4
+ }
5
+ export interface AddPilotUserResult {
6
+ handle: string;
7
+ cohort: string;
8
+ userPath: string;
9
+ secretsTemplatePath: string;
10
+ cohortPath: string;
11
+ createdUser: boolean;
12
+ createdSecretsTemplate: boolean;
13
+ addedToCohort: boolean;
14
+ }
15
+ export declare function addPilotUser(rootDir: string, handle: string, options: AddPilotUserOptions): Promise<AddPilotUserResult>;
@@ -0,0 +1,19 @@
1
+ import type { FetchLike } from "@brains/deploy-support/origin-ca";
2
+ import { type ResolvedUser } from "./load-registry";
3
+ export interface VerifyPilotUserOptions {
4
+ fetchImpl?: FetchLike;
5
+ logger?: (message: string) => void;
6
+ }
7
+ export interface FailedCheck {
8
+ name: string;
9
+ message: string;
10
+ }
11
+ export interface VerifyPilotUserResult {
12
+ handle: string;
13
+ preset: ResolvedUser["preset"];
14
+ domain: string;
15
+ contentRepo: string;
16
+ checks: string[];
17
+ failedChecks: FailedCheck[];
18
+ }
19
+ export declare function verifyPilotUser(rootDir: string, handle: string, options?: VerifyPilotUserOptions): Promise<VerifyPilotUserResult>;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.2.0-alpha.14",
7
+ "version": "0.2.0-alpha.141",
8
8
  "type": "module",
9
9
  "exports": {
10
10
  ".": {
@@ -30,14 +30,16 @@
30
30
  "build": "bun scripts/build.ts",
31
31
  "prepublishOnly": "bun scripts/build.ts",
32
32
  "typecheck": "tsc --noEmit",
33
- "lint": "eslint . --ext .ts",
34
- "lint:fix": "eslint . --ext .ts --fix",
35
- "test": "bun test --timeout 20000"
33
+ "lint": "eslint . --ext .ts,.tsx --max-warnings 0",
34
+ "lint:fix": "eslint . --ext .ts,.tsx --max-warnings 0 --fix",
35
+ "test": "bun run build && bun test --timeout 20000",
36
+ "test:smoke": "bun run build && RUN_SMOKE_TESTS=1 bun test --timeout 60000"
36
37
  },
37
38
  "dependencies": {
38
39
  "age-encryption": "^0.3.0"
39
40
  },
40
41
  "devDependencies": {
42
+ "@brains/deploy-support": "workspace:*",
41
43
  "@brains/eslint-config": "workspace:*",
42
44
  "@brains/typescript-config": "workspace:*",
43
45
  "@brains/utils": "workspace:*",
@@ -14,16 +14,30 @@ AI_API_KEY=
14
14
  # @required @sensitive
15
15
  GIT_SYNC_TOKEN=
16
16
 
17
- # MCP interface
18
- # Comes from the decrypted users/<handle>.secrets.yaml.age file.
17
+ # Content repo administration
18
+ # Local/operator secret only. Used by brains-ops to create missing GitHub repos;
19
+ # do not deploy it into Rover runtime config.
19
20
  # @required @sensitive
20
- MCP_AUTH_TOKEN=
21
+ CONTENT_REPO_ADMIN_TOKEN=
21
22
 
22
23
  # Discord (optional, per-user)
23
24
  # Comes from the decrypted users/<handle>.secrets.yaml.age file when enabled.
24
25
  # @sensitive
25
26
  DISCORD_BOT_TOKEN=
26
27
 
28
+ # AT Protocol publishing/discovery (optional, per-user)
29
+ # Comes from the decrypted users/<handle>.secrets.yaml.age file when configured.
30
+ # @sensitive
31
+ ATPROTO_APP_PASSWORD=
32
+
33
+ # Setup email delivery (optional, shared)
34
+ # Used when a user file declares setup.delivery: email.
35
+ # @sensitive
36
+ SETUP_EMAIL_API_KEY=
37
+
38
+ # @sensitive
39
+ SETUP_EMAIL_FROM=
40
+
27
41
  # ---- deploy/provision vars ----
28
42
 
29
43
  # @required @sensitive
@@ -2,6 +2,11 @@ name: Build
2
2
 
3
3
  on:
4
4
  workflow_dispatch:
5
+ inputs:
6
+ brain_version:
7
+ description: Brain version to build; defaults to pilot.yaml brainVersion
8
+ required: false
9
+ type: string
5
10
  push:
6
11
  branches: ["main"]
7
12
  paths:
@@ -23,7 +28,10 @@ jobs:
23
28
 
24
29
  - name: Extract image metadata inputs
25
30
  run: |
26
- BRAIN_VERSION="$(grep '^brainVersion:' pilot.yaml | sed 's/^brainVersion:[[:space:]]*//' | tr -d '"' | tr -d "'")"
31
+ BRAIN_VERSION="${{ inputs.brain_version || '' }}"
32
+ if [ -z "$BRAIN_VERSION" ]; then
33
+ BRAIN_VERSION="$(grep '^brainVersion:' pilot.yaml | sed 's/^brainVersion:[[:space:]]*//' | tr -d '"' | tr -d "'")"
34
+ fi
27
35
  if [ -z "$BRAIN_VERSION" ]; then
28
36
  echo "Missing brainVersion in pilot.yaml" >&2
29
37
  exit 1
@@ -32,25 +40,25 @@ jobs:
32
40
  echo "IMAGE_REPOSITORY=ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}" >> "$GITHUB_ENV"
33
41
 
34
42
  - name: Set up Docker Buildx
35
- uses: docker/setup-buildx-action@v3
43
+ uses: docker/setup-buildx-action@v4
36
44
 
37
45
  - name: Extract image metadata
38
46
  id: meta
39
- uses: docker/metadata-action@v5
47
+ uses: docker/metadata-action@v6
40
48
  with:
41
49
  images: ${{ env.IMAGE_REPOSITORY }}
42
50
  tags: |
43
51
  type=raw,value=brain-${{ env.BRAIN_VERSION }}
44
52
 
45
53
  - name: Log in to GHCR
46
- uses: docker/login-action@v3
54
+ uses: docker/login-action@v4
47
55
  with:
48
56
  registry: ghcr.io
49
57
  username: ${{ github.actor }}
50
58
  password: ${{ secrets.GITHUB_TOKEN }}
51
59
 
52
60
  - name: Build and push image
53
- uses: docker/build-push-action@v6
61
+ uses: docker/build-push-action@v7
54
62
  with:
55
63
  context: .
56
64
  file: deploy/Dockerfile
@@ -16,6 +16,10 @@ on:
16
16
  - users/*.secrets.yaml.age
17
17
  - deploy/**
18
18
  - .github/workflows/deploy.yml
19
+ workflow_run:
20
+ workflows: [Reconcile]
21
+ types: [completed]
22
+ branches: [main]
19
23
 
20
24
  permissions:
21
25
  contents: write
@@ -23,13 +27,16 @@ permissions:
23
27
 
24
28
  jobs:
25
29
  resolve_handles:
30
+ if: >
31
+ github.event_name != 'workflow_run' ||
32
+ github.event.workflow_run.conclusion == 'success'
26
33
  runs-on: ubuntu-latest
27
34
  outputs:
28
35
  handles_json: ${{ steps.resolve.outputs.handles_json }}
29
36
  steps:
30
37
  - uses: actions/checkout@v5
31
38
  with:
32
- ref: ${{ github.sha }}
39
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
33
40
  fetch-depth: 0
34
41
 
35
42
  - uses: oven-sh/setup-bun@v2
@@ -41,7 +48,7 @@ jobs:
41
48
  id: resolve
42
49
  env:
43
50
  HANDLE_INPUT: ${{ inputs.handle || '' }}
44
- BEFORE_SHA: ${{ github.event.before || '' }}
51
+ BEFORE_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.before || '' }}
45
52
  run: bun deploy/scripts/resolve-deploy-handles.ts
46
53
 
47
54
  no_changes:
@@ -68,7 +75,7 @@ jobs:
68
75
  steps:
69
76
  - uses: actions/checkout@v5
70
77
  with:
71
- ref: ${{ github.sha }}
78
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
72
79
 
73
80
  - uses: oven-sh/setup-bun@v2
74
81
 
@@ -96,7 +103,8 @@ jobs:
96
103
  env:
97
104
  SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
98
105
  SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
99
- SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
106
+ SETUP_EMAIL_API_KEY: ${{ secrets.SETUP_EMAIL_API_KEY }}
107
+ SETUP_EMAIL_FROM: ${{ secrets.SETUP_EMAIL_FROM }}
100
108
  HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
101
109
  HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
102
110
  HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
@@ -110,7 +118,6 @@ jobs:
110
118
  run: |
111
119
  export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
112
120
  export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
113
- export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
114
121
  bun deploy/scripts/validate-secrets.ts
115
122
 
116
123
  - name: Seed content repo
@@ -122,7 +129,7 @@ jobs:
122
129
  bun deploy/scripts/sync-content-repo.ts
123
130
 
124
131
  - name: Log in to GHCR
125
- uses: docker/login-action@v3
132
+ uses: docker/login-action@v4
126
133
  with:
127
134
  registry: ghcr.io
128
135
  username: ${{ github.actor }}
@@ -168,14 +175,14 @@ jobs:
168
175
  env:
169
176
  SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
170
177
  SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
171
- SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
178
+ SETUP_EMAIL_API_KEY: ${{ secrets.SETUP_EMAIL_API_KEY }}
179
+ SETUP_EMAIL_FROM: ${{ secrets.SETUP_EMAIL_FROM }}
172
180
  KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
173
181
  CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
174
182
  PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
175
183
  run: |
176
184
  export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
177
185
  export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
178
- export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
179
186
  bun deploy/scripts/write-kamal-secrets.ts
180
187
 
181
188
  - name: Provision server
@@ -271,7 +278,7 @@ jobs:
271
278
  steps:
272
279
  - uses: actions/checkout@v5
273
280
  with:
274
- ref: ${{ github.sha }}
281
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
275
282
 
276
283
  - uses: oven-sh/setup-bun@v2
277
284
 
@@ -293,8 +300,22 @@ jobs:
293
300
  if git diff --quiet -- users views; then
294
301
  exit 0
295
302
  fi
303
+
304
+ git fetch origin "${{ github.ref_name }}"
305
+ if git diff --quiet "origin/${{ github.ref_name }}" -- users views; then
306
+ exit 0
307
+ fi
308
+
309
+ patch_file="$(mktemp)"
310
+ git diff --binary -- users views > "$patch_file"
311
+ git reset --hard "origin/${{ github.ref_name }}"
312
+ git apply --3way --index "$patch_file"
313
+
314
+ if git diff --cached --quiet -- users views; then
315
+ exit 0
316
+ fi
317
+
296
318
  git config user.name "github-actions[bot]"
297
319
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
298
- git add users views
299
320
  git commit -m "chore(ops): reconcile generated config"
300
321
  git push origin HEAD:${{ github.ref_name }}
@@ -38,8 +38,22 @@ jobs:
38
38
  if git diff --quiet -- views users; then
39
39
  exit 0
40
40
  fi
41
+
42
+ git fetch origin "${{ github.ref_name }}"
43
+ if git diff --quiet "origin/${{ github.ref_name }}" -- views users; then
44
+ exit 0
45
+ fi
46
+
47
+ patch_file="$(mktemp)"
48
+ git diff --binary -- views users > "$patch_file"
49
+ git reset --hard "origin/${{ github.ref_name }}"
50
+ git apply --3way --index "$patch_file"
51
+
52
+ if git diff --cached --quiet -- views users; then
53
+ exit 0
54
+ fi
55
+
41
56
  git config user.name "github-actions[bot]"
42
57
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
43
- git add views users
44
58
  git commit -m "chore(ops): reconcile pilot outputs"
45
- git push
59
+ git push origin HEAD:${{ github.ref_name }}
@@ -29,6 +29,7 @@ The repo also checks in its deploy contract:
29
29
  - `.github/workflows/*`
30
30
 
31
31
  `.env.schema` is the single source of truth for required and sensitive deploy vars.
32
+ Use separate GitHub tokens: `CONTENT_REPO_ADMIN_TOKEN` for operator-side content repo creation/checks, and `GIT_SYNC_TOKEN` for runtime directory-sync git access.
32
33
  The shared pilot image tag is `brain-${brainVersion}` end to end.
33
34
  When `pilot.yaml.brainVersion` changes and you push, CI rebuilds the shared tag, refreshes generated user env files, and redeploys affected users.
34
35
  When a push changes only deploy contract files, CI prints `No affected user configs; skipping deploy.` and stops before Kamal.
@@ -37,7 +38,8 @@ When a push changes only deploy contract files, CI prints `No affected user conf
37
38
 
38
39
  - `brains-ops init <repo>`
39
40
  - `brains-ops render <repo>` — regenerates `views/users.md` with live DNS, `/health`, and unauthenticated `/mcp` status checks
40
- - `brains-ops onboard <repo> <handle>`
41
+ - `brains-ops user:add <repo> <handle> --cohort <cohort>` — scaffolds a user file, per-user secrets template, and cohort membership
42
+ - `brains-ops onboard <repo> <handle>` — creates/seeds the user's content repo with separate admin and sync tokens
41
43
  - `brains-ops age-key:bootstrap <repo>`
42
44
  - `brains-ops ssh-key:bootstrap <repo>`
43
45
  - `brains-ops cert:bootstrap <repo>`