@rizom/ops 0.2.0-alpha.0
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/README.md +24 -0
- package/dist/brains-ops.js +174 -0
- package/dist/default-user-runner.d.ts +3 -0
- package/dist/deploy.js +171 -0
- package/dist/entries/deploy.d.ts +2 -0
- package/dist/entrypoint.d.ts +2 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +173 -0
- package/dist/init.d.ts +1 -0
- package/dist/load-registry.d.ts +46 -0
- package/dist/onboard-user.d.ts +2 -0
- package/dist/parse-args.d.ts +9 -0
- package/dist/reconcile-all.d.ts +2 -0
- package/dist/reconcile-cohort.d.ts +2 -0
- package/dist/reconcile-lib.d.ts +16 -0
- package/dist/render-users-table.d.ts +5 -0
- package/dist/run-command.d.ts +11 -0
- package/dist/schema.d.ts +86 -0
- package/dist/user-runner.d.ts +6 -0
- package/dist/user-secret-names.d.ts +6 -0
- package/package.json +65 -0
- package/templates/rover-pilot/.env.schema +54 -0
- package/templates/rover-pilot/.github/workflows/build.yml +63 -0
- package/templates/rover-pilot/.github/workflows/deploy.yml +261 -0
- package/templates/rover-pilot/.github/workflows/reconcile.yml +45 -0
- package/templates/rover-pilot/.kamal/hooks/pre-deploy +9 -0
- package/templates/rover-pilot/README.md +42 -0
- package/templates/rover-pilot/cohorts/cohort-1.yaml +2 -0
- package/templates/rover-pilot/deploy/Dockerfile +15 -0
- package/templates/rover-pilot/deploy/kamal/deploy.yml +39 -0
- package/templates/rover-pilot/deploy/scripts/helpers.ts +10 -0
- package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +59 -0
- package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +49 -0
- package/templates/rover-pilot/docs/onboarding-checklist.md +10 -0
- package/templates/rover-pilot/docs/operator-playbook.md +47 -0
- package/templates/rover-pilot/package.json +9 -0
- package/templates/rover-pilot/pilot.yaml +8 -0
- package/templates/rover-pilot/users/alice.yaml +3 -0
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initPilotRepo(rootDir: string): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type PilotConfig, type PilotPreset } from "./schema";
|
|
2
|
+
export type ExternalStatus = "unknown" | "ready" | "failed";
|
|
3
|
+
export type SnapshotStatus = "present" | "missing";
|
|
4
|
+
export interface ObservedUserStatus {
|
|
5
|
+
serverStatus?: ExternalStatus;
|
|
6
|
+
deployStatus?: ExternalStatus;
|
|
7
|
+
dnsStatus?: ExternalStatus;
|
|
8
|
+
mcpStatus?: ExternalStatus;
|
|
9
|
+
}
|
|
10
|
+
export interface ResolvedCohort {
|
|
11
|
+
id: string;
|
|
12
|
+
members: string[];
|
|
13
|
+
brainVersionOverride?: string;
|
|
14
|
+
presetOverride?: PilotPreset;
|
|
15
|
+
aiApiKeyOverride?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ResolvedUserIdentity {
|
|
18
|
+
handle: string;
|
|
19
|
+
cohort: string;
|
|
20
|
+
brainVersion: string;
|
|
21
|
+
model: "rover";
|
|
22
|
+
preset: PilotPreset;
|
|
23
|
+
domain: string;
|
|
24
|
+
contentRepo: string;
|
|
25
|
+
discordEnabled: boolean;
|
|
26
|
+
effectiveAiApiKey: string;
|
|
27
|
+
snapshotStatus: SnapshotStatus;
|
|
28
|
+
}
|
|
29
|
+
export interface ResolvedUser extends ResolvedUserIdentity {
|
|
30
|
+
serverStatus: ExternalStatus;
|
|
31
|
+
deployStatus: ExternalStatus;
|
|
32
|
+
dnsStatus: ExternalStatus;
|
|
33
|
+
mcpStatus: ExternalStatus;
|
|
34
|
+
}
|
|
35
|
+
export interface LoadPilotRegistryOptions {
|
|
36
|
+
resolveStatus?: (user: ResolvedUserIdentity) => Promise<ObservedUserStatus | undefined>;
|
|
37
|
+
}
|
|
38
|
+
export interface PilotRegistry {
|
|
39
|
+
pilot: PilotConfig;
|
|
40
|
+
cohorts: ResolvedCohort[];
|
|
41
|
+
users: ResolvedUser[];
|
|
42
|
+
}
|
|
43
|
+
declare class PilotRegistryError extends Error {
|
|
44
|
+
}
|
|
45
|
+
export declare function loadPilotRegistry(rootDir: string, options?: LoadPilotRegistryOptions): Promise<PilotRegistry>;
|
|
46
|
+
export { PilotRegistryError };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type PilotRegistry, type ResolvedUser } from "./load-registry";
|
|
2
|
+
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>;
|
|
5
|
+
export declare function findUser(rootDir: string, handle: string): Promise<{
|
|
6
|
+
registry: PilotRegistry;
|
|
7
|
+
user: ResolvedUser;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function findCohortUsers(rootDir: string, cohortId: string): Promise<{
|
|
10
|
+
registry: PilotRegistry;
|
|
11
|
+
users: ResolvedUser[];
|
|
12
|
+
}>;
|
|
13
|
+
export declare function findAllUsers(rootDir: string): Promise<{
|
|
14
|
+
registry: PilotRegistry;
|
|
15
|
+
users: ResolvedUser[];
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type LoadPilotRegistryOptions, type PilotRegistry } from "./load-registry";
|
|
2
|
+
export interface WriteUsersTableOptions extends LoadPilotRegistryOptions {
|
|
3
|
+
registry?: PilotRegistry;
|
|
4
|
+
}
|
|
5
|
+
export declare function writeUsersTable(rootDir: string, options?: WriteUsersTableOptions): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LoadPilotRegistryOptions } from "./load-registry";
|
|
2
|
+
import type { ParsedArgs } from "./parse-args";
|
|
3
|
+
import type { UserRunner } from "./user-runner";
|
|
4
|
+
export interface CommandResult {
|
|
5
|
+
success: boolean;
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CommandDependencies extends LoadPilotRegistryOptions {
|
|
9
|
+
runner?: UserRunner;
|
|
10
|
+
}
|
|
11
|
+
export declare function runCommand(parsed: ParsedArgs, dependencies?: CommandDependencies): Promise<CommandResult>;
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from "@brains/utils";
|
|
2
|
+
export declare const presetSchema: z.ZodEnum<["core", "default", "pro"]>;
|
|
3
|
+
export declare const exactVersionSchema: z.ZodString;
|
|
4
|
+
export declare const handleSchema: z.ZodString;
|
|
5
|
+
export declare const secretNameSchema: z.ZodString;
|
|
6
|
+
export declare const pilotSchema: z.ZodObject<{
|
|
7
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
8
|
+
brainVersion: z.ZodString;
|
|
9
|
+
model: z.ZodLiteral<"rover">;
|
|
10
|
+
githubOrg: z.ZodString;
|
|
11
|
+
contentRepoPrefix: z.ZodString;
|
|
12
|
+
domainSuffix: z.ZodString;
|
|
13
|
+
preset: z.ZodEnum<["core", "default", "pro"]>;
|
|
14
|
+
aiApiKey: z.ZodString;
|
|
15
|
+
}, "strict", z.ZodTypeAny, {
|
|
16
|
+
schemaVersion: 1;
|
|
17
|
+
brainVersion: string;
|
|
18
|
+
model: "rover";
|
|
19
|
+
githubOrg: string;
|
|
20
|
+
contentRepoPrefix: string;
|
|
21
|
+
domainSuffix: string;
|
|
22
|
+
preset: "core" | "default" | "pro";
|
|
23
|
+
aiApiKey: string;
|
|
24
|
+
}, {
|
|
25
|
+
schemaVersion: 1;
|
|
26
|
+
brainVersion: string;
|
|
27
|
+
model: "rover";
|
|
28
|
+
githubOrg: string;
|
|
29
|
+
contentRepoPrefix: string;
|
|
30
|
+
domainSuffix: string;
|
|
31
|
+
preset: "core" | "default" | "pro";
|
|
32
|
+
aiApiKey: string;
|
|
33
|
+
}>;
|
|
34
|
+
export declare const userSchema: z.ZodObject<{
|
|
35
|
+
handle: z.ZodString;
|
|
36
|
+
discord: z.ZodObject<{
|
|
37
|
+
enabled: z.ZodBoolean;
|
|
38
|
+
}, "strict", z.ZodTypeAny, {
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
}, {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
aiApiKeyOverride: z.ZodOptional<z.ZodString>;
|
|
44
|
+
}, "strict", z.ZodTypeAny, {
|
|
45
|
+
handle: string;
|
|
46
|
+
discord: {
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
};
|
|
49
|
+
aiApiKeyOverride?: string | undefined;
|
|
50
|
+
}, {
|
|
51
|
+
handle: string;
|
|
52
|
+
discord: {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
};
|
|
55
|
+
aiApiKeyOverride?: string | undefined;
|
|
56
|
+
}>;
|
|
57
|
+
export declare const cohortSchema: z.ZodEffects<z.ZodObject<{
|
|
58
|
+
members: z.ZodArray<z.ZodString, "many">;
|
|
59
|
+
brainVersionOverride: z.ZodOptional<z.ZodString>;
|
|
60
|
+
presetOverride: z.ZodOptional<z.ZodEnum<["core", "default", "pro"]>>;
|
|
61
|
+
aiApiKeyOverride: z.ZodOptional<z.ZodString>;
|
|
62
|
+
}, "strict", z.ZodTypeAny, {
|
|
63
|
+
members: string[];
|
|
64
|
+
aiApiKeyOverride?: string | undefined;
|
|
65
|
+
brainVersionOverride?: string | undefined;
|
|
66
|
+
presetOverride?: "core" | "default" | "pro" | undefined;
|
|
67
|
+
}, {
|
|
68
|
+
members: string[];
|
|
69
|
+
aiApiKeyOverride?: string | undefined;
|
|
70
|
+
brainVersionOverride?: string | undefined;
|
|
71
|
+
presetOverride?: "core" | "default" | "pro" | undefined;
|
|
72
|
+
}>, {
|
|
73
|
+
members: string[];
|
|
74
|
+
aiApiKeyOverride?: string | undefined;
|
|
75
|
+
brainVersionOverride?: string | undefined;
|
|
76
|
+
presetOverride?: "core" | "default" | "pro" | undefined;
|
|
77
|
+
}, {
|
|
78
|
+
members: string[];
|
|
79
|
+
aiApiKeyOverride?: string | undefined;
|
|
80
|
+
brainVersionOverride?: string | undefined;
|
|
81
|
+
presetOverride?: "core" | "default" | "pro" | undefined;
|
|
82
|
+
}>;
|
|
83
|
+
export type PilotConfig = z.infer<typeof pilotSchema>;
|
|
84
|
+
export type UserConfig = z.infer<typeof userSchema>;
|
|
85
|
+
export type CohortConfig = z.infer<typeof cohortSchema>;
|
|
86
|
+
export type PilotPreset = z.infer<typeof presetSchema>;
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rizom/ops",
|
|
3
|
+
"description": "Operator CLI for managing private brain fleet registry repos",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"version": "0.2.0-alpha.0",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./deploy": {
|
|
15
|
+
"bun": "./src/entries/deploy.ts",
|
|
16
|
+
"types": "./dist/deploy.d.ts",
|
|
17
|
+
"import": "./dist/deploy.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"bin": {
|
|
23
|
+
"brains-ops": "./dist/brains-ops.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"templates"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "bun scripts/build.ts",
|
|
31
|
+
"prepublishOnly": "bun scripts/build.ts",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"lint": "eslint . --ext .ts",
|
|
34
|
+
"lint:fix": "eslint . --ext .ts --fix",
|
|
35
|
+
"test": "bun test"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@brains/eslint-config": "workspace:*",
|
|
40
|
+
"@brains/typescript-config": "workspace:*",
|
|
41
|
+
"@brains/utils": "workspace:*",
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"typescript": "^5.3.3"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/rizom-ai/brains.git",
|
|
48
|
+
"directory": "packages/brains-ops"
|
|
49
|
+
},
|
|
50
|
+
"license": "Apache-2.0",
|
|
51
|
+
"author": "Yeehaa <yeehaa@rizom.ai> (https://rizom.ai)",
|
|
52
|
+
"homepage": "https://github.com/rizom-ai/brains/tree/main/packages/brains-ops#readme",
|
|
53
|
+
"bugs": "https://github.com/rizom-ai/brains/issues",
|
|
54
|
+
"engines": {
|
|
55
|
+
"bun": ">=1.3.3"
|
|
56
|
+
},
|
|
57
|
+
"keywords": [
|
|
58
|
+
"brains",
|
|
59
|
+
"ops",
|
|
60
|
+
"cli",
|
|
61
|
+
"deploy",
|
|
62
|
+
"fleet",
|
|
63
|
+
"operator"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Rover pilot instance env schema
|
|
2
|
+
# This file is the single source of truth for required and sensitive deploy vars.
|
|
3
|
+
# @defaultRequired=false @defaultSensitive=false
|
|
4
|
+
# ----------
|
|
5
|
+
|
|
6
|
+
# AI provider
|
|
7
|
+
# @required @sensitive
|
|
8
|
+
AI_API_KEY=
|
|
9
|
+
|
|
10
|
+
# Git sync
|
|
11
|
+
# @required @sensitive
|
|
12
|
+
GIT_SYNC_TOKEN=
|
|
13
|
+
|
|
14
|
+
# MCP interface
|
|
15
|
+
# @required @sensitive
|
|
16
|
+
MCP_AUTH_TOKEN=
|
|
17
|
+
|
|
18
|
+
# Discord (optional, per-user)
|
|
19
|
+
# @sensitive
|
|
20
|
+
DISCORD_BOT_TOKEN=
|
|
21
|
+
|
|
22
|
+
# ---- deploy/provision vars ----
|
|
23
|
+
|
|
24
|
+
# @required @sensitive
|
|
25
|
+
HCLOUD_TOKEN=
|
|
26
|
+
|
|
27
|
+
# @required
|
|
28
|
+
HCLOUD_SSH_KEY_NAME=
|
|
29
|
+
|
|
30
|
+
# @required
|
|
31
|
+
HCLOUD_SERVER_TYPE=
|
|
32
|
+
|
|
33
|
+
# @required
|
|
34
|
+
HCLOUD_LOCATION=
|
|
35
|
+
|
|
36
|
+
# @required @sensitive
|
|
37
|
+
KAMAL_SSH_PRIVATE_KEY=
|
|
38
|
+
|
|
39
|
+
# @required @sensitive
|
|
40
|
+
KAMAL_REGISTRY_PASSWORD=
|
|
41
|
+
|
|
42
|
+
# @required @sensitive
|
|
43
|
+
CF_API_TOKEN=
|
|
44
|
+
|
|
45
|
+
# @required
|
|
46
|
+
CF_ZONE_ID=
|
|
47
|
+
|
|
48
|
+
# ---- TLS cert vars ----
|
|
49
|
+
|
|
50
|
+
# @required @sensitive
|
|
51
|
+
CERTIFICATE_PEM=
|
|
52
|
+
|
|
53
|
+
# @required @sensitive
|
|
54
|
+
PRIVATE_KEY_PEM=
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches: ["main"]
|
|
7
|
+
paths:
|
|
8
|
+
- pilot.yaml
|
|
9
|
+
- deploy/**
|
|
10
|
+
- .github/workflows/build.yml
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
packages: write
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v5
|
|
21
|
+
with:
|
|
22
|
+
ref: ${{ github.sha }}
|
|
23
|
+
|
|
24
|
+
- name: Extract image metadata inputs
|
|
25
|
+
run: |
|
|
26
|
+
BRAIN_VERSION="$(grep '^brainVersion:' pilot.yaml | sed 's/^brainVersion:[[:space:]]*//' | tr -d '"' | tr -d "'")"
|
|
27
|
+
if [ -z "$BRAIN_VERSION" ]; then
|
|
28
|
+
echo "Missing brainVersion in pilot.yaml" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
echo "BRAIN_VERSION=$BRAIN_VERSION" >> "$GITHUB_ENV"
|
|
32
|
+
echo "IMAGE_REPOSITORY=ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}" >> "$GITHUB_ENV"
|
|
33
|
+
|
|
34
|
+
- name: Set up Docker Buildx
|
|
35
|
+
uses: docker/setup-buildx-action@v3
|
|
36
|
+
|
|
37
|
+
- name: Extract image metadata
|
|
38
|
+
id: meta
|
|
39
|
+
uses: docker/metadata-action@v5
|
|
40
|
+
with:
|
|
41
|
+
images: ${{ env.IMAGE_REPOSITORY }}
|
|
42
|
+
tags: |
|
|
43
|
+
type=raw,value=brain-${{ env.BRAIN_VERSION }}
|
|
44
|
+
|
|
45
|
+
- name: Log in to GHCR
|
|
46
|
+
uses: docker/login-action@v3
|
|
47
|
+
with:
|
|
48
|
+
registry: ghcr.io
|
|
49
|
+
username: ${{ github.actor }}
|
|
50
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
+
|
|
52
|
+
- name: Build and push image
|
|
53
|
+
uses: docker/build-push-action@v6
|
|
54
|
+
with:
|
|
55
|
+
context: .
|
|
56
|
+
file: deploy/Dockerfile
|
|
57
|
+
push: true
|
|
58
|
+
build-args: |
|
|
59
|
+
BRAIN_VERSION=${{ env.BRAIN_VERSION }}
|
|
60
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
61
|
+
labels: |
|
|
62
|
+
${{ steps.meta.outputs.labels }}
|
|
63
|
+
service=rover
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
handle:
|
|
7
|
+
description: User handle
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
push:
|
|
11
|
+
branches: ["main"]
|
|
12
|
+
paths:
|
|
13
|
+
- users/*/.env
|
|
14
|
+
- users/*/brain.yaml
|
|
15
|
+
- deploy/**
|
|
16
|
+
- .github/workflows/deploy.yml
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: write
|
|
20
|
+
packages: read
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
resolve_handles:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
outputs:
|
|
26
|
+
handles_json: ${{ steps.resolve.outputs.handles_json }}
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v5
|
|
29
|
+
with:
|
|
30
|
+
ref: ${{ github.sha }}
|
|
31
|
+
|
|
32
|
+
- uses: oven-sh/setup-bun@v2
|
|
33
|
+
|
|
34
|
+
- name: Install operator tooling
|
|
35
|
+
run: bun install
|
|
36
|
+
|
|
37
|
+
- name: Resolve deploy handles
|
|
38
|
+
id: resolve
|
|
39
|
+
env:
|
|
40
|
+
HANDLE_INPUT: ${{ inputs.handle || '' }}
|
|
41
|
+
BEFORE_SHA: ${{ github.event.before || '' }}
|
|
42
|
+
run: bun deploy/scripts/resolve-deploy-handles.ts
|
|
43
|
+
|
|
44
|
+
no_changes:
|
|
45
|
+
needs: resolve_handles
|
|
46
|
+
if: needs.resolve_handles.outputs.handles_json == '[]'
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- name: Skip when no user configs changed
|
|
50
|
+
run: echo "No affected user configs; skipping deploy."
|
|
51
|
+
|
|
52
|
+
deploy:
|
|
53
|
+
needs: resolve_handles
|
|
54
|
+
if: needs.resolve_handles.outputs.handles_json != '[]'
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
strategy:
|
|
57
|
+
fail-fast: false
|
|
58
|
+
matrix:
|
|
59
|
+
handle: ${{ fromJson(needs.resolve_handles.outputs.handles_json) }}
|
|
60
|
+
concurrency:
|
|
61
|
+
group: deploy-${{ matrix.handle }}
|
|
62
|
+
cancel-in-progress: false
|
|
63
|
+
env:
|
|
64
|
+
HANDLE: ${{ matrix.handle }}
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v5
|
|
67
|
+
with:
|
|
68
|
+
ref: ${{ github.sha }}
|
|
69
|
+
|
|
70
|
+
- uses: oven-sh/setup-bun@v2
|
|
71
|
+
|
|
72
|
+
- name: Install operator tooling
|
|
73
|
+
run: bun install
|
|
74
|
+
|
|
75
|
+
- name: Reconcile selected user config
|
|
76
|
+
run: bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
77
|
+
|
|
78
|
+
- name: Resolve generated user config
|
|
79
|
+
id: user_config
|
|
80
|
+
run: bun deploy/scripts/resolve-user-config.ts
|
|
81
|
+
|
|
82
|
+
- name: Validate selected secrets
|
|
83
|
+
env:
|
|
84
|
+
AI_API_KEY: ${{ secrets[steps.user_config.outputs.ai_api_key_secret_name] }}
|
|
85
|
+
GIT_SYNC_TOKEN: ${{ secrets[steps.user_config.outputs.git_sync_token_secret_name] }}
|
|
86
|
+
MCP_AUTH_TOKEN: ${{ secrets[steps.user_config.outputs.mcp_auth_token_secret_name] }}
|
|
87
|
+
DISCORD_BOT_TOKEN: ${{ steps.user_config.outputs.discord_bot_token_secret_name != '' && secrets[steps.user_config.outputs.discord_bot_token_secret_name] || '' }}
|
|
88
|
+
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
89
|
+
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
90
|
+
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
91
|
+
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
92
|
+
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
93
|
+
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
94
|
+
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
95
|
+
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
96
|
+
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
97
|
+
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
98
|
+
run: bun deploy/scripts/validate-secrets.ts
|
|
99
|
+
|
|
100
|
+
- name: Log in to GHCR
|
|
101
|
+
uses: docker/login-action@v3
|
|
102
|
+
with:
|
|
103
|
+
registry: ghcr.io
|
|
104
|
+
username: ${{ github.actor }}
|
|
105
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
106
|
+
|
|
107
|
+
- name: Wait for shared image tag
|
|
108
|
+
run: |
|
|
109
|
+
IMAGE_TAG="${{ steps.user_config.outputs.image_repository }}:brain-${{ steps.user_config.outputs.brain_version }}"
|
|
110
|
+
for attempt in $(seq 1 24); do
|
|
111
|
+
if docker manifest inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
echo "Shared image tag not ready yet ($attempt/24): $IMAGE_TAG"
|
|
115
|
+
sleep 10
|
|
116
|
+
done
|
|
117
|
+
echo "Timed out waiting for $IMAGE_TAG" >&2
|
|
118
|
+
exit 1
|
|
119
|
+
|
|
120
|
+
- name: Install Kamal
|
|
121
|
+
run: |
|
|
122
|
+
gem install --user-install kamal
|
|
123
|
+
ruby -r rubygems -e 'puts Gem.user_dir + "/bin"' >> "$GITHUB_PATH"
|
|
124
|
+
|
|
125
|
+
- name: Write Kamal SSH key
|
|
126
|
+
env:
|
|
127
|
+
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
128
|
+
run: bun deploy/scripts/write-ssh-key.ts
|
|
129
|
+
|
|
130
|
+
- name: Configure SSH client
|
|
131
|
+
run: |
|
|
132
|
+
mkdir -p ~/.ssh
|
|
133
|
+
cat > ~/.ssh/config <<'EOF'
|
|
134
|
+
Host *
|
|
135
|
+
IdentityFile ~/.ssh/id_ed25519
|
|
136
|
+
IdentitiesOnly yes
|
|
137
|
+
BatchMode yes
|
|
138
|
+
StrictHostKeyChecking no
|
|
139
|
+
UserKnownHostsFile /dev/null
|
|
140
|
+
EOF
|
|
141
|
+
chmod 600 ~/.ssh/config
|
|
142
|
+
|
|
143
|
+
- name: Write .kamal/secrets
|
|
144
|
+
env:
|
|
145
|
+
AI_API_KEY: ${{ secrets[steps.user_config.outputs.ai_api_key_secret_name] }}
|
|
146
|
+
GIT_SYNC_TOKEN: ${{ secrets[steps.user_config.outputs.git_sync_token_secret_name] }}
|
|
147
|
+
MCP_AUTH_TOKEN: ${{ secrets[steps.user_config.outputs.mcp_auth_token_secret_name] }}
|
|
148
|
+
DISCORD_BOT_TOKEN: ${{ steps.user_config.outputs.discord_bot_token_secret_name != '' && secrets[steps.user_config.outputs.discord_bot_token_secret_name] || '' }}
|
|
149
|
+
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
150
|
+
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
151
|
+
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
152
|
+
run: bun deploy/scripts/write-kamal-secrets.ts
|
|
153
|
+
|
|
154
|
+
- name: Provision server
|
|
155
|
+
id: provision
|
|
156
|
+
env:
|
|
157
|
+
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
158
|
+
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
159
|
+
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
160
|
+
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
161
|
+
INSTANCE_NAME: ${{ steps.user_config.outputs.instance_name }}
|
|
162
|
+
run: bun deploy/scripts/provision-server.ts
|
|
163
|
+
|
|
164
|
+
- name: Update Cloudflare DNS
|
|
165
|
+
env:
|
|
166
|
+
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
167
|
+
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
168
|
+
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
169
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
170
|
+
run: bun deploy/scripts/update-dns.ts
|
|
171
|
+
|
|
172
|
+
- name: Validate SSH key
|
|
173
|
+
run: ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null
|
|
174
|
+
|
|
175
|
+
- name: Wait for SSH access
|
|
176
|
+
env:
|
|
177
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
178
|
+
run: |
|
|
179
|
+
SSH_USER="$(ruby -e 'require "yaml"; config = YAML.load_file("deploy/kamal/deploy.yml") || {}; puts(config.dig("ssh", "user") || "root")')"
|
|
180
|
+
for attempt in $(seq 1 18); do
|
|
181
|
+
if ssh "$SSH_USER@$SERVER_IP" true >/dev/null 2>&1; then
|
|
182
|
+
exit 0
|
|
183
|
+
fi
|
|
184
|
+
echo "SSH not ready yet (attempt $attempt/18); retrying in 5s..."
|
|
185
|
+
sleep 5
|
|
186
|
+
done
|
|
187
|
+
echo "SSH never became ready for $SSH_USER@$SERVER_IP" >&2
|
|
188
|
+
exit 1
|
|
189
|
+
|
|
190
|
+
- name: Deploy
|
|
191
|
+
env:
|
|
192
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
193
|
+
VERSION: brain-${{ steps.user_config.outputs.brain_version }}
|
|
194
|
+
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
195
|
+
|
|
196
|
+
- name: Verify origin TLS
|
|
197
|
+
env:
|
|
198
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
199
|
+
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
200
|
+
run: curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
|
|
201
|
+
|
|
202
|
+
- name: Upload generated config
|
|
203
|
+
uses: actions/upload-artifact@v4
|
|
204
|
+
with:
|
|
205
|
+
name: generated-${{ matrix.handle }}-config
|
|
206
|
+
path: |
|
|
207
|
+
users/${{ matrix.handle }}/brain.yaml
|
|
208
|
+
users/${{ matrix.handle }}/.env
|
|
209
|
+
|
|
210
|
+
- name: Dump remote proxy diagnostics
|
|
211
|
+
if: failure()
|
|
212
|
+
env:
|
|
213
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
214
|
+
run: |
|
|
215
|
+
if [ -z "$SERVER_IP" ]; then
|
|
216
|
+
echo "No server IP; skipping diagnostics"
|
|
217
|
+
exit 0
|
|
218
|
+
fi
|
|
219
|
+
SSH_USER="$(ruby -e 'require "yaml"; config = YAML.load_file("deploy/kamal/deploy.yml") || {}; puts(config.dig("ssh", "user") || "root")')"
|
|
220
|
+
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SSH_USER@$SERVER_IP" '
|
|
221
|
+
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
|
|
222
|
+
echo "--- kamal-proxy logs ---"
|
|
223
|
+
docker logs kamal-proxy --tail 200 || true
|
|
224
|
+
echo "--- kamal-proxy inspect ---"
|
|
225
|
+
docker inspect kamal-proxy || true
|
|
226
|
+
'
|
|
227
|
+
|
|
228
|
+
finalize_generated_config:
|
|
229
|
+
needs: [resolve_handles, deploy]
|
|
230
|
+
if: needs.resolve_handles.outputs.handles_json != '[]'
|
|
231
|
+
runs-on: ubuntu-latest
|
|
232
|
+
steps:
|
|
233
|
+
- uses: actions/checkout@v5
|
|
234
|
+
with:
|
|
235
|
+
ref: ${{ github.sha }}
|
|
236
|
+
|
|
237
|
+
- uses: oven-sh/setup-bun@v2
|
|
238
|
+
|
|
239
|
+
- name: Install operator tooling
|
|
240
|
+
run: bun install
|
|
241
|
+
|
|
242
|
+
- name: Finalize generated config
|
|
243
|
+
uses: actions/download-artifact@v4
|
|
244
|
+
with:
|
|
245
|
+
pattern: generated-*-config
|
|
246
|
+
merge-multiple: true
|
|
247
|
+
path: .
|
|
248
|
+
|
|
249
|
+
- name: Regenerate users table
|
|
250
|
+
run: bunx brains-ops render "$GITHUB_WORKSPACE"
|
|
251
|
+
|
|
252
|
+
- name: Commit generated config
|
|
253
|
+
run: |
|
|
254
|
+
if git diff --quiet -- users views; then
|
|
255
|
+
exit 0
|
|
256
|
+
fi
|
|
257
|
+
git config user.name "github-actions[bot]"
|
|
258
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
259
|
+
git add users views
|
|
260
|
+
git commit -m "chore(ops): reconcile generated config"
|
|
261
|
+
git push
|