@rizom/ops 0.2.0-alpha.6 → 0.2.0-alpha.61

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 (45) hide show
  1. package/README.md +6 -3
  2. package/dist/age-key-bootstrap.d.ts +17 -0
  3. package/dist/brains-ops.js +305 -149
  4. package/dist/cert-bootstrap.d.ts +2 -2
  5. package/dist/content-repo.d.ts +13 -0
  6. package/dist/default-user-runner.d.ts +1 -1
  7. package/dist/deploy.js +24 -24
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.js +305 -149
  10. package/dist/load-registry.d.ts +19 -3
  11. package/dist/onboard-user.d.ts +2 -2
  12. package/dist/parse-args.d.ts +2 -0
  13. package/dist/push-secrets.d.ts +1 -1
  14. package/dist/reconcile-all.d.ts +2 -2
  15. package/dist/reconcile-cohort.d.ts +2 -2
  16. package/dist/reconcile-lib.d.ts +4 -2
  17. package/dist/run-command.d.ts +0 -1
  18. package/dist/run-subprocess.d.ts +1 -0
  19. package/dist/schema.d.ts +100 -0
  20. package/dist/secrets-encrypt.d.ts +32 -0
  21. package/dist/secrets-push.d.ts +1 -1
  22. package/dist/user-add.d.ts +15 -0
  23. package/dist/user-runner.d.ts +5 -0
  24. package/package.json +7 -3
  25. package/templates/rover-pilot/.env.schema +11 -0
  26. package/templates/rover-pilot/.github/workflows/build.yml +1 -0
  27. package/templates/rover-pilot/.github/workflows/deploy.yml +74 -19
  28. package/templates/rover-pilot/.github/workflows/reconcile.yml +16 -2
  29. package/templates/rover-pilot/README.md +6 -3
  30. package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +83 -0
  31. package/templates/rover-pilot/deploy/scripts/provision-server.ts +1 -1
  32. package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +15 -4
  33. package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +12 -12
  34. package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +179 -0
  35. package/templates/rover-pilot/deploy/scripts/update-dns.ts +14 -4
  36. package/templates/rover-pilot/docs/onboarding-checklist.md +28 -11
  37. package/templates/rover-pilot/docs/operator-playbook.md +43 -5
  38. package/templates/rover-pilot/docs/user-onboarding.md +505 -0
  39. package/templates/rover-pilot/package.json +3 -0
  40. package/templates/rover-pilot/pilot.yaml +4 -0
  41. package/templates/rover-pilot/users/alice.yaml +5 -1
  42. package/dist/user-secret-names.d.ts +0 -6
  43. package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
  44. package/templates/rover-pilot/deploy/Dockerfile +0 -15
  45. package/templates/rover-pilot/deploy/kamal/deploy.yml +0 -39
@@ -13,6 +13,21 @@ export interface ResolvedCohort {
13
13
  brainVersionOverride?: string;
14
14
  presetOverride?: PilotPreset;
15
15
  aiApiKeyOverride?: string;
16
+ gitSyncTokenOverride?: string;
17
+ mcpAuthTokenOverride?: string;
18
+ }
19
+ export interface ResolvedAnchorProfileSocialLink {
20
+ platform: "github" | "instagram" | "linkedin" | "email" | "website";
21
+ url: string;
22
+ label?: string;
23
+ }
24
+ export interface ResolvedAnchorProfile {
25
+ name: string;
26
+ description?: string;
27
+ website?: string;
28
+ email?: string;
29
+ story?: string;
30
+ socialLinks?: ResolvedAnchorProfileSocialLink[];
16
31
  }
17
32
  export interface ResolvedUserIdentity {
18
33
  handle: string;
@@ -23,7 +38,11 @@ export interface ResolvedUserIdentity {
23
38
  domain: string;
24
39
  contentRepo: string;
25
40
  discordEnabled: boolean;
41
+ discordAnchorUserId?: string;
26
42
  effectiveAiApiKey: string;
43
+ effectiveGitSyncToken: string;
44
+ effectiveMcpAuthToken: string;
45
+ anchorProfile: ResolvedAnchorProfile;
27
46
  snapshotStatus: SnapshotStatus;
28
47
  }
29
48
  export interface ResolvedUser extends ResolvedUserIdentity {
@@ -40,7 +59,4 @@ export interface PilotRegistry {
40
59
  cohorts: ResolvedCohort[];
41
60
  users: ResolvedUser[];
42
61
  }
43
- declare class PilotRegistryError extends Error {
44
- }
45
62
  export declare function loadPilotRegistry(rootDir: string, options?: LoadPilotRegistryOptions): Promise<PilotRegistry>;
46
- export { PilotRegistryError };
@@ -1,2 +1,2 @@
1
- import { type UserRunner } from "./reconcile-lib";
2
- export declare function onboardUser(rootDir: string, handle: string, runner?: UserRunner): Promise<void>;
1
+ import { type UserRunner, type ContentRepoSyncOptions } from "./reconcile-lib";
2
+ export declare function onboardUser(rootDir: string, handle: string, runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
@@ -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;
@@ -5,5 +5,5 @@ export interface PushSecretsOptions {
5
5
  runCommand?: RunCommand | undefined;
6
6
  logger?: ((message: string) => void) | undefined;
7
7
  }
8
- export declare function pushSecretsToBackend(target: PushTarget, secrets: readonly SecretPair[], options?: PushSecretsOptions): Promise<void>;
8
+ export declare function pushSecretsToBackend(_target: PushTarget, secrets: readonly SecretPair[], options?: PushSecretsOptions): Promise<void>;
9
9
  export { normalizePushTarget };
@@ -1,2 +1,2 @@
1
- import { type UserRunner } from "./reconcile-lib";
2
- export declare function reconcileAll(rootDir: string, runner?: UserRunner): Promise<void>;
1
+ import { type UserRunner, type ContentRepoSyncOptions } from "./reconcile-lib";
2
+ export declare function reconcileAll(rootDir: string, runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
@@ -1,2 +1,2 @@
1
- import { type UserRunner } from "./reconcile-lib";
2
- export declare function reconcileCohort(rootDir: string, cohortId: string, runner?: UserRunner): Promise<void>;
1
+ import { type UserRunner, type ContentRepoSyncOptions } from "./reconcile-lib";
2
+ export declare function reconcileCohort(rootDir: string, cohortId: string, runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
@@ -1,7 +1,9 @@
1
+ import { type ContentRepoSyncOptions } from "./content-repo";
1
2
  import { type PilotRegistry, type ResolvedUser } from "./load-registry";
2
3
  import type { UserRunner } from "./user-runner";
3
- export type { UserRunResult, UserRunner } from "./user-runner";
4
- export declare function runUsers(rootDir: string, registry: PilotRegistry, users: ResolvedUser[], runner?: UserRunner): Promise<void>;
4
+ export type { ContentRepoSyncOptions } from "./content-repo";
5
+ export type { ContentRepoFile, UserRunResult, UserRunner } from "./user-runner";
6
+ export declare function runUsers(rootDir: string, registry: PilotRegistry, users: ResolvedUser[], runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
5
7
  export declare function findUser(rootDir: string, handle: string): Promise<{
6
8
  registry: PilotRegistry;
7
9
  user: ResolvedUser;
@@ -15,7 +15,6 @@ export interface CommandDependencies extends LoadPilotRegistryOptions {
15
15
  logger?: ((message: string) => void) | undefined;
16
16
  fetchImpl?: FetchLike | undefined;
17
17
  lookupHost?: LookupHost | undefined;
18
- secretRunCommand?: OpsRunCommand | undefined;
19
18
  bootstrapRunCommand?: OpsRunCommand | undefined;
20
19
  sshKeygen?: SshKeygen | undefined;
21
20
  }
@@ -1,5 +1,6 @@
1
1
  export type RunCommand = (command: string, args: string[], options?: {
2
2
  stdin?: string;
3
3
  env?: NodeJS.ProcessEnv;
4
+ cwd?: string;
4
5
  }) => Promise<void>;
5
6
  export declare const runSubprocess: RunCommand;
package/dist/schema.d.ts CHANGED
@@ -3,6 +3,7 @@ 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;
5
5
  export declare const secretNameSchema: z.ZodString;
6
+ export declare const agePublicKeySchema: z.ZodString;
6
7
  export declare const pilotSchema: z.ZodObject<{
7
8
  schemaVersion: z.ZodLiteral<1>;
8
9
  brainVersion: z.ZodString;
@@ -12,7 +13,12 @@ export declare const pilotSchema: z.ZodObject<{
12
13
  domainSuffix: z.ZodString;
13
14
  preset: z.ZodEnum<["core", "default", "pro"]>;
14
15
  aiApiKey: z.ZodString;
16
+ gitSyncToken: z.ZodString;
17
+ contentRepoAdminToken: z.ZodString;
18
+ mcpAuthToken: z.ZodString;
19
+ agePublicKey: z.ZodString;
15
20
  }, "strict", z.ZodTypeAny, {
21
+ agePublicKey: string;
16
22
  schemaVersion: 1;
17
23
  brainVersion: string;
18
24
  model: "rover";
@@ -21,7 +27,11 @@ export declare const pilotSchema: z.ZodObject<{
21
27
  domainSuffix: string;
22
28
  preset: "default" | "core" | "pro";
23
29
  aiApiKey: string;
30
+ gitSyncToken: string;
31
+ contentRepoAdminToken: string;
32
+ mcpAuthToken: string;
24
33
  }, {
34
+ agePublicKey: string;
25
35
  schemaVersion: 1;
26
36
  brainVersion: string;
27
37
  model: "rover";
@@ -30,53 +40,143 @@ export declare const pilotSchema: z.ZodObject<{
30
40
  domainSuffix: string;
31
41
  preset: "default" | "core" | "pro";
32
42
  aiApiKey: string;
43
+ gitSyncToken: string;
44
+ contentRepoAdminToken: string;
45
+ mcpAuthToken: string;
33
46
  }>;
34
47
  export declare const userSchema: z.ZodObject<{
35
48
  handle: z.ZodString;
36
49
  discord: z.ZodObject<{
37
50
  enabled: z.ZodBoolean;
51
+ anchorUserId: z.ZodOptional<z.ZodString>;
38
52
  }, "strict", z.ZodTypeAny, {
39
53
  enabled: boolean;
54
+ anchorUserId?: string | undefined;
40
55
  }, {
41
56
  enabled: boolean;
57
+ anchorUserId?: string | undefined;
42
58
  }>;
43
59
  aiApiKeyOverride: z.ZodOptional<z.ZodString>;
60
+ gitSyncTokenOverride: z.ZodOptional<z.ZodString>;
61
+ mcpAuthTokenOverride: z.ZodOptional<z.ZodString>;
62
+ anchorProfile: z.ZodOptional<z.ZodObject<{
63
+ name: z.ZodOptional<z.ZodString>;
64
+ description: z.ZodOptional<z.ZodString>;
65
+ website: z.ZodOptional<z.ZodString>;
66
+ email: z.ZodOptional<z.ZodString>;
67
+ story: z.ZodOptional<z.ZodString>;
68
+ socialLinks: z.ZodOptional<z.ZodArray<z.ZodObject<{
69
+ platform: z.ZodEnum<["github", "instagram", "linkedin", "email", "website"]>;
70
+ url: z.ZodString;
71
+ label: z.ZodOptional<z.ZodString>;
72
+ }, "strict", z.ZodTypeAny, {
73
+ url: string;
74
+ platform: "github" | "instagram" | "linkedin" | "email" | "website";
75
+ label?: string | undefined;
76
+ }, {
77
+ url: string;
78
+ platform: "github" | "instagram" | "linkedin" | "email" | "website";
79
+ label?: string | undefined;
80
+ }>, "many">>;
81
+ }, "strict", z.ZodTypeAny, {
82
+ name?: string | undefined;
83
+ email?: string | undefined;
84
+ website?: string | undefined;
85
+ description?: string | undefined;
86
+ story?: string | undefined;
87
+ socialLinks?: {
88
+ url: string;
89
+ platform: "github" | "instagram" | "linkedin" | "email" | "website";
90
+ label?: string | undefined;
91
+ }[] | undefined;
92
+ }, {
93
+ name?: string | undefined;
94
+ email?: string | undefined;
95
+ website?: string | undefined;
96
+ description?: string | undefined;
97
+ story?: string | undefined;
98
+ socialLinks?: {
99
+ url: string;
100
+ platform: "github" | "instagram" | "linkedin" | "email" | "website";
101
+ label?: string | undefined;
102
+ }[] | undefined;
103
+ }>>;
44
104
  }, "strict", z.ZodTypeAny, {
45
105
  handle: string;
46
106
  discord: {
47
107
  enabled: boolean;
108
+ anchorUserId?: string | undefined;
48
109
  };
49
110
  aiApiKeyOverride?: string | undefined;
111
+ gitSyncTokenOverride?: string | undefined;
112
+ mcpAuthTokenOverride?: string | undefined;
113
+ anchorProfile?: {
114
+ name?: string | undefined;
115
+ email?: string | undefined;
116
+ website?: string | undefined;
117
+ description?: string | undefined;
118
+ story?: string | undefined;
119
+ socialLinks?: {
120
+ url: string;
121
+ platform: "github" | "instagram" | "linkedin" | "email" | "website";
122
+ label?: string | undefined;
123
+ }[] | undefined;
124
+ } | undefined;
50
125
  }, {
51
126
  handle: string;
52
127
  discord: {
53
128
  enabled: boolean;
129
+ anchorUserId?: string | undefined;
54
130
  };
55
131
  aiApiKeyOverride?: string | undefined;
132
+ gitSyncTokenOverride?: string | undefined;
133
+ mcpAuthTokenOverride?: string | undefined;
134
+ anchorProfile?: {
135
+ name?: string | undefined;
136
+ email?: string | undefined;
137
+ website?: string | undefined;
138
+ description?: string | undefined;
139
+ story?: string | undefined;
140
+ socialLinks?: {
141
+ url: string;
142
+ platform: "github" | "instagram" | "linkedin" | "email" | "website";
143
+ label?: string | undefined;
144
+ }[] | undefined;
145
+ } | undefined;
56
146
  }>;
57
147
  export declare const cohortSchema: z.ZodEffects<z.ZodObject<{
58
148
  members: z.ZodArray<z.ZodString, "many">;
59
149
  brainVersionOverride: z.ZodOptional<z.ZodString>;
60
150
  presetOverride: z.ZodOptional<z.ZodEnum<["core", "default", "pro"]>>;
61
151
  aiApiKeyOverride: z.ZodOptional<z.ZodString>;
152
+ gitSyncTokenOverride: z.ZodOptional<z.ZodString>;
153
+ mcpAuthTokenOverride: z.ZodOptional<z.ZodString>;
62
154
  }, "strict", z.ZodTypeAny, {
63
155
  members: string[];
64
156
  aiApiKeyOverride?: string | undefined;
157
+ gitSyncTokenOverride?: string | undefined;
158
+ mcpAuthTokenOverride?: string | undefined;
65
159
  brainVersionOverride?: string | undefined;
66
160
  presetOverride?: "default" | "core" | "pro" | undefined;
67
161
  }, {
68
162
  members: string[];
69
163
  aiApiKeyOverride?: string | undefined;
164
+ gitSyncTokenOverride?: string | undefined;
165
+ mcpAuthTokenOverride?: string | undefined;
70
166
  brainVersionOverride?: string | undefined;
71
167
  presetOverride?: "default" | "core" | "pro" | undefined;
72
168
  }>, {
73
169
  members: string[];
74
170
  aiApiKeyOverride?: string | undefined;
171
+ gitSyncTokenOverride?: string | undefined;
172
+ mcpAuthTokenOverride?: string | undefined;
75
173
  brainVersionOverride?: string | undefined;
76
174
  presetOverride?: "default" | "core" | "pro" | undefined;
77
175
  }, {
78
176
  members: string[];
79
177
  aiApiKeyOverride?: string | undefined;
178
+ gitSyncTokenOverride?: string | undefined;
179
+ mcpAuthTokenOverride?: string | undefined;
80
180
  brainVersionOverride?: string | undefined;
81
181
  presetOverride?: "default" | "core" | "pro" | undefined;
82
182
  }>;
@@ -0,0 +1,32 @@
1
+ import { z } from "@brains/utils";
2
+ declare const encryptedUserSecretsSchema: z.ZodObject<{
3
+ gitSyncToken: z.ZodOptional<z.ZodString>;
4
+ mcpAuthToken: z.ZodOptional<z.ZodString>;
5
+ discordBotToken: z.ZodOptional<z.ZodString>;
6
+ aiApiKey: z.ZodOptional<z.ZodString>;
7
+ }, "strict", z.ZodTypeAny, {
8
+ aiApiKey?: string | undefined;
9
+ gitSyncToken?: string | undefined;
10
+ mcpAuthToken?: string | undefined;
11
+ discordBotToken?: string | undefined;
12
+ }, {
13
+ aiApiKey?: string | undefined;
14
+ gitSyncToken?: string | undefined;
15
+ mcpAuthToken?: string | undefined;
16
+ discordBotToken?: string | undefined;
17
+ }>;
18
+ export type EncryptedUserSecrets = z.infer<typeof encryptedUserSecretsSchema>;
19
+ export interface SecretsEncryptOptions {
20
+ env?: NodeJS.ProcessEnv | undefined;
21
+ logger?: ((message: string) => void) | undefined;
22
+ dryRun?: boolean | undefined;
23
+ }
24
+ export interface SecretsEncryptResult {
25
+ encryptedPath: string;
26
+ plaintextPath: string;
27
+ deletedPlaintext: boolean;
28
+ encryptedKeys: Array<keyof EncryptedUserSecrets>;
29
+ dryRun?: boolean | undefined;
30
+ }
31
+ export declare function encryptPilotSecrets(rootDir: string, handle: string, options?: SecretsEncryptOptions): Promise<SecretsEncryptResult>;
32
+ export {};
@@ -10,4 +10,4 @@ export interface SecretsPushResult {
10
10
  skippedKeys: string[];
11
11
  dryRun?: boolean | undefined;
12
12
  }
13
- export declare function pushPilotSecrets(rootDir: string, handle: string, options?: SecretsPushOptions): Promise<SecretsPushResult>;
13
+ export declare function pushPilotSecrets(rootDir: string, options?: SecretsPushOptions): Promise<SecretsPushResult>;
@@ -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>;
@@ -1,6 +1,11 @@
1
1
  import type { ResolvedUser } from "./load-registry";
2
+ export interface ContentRepoFile {
3
+ path: string;
4
+ content: string;
5
+ }
2
6
  export interface UserRunResult {
3
7
  brainYaml?: string;
4
8
  envFile?: string;
9
+ contentRepoFiles?: ContentRepoFile[];
5
10
  }
6
11
  export type UserRunner = (user: ResolvedUser) => Promise<UserRunResult | void>;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.2.0-alpha.6",
7
+ "version": "0.2.0-alpha.61",
8
8
  "type": "module",
9
9
  "exports": {
10
10
  ".": {
@@ -32,10 +32,14 @@
32
32
  "typecheck": "tsc --noEmit",
33
33
  "lint": "eslint . --ext .ts",
34
34
  "lint:fix": "eslint . --ext .ts --fix",
35
- "test": "bun test"
35
+ "test": "bun test --timeout 20000",
36
+ "test:smoke": "RUN_SMOKE_TESTS=1 bun test --timeout 60000"
37
+ },
38
+ "dependencies": {
39
+ "age-encryption": "^0.3.0"
36
40
  },
37
- "dependencies": {},
38
41
  "devDependencies": {
42
+ "@brains/deploy-templates": "workspace:*",
39
43
  "@brains/eslint-config": "workspace:*",
40
44
  "@brains/typescript-config": "workspace:*",
41
45
  "@brains/utils": "workspace:*",
@@ -4,18 +4,29 @@
4
4
  # ----------
5
5
 
6
6
  # AI provider
7
+ # Shared GitHub secret by default; a per-user override may come from the decrypted
8
+ # users/<handle>.secrets.yaml.age file at deploy time.
7
9
  # @required @sensitive
8
10
  AI_API_KEY=
9
11
 
10
12
  # Git sync
13
+ # Comes from the decrypted users/<handle>.secrets.yaml.age file.
11
14
  # @required @sensitive
12
15
  GIT_SYNC_TOKEN=
13
16
 
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.
20
+ # @required @sensitive
21
+ CONTENT_REPO_ADMIN_TOKEN=
22
+
14
23
  # MCP interface
24
+ # Comes from the decrypted users/<handle>.secrets.yaml.age file.
15
25
  # @required @sensitive
16
26
  MCP_AUTH_TOKEN=
17
27
 
18
28
  # Discord (optional, per-user)
29
+ # Comes from the decrypted users/<handle>.secrets.yaml.age file when enabled.
19
30
  # @sensitive
20
31
  DISCORD_BOT_TOKEN=
21
32
 
@@ -54,6 +54,7 @@ jobs:
54
54
  with:
55
55
  context: .
56
56
  file: deploy/Dockerfile
57
+ target: fleet
57
58
  push: true
58
59
  build-args: |
59
60
  BRAIN_VERSION=${{ env.BRAIN_VERSION }}
@@ -12,8 +12,14 @@ on:
12
12
  paths:
13
13
  - users/*/.env
14
14
  - users/*/brain.yaml
15
+ - users/*/content/**
16
+ - users/*.secrets.yaml.age
15
17
  - deploy/**
16
18
  - .github/workflows/deploy.yml
19
+ workflow_run:
20
+ workflows: [Reconcile]
21
+ types: [completed]
22
+ branches: [main]
17
23
 
18
24
  permissions:
19
25
  contents: write
@@ -21,13 +27,16 @@ permissions:
21
27
 
22
28
  jobs:
23
29
  resolve_handles:
30
+ if: >
31
+ github.event_name != 'workflow_run' ||
32
+ github.event.workflow_run.conclusion == 'success'
24
33
  runs-on: ubuntu-latest
25
34
  outputs:
26
35
  handles_json: ${{ steps.resolve.outputs.handles_json }}
27
36
  steps:
28
37
  - uses: actions/checkout@v5
29
38
  with:
30
- ref: ${{ github.sha }}
39
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
31
40
  fetch-depth: 0
32
41
 
33
42
  - uses: oven-sh/setup-bun@v2
@@ -39,7 +48,7 @@ jobs:
39
48
  id: resolve
40
49
  env:
41
50
  HANDLE_INPUT: ${{ inputs.handle || '' }}
42
- BEFORE_SHA: ${{ github.event.before || '' }}
51
+ BEFORE_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.before || '' }}
43
52
  run: bun deploy/scripts/resolve-deploy-handles.ts
44
53
 
45
54
  no_changes:
@@ -66,15 +75,25 @@ jobs:
66
75
  steps:
67
76
  - uses: actions/checkout@v5
68
77
  with:
69
- ref: ${{ github.sha }}
78
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
70
79
 
71
80
  - uses: oven-sh/setup-bun@v2
72
81
 
73
82
  - name: Install operator tooling
74
83
  run: bun install
75
84
 
85
+ - name: Decrypt user secrets
86
+ id: user_secrets
87
+ env:
88
+ AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
89
+ run: bun deploy/scripts/decrypt-user-secrets.ts "$HANDLE"
90
+
76
91
  - name: Reconcile selected user config
77
- run: bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
92
+ env:
93
+ SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
94
+ run: |
95
+ export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
96
+ bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
78
97
 
79
98
  - name: Resolve generated user config
80
99
  id: user_config
@@ -82,10 +101,9 @@ jobs:
82
101
 
83
102
  - name: Validate selected secrets
84
103
  env:
85
- AI_API_KEY: ${{ secrets[steps.user_config.outputs.ai_api_key_secret_name] }}
86
- GIT_SYNC_TOKEN: ${{ secrets[steps.user_config.outputs.git_sync_token_secret_name] }}
87
- MCP_AUTH_TOKEN: ${{ secrets[steps.user_config.outputs.mcp_auth_token_secret_name] }}
88
- DISCORD_BOT_TOKEN: ${{ steps.user_config.outputs.discord_bot_token_secret_name != '' && secrets[steps.user_config.outputs.discord_bot_token_secret_name] || '' }}
104
+ SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
105
+ SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
106
+ SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
89
107
  HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
90
108
  HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
91
109
  HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
@@ -96,7 +114,19 @@ jobs:
96
114
  CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
97
115
  CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
98
116
  PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
99
- run: bun deploy/scripts/validate-secrets.ts
117
+ run: |
118
+ export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
119
+ export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
120
+ export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
121
+ bun deploy/scripts/validate-secrets.ts
122
+
123
+ - name: Seed content repo
124
+ env:
125
+ CONTENT_REPO: ${{ steps.user_config.outputs.content_repo }}
126
+ SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
127
+ run: |
128
+ export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
129
+ bun deploy/scripts/sync-content-repo.ts
100
130
 
101
131
  - name: Log in to GHCR
102
132
  uses: docker/login-action@v3
@@ -143,14 +173,17 @@ jobs:
143
173
 
144
174
  - name: Write .kamal/secrets
145
175
  env:
146
- AI_API_KEY: ${{ secrets[steps.user_config.outputs.ai_api_key_secret_name] }}
147
- GIT_SYNC_TOKEN: ${{ secrets[steps.user_config.outputs.git_sync_token_secret_name] }}
148
- MCP_AUTH_TOKEN: ${{ secrets[steps.user_config.outputs.mcp_auth_token_secret_name] }}
149
- DISCORD_BOT_TOKEN: ${{ steps.user_config.outputs.discord_bot_token_secret_name != '' && secrets[steps.user_config.outputs.discord_bot_token_secret_name] || '' }}
176
+ SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
177
+ SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
178
+ SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
150
179
  KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
151
180
  CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
152
181
  PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
153
- run: bun deploy/scripts/write-kamal-secrets.ts
182
+ run: |
183
+ export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
184
+ export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
185
+ export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
186
+ bun deploy/scripts/write-kamal-secrets.ts
154
187
 
155
188
  - name: Provision server
156
189
  id: provision
@@ -167,8 +200,11 @@ jobs:
167
200
  CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
168
201
  CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
169
202
  BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
203
+ PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
170
204
  SERVER_IP: ${{ steps.provision.outputs.server_ip }}
171
- run: bun deploy/scripts/update-dns.ts
205
+ run: |
206
+ bun deploy/scripts/update-dns.ts
207
+ BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
172
208
 
173
209
  - name: Validate SSH key
174
210
  run: ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null
@@ -195,6 +231,7 @@ jobs:
195
231
  IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
196
232
  REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
197
233
  BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
234
+ PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
198
235
  BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
199
236
  run: kamal setup --skip-push -c deploy/kamal/deploy.yml
200
237
 
@@ -202,7 +239,10 @@ jobs:
202
239
  env:
203
240
  SERVER_IP: ${{ steps.provision.outputs.server_ip }}
204
241
  BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
205
- run: curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
242
+ PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
243
+ run: |
244
+ curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
245
+ curl -I -k --max-time 20 --resolve "$PREVIEW_DOMAIN:443:$SERVER_IP" "https://$PREVIEW_DOMAIN/"
206
246
 
207
247
  - name: Upload generated config
208
248
  uses: actions/upload-artifact@v4
@@ -211,6 +251,7 @@ jobs:
211
251
  path: |
212
252
  users/${{ matrix.handle }}/brain.yaml
213
253
  users/${{ matrix.handle }}/.env
254
+ users/${{ matrix.handle }}/content
214
255
 
215
256
  - name: Dump remote proxy diagnostics
216
257
  if: failure()
@@ -237,7 +278,7 @@ jobs:
237
278
  steps:
238
279
  - uses: actions/checkout@v5
239
280
  with:
240
- ref: ${{ github.sha }}
281
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
241
282
 
242
283
  - uses: oven-sh/setup-bun@v2
243
284
 
@@ -259,8 +300,22 @@ jobs:
259
300
  if git diff --quiet -- users views; then
260
301
  exit 0
261
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
+
262
318
  git config user.name "github-actions[bot]"
263
319
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
264
- git add users views
265
320
  git commit -m "chore(ops): reconcile generated config"
266
- git push
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 }}