@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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Reconcile
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches: ["main"]
|
|
7
|
+
paths:
|
|
8
|
+
- pilot.yaml
|
|
9
|
+
- cohorts/**
|
|
10
|
+
- users/*.yaml
|
|
11
|
+
- .github/workflows/reconcile.yml
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: reconcile
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
reconcile:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v5
|
|
25
|
+
with:
|
|
26
|
+
ref: ${{ github.sha }}
|
|
27
|
+
|
|
28
|
+
- uses: oven-sh/setup-bun@v2
|
|
29
|
+
|
|
30
|
+
- name: Install operator tooling
|
|
31
|
+
run: bun install
|
|
32
|
+
|
|
33
|
+
- name: Reconcile generated pilot outputs
|
|
34
|
+
run: bunx brains-ops reconcile-all "$GITHUB_WORKSPACE"
|
|
35
|
+
|
|
36
|
+
- name: Commit generated outputs
|
|
37
|
+
run: |
|
|
38
|
+
if git diff --quiet -- views users; then
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
git config user.name "github-actions[bot]"
|
|
42
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
43
|
+
git add views users
|
|
44
|
+
git commit -m "chore(ops): reconcile pilot outputs"
|
|
45
|
+
git push
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
BRAIN_FILE="${BRAIN_YAML_PATH:-brain.yaml}"
|
|
5
|
+
SSH_USER="$(ruby -e 'require "yaml"; config = YAML.load_file("deploy/kamal/deploy.yml") || {}; puts(config.dig("ssh", "user") || "root")')"
|
|
6
|
+
IFS=',' read -ra HOSTS <<< "$KAMAL_HOSTS"
|
|
7
|
+
for host in "${HOSTS[@]}"; do
|
|
8
|
+
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BRAIN_FILE" "${SSH_USER}@${host}:/opt/brain.yaml"
|
|
9
|
+
done
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# rover-pilot
|
|
2
|
+
|
|
3
|
+
Private desired-state repo for the rover pilot.
|
|
4
|
+
|
|
5
|
+
This is a single operator-owned repo. Pilot users do not get their own brain repos.
|
|
6
|
+
Per-user deploy config lives under `users/<handle>/`, while content stays in per-user content repos.
|
|
7
|
+
|
|
8
|
+
## Operator tooling
|
|
9
|
+
|
|
10
|
+
This repo pins `@rizom/ops` in `package.json`.
|
|
11
|
+
|
|
12
|
+
Install it with:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
bun install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then run commands with:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
bunx brains-ops <command>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The repo also checks in its deploy contract:
|
|
25
|
+
|
|
26
|
+
- `.env.schema`
|
|
27
|
+
- `deploy/kamal/deploy.yml`
|
|
28
|
+
- `deploy/scripts/`
|
|
29
|
+
- `.github/workflows/*`
|
|
30
|
+
|
|
31
|
+
`.env.schema` is the single source of truth for required and sensitive deploy vars.
|
|
32
|
+
The shared pilot image tag is `brain-${brainVersion}` end to end.
|
|
33
|
+
When `pilot.yaml.brainVersion` changes and you push, CI rebuilds the shared tag, refreshes generated user env files, and redeploys affected users.
|
|
34
|
+
When a push changes only deploy contract files, CI prints `No affected user configs; skipping deploy.` and stops before Kamal.
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
- `brains-ops init <repo>`
|
|
39
|
+
- `brains-ops render <repo>`
|
|
40
|
+
- `brains-ops onboard <repo> <handle>`
|
|
41
|
+
- `brains-ops reconcile-cohort <repo> <cohort>`
|
|
42
|
+
- `brains-ops reconcile-all <repo>`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ARG BUN_VERSION=1.3.10
|
|
2
|
+
FROM oven/bun:${BUN_VERSION}-slim
|
|
3
|
+
|
|
4
|
+
ARG BRAIN_VERSION
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
RUN test -n "$BRAIN_VERSION" \
|
|
8
|
+
&& printf '{"name":"rover-pilot-runtime","private":true}\n' > package.json \
|
|
9
|
+
&& bun add @rizom/brain@$BRAIN_VERSION
|
|
10
|
+
|
|
11
|
+
ENV XDG_DATA_HOME=/data
|
|
12
|
+
ENV XDG_CONFIG_HOME=/config
|
|
13
|
+
RUN mkdir -p /app/brain-data /data /config && chmod -R 777 /app/brain-data /data /config
|
|
14
|
+
|
|
15
|
+
CMD ["sh", "-c", "exec ./node_modules/.bin/brain start"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
service: rover
|
|
2
|
+
image: <%= ENV['IMAGE_REPOSITORY'] %>
|
|
3
|
+
|
|
4
|
+
servers:
|
|
5
|
+
mcp:
|
|
6
|
+
hosts:
|
|
7
|
+
- <%= ENV['SERVER_IP'] %>
|
|
8
|
+
|
|
9
|
+
proxy:
|
|
10
|
+
ssl:
|
|
11
|
+
certificate_pem: CERTIFICATE_PEM
|
|
12
|
+
private_key_pem: PRIVATE_KEY_PEM
|
|
13
|
+
hosts:
|
|
14
|
+
- <%= ENV['BRAIN_DOMAIN'] %>
|
|
15
|
+
app_port: 3333
|
|
16
|
+
healthcheck:
|
|
17
|
+
path: /health
|
|
18
|
+
|
|
19
|
+
registry:
|
|
20
|
+
server: ghcr.io
|
|
21
|
+
username: <%= ENV['REGISTRY_USERNAME'] %>
|
|
22
|
+
password:
|
|
23
|
+
- KAMAL_REGISTRY_PASSWORD
|
|
24
|
+
|
|
25
|
+
builder:
|
|
26
|
+
arch: amd64
|
|
27
|
+
|
|
28
|
+
env:
|
|
29
|
+
clear:
|
|
30
|
+
NODE_ENV: production
|
|
31
|
+
secret:
|
|
32
|
+
- AI_API_KEY
|
|
33
|
+
- GIT_SYNC_TOKEN
|
|
34
|
+
- MCP_AUTH_TOKEN
|
|
35
|
+
- DISCORD_BOT_TOKEN
|
|
36
|
+
|
|
37
|
+
volumes:
|
|
38
|
+
- /opt/brain-data:/app/brain-data
|
|
39
|
+
- /opt/brain.yaml:/app/brain.yaml
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { requireEnv, writeGitHubOutput } from "./helpers";
|
|
3
|
+
|
|
4
|
+
const eventName = requireEnv("GITHUB_EVENT_NAME");
|
|
5
|
+
|
|
6
|
+
if (eventName === "workflow_dispatch") {
|
|
7
|
+
const handle = requireEnv("HANDLE_INPUT");
|
|
8
|
+
writeGitHubOutput("handles_json", JSON.stringify([handle]));
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (eventName !== "push") {
|
|
13
|
+
throw new Error(`Unsupported GITHUB_EVENT_NAME: ${eventName}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const beforeSha = requireEnv("BEFORE_SHA");
|
|
17
|
+
const currentSha = requireEnv("GITHUB_SHA");
|
|
18
|
+
|
|
19
|
+
if (!isUsableGitRevision(beforeSha) || !isUsableGitRevision(currentSha)) {
|
|
20
|
+
writeGitHubOutput("handles_json", JSON.stringify([]));
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const diffOutput = execFileSync(
|
|
25
|
+
"git",
|
|
26
|
+
["diff", "--name-only", beforeSha, currentSha],
|
|
27
|
+
{ encoding: "utf8" },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const handles = [
|
|
31
|
+
...new Set(
|
|
32
|
+
diffOutput
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.map((path) => {
|
|
35
|
+
const match = path.match(/^users\/([^/]+)\/(?:\.env|brain\.yaml)$/);
|
|
36
|
+
return match?.[1] ?? null;
|
|
37
|
+
})
|
|
38
|
+
.filter((handle): handle is string => handle !== null)
|
|
39
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
40
|
+
),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
writeGitHubOutput("handles_json", JSON.stringify(handles));
|
|
44
|
+
|
|
45
|
+
function isUsableGitRevision(revision: string): boolean {
|
|
46
|
+
if (!revision || /^0+$/.test(revision)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
execFileSync("git", ["rev-parse", "--verify", revision], {
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { parseEnvFile, requireEnv, writeGitHubOutput } from "./helpers";
|
|
3
|
+
|
|
4
|
+
const handle = requireEnv("HANDLE");
|
|
5
|
+
const envPath = `users/${handle}/.env`;
|
|
6
|
+
const brainYamlPath = `users/${handle}/brain.yaml`;
|
|
7
|
+
|
|
8
|
+
const envEntries = parseEnvFile(envPath);
|
|
9
|
+
const repository = process.env["GITHUB_REPOSITORY"] ?? "";
|
|
10
|
+
const repositoryOwner = repository.split("/")[0] ?? "";
|
|
11
|
+
|
|
12
|
+
const brainYaml = readFileSync(brainYamlPath, "utf8");
|
|
13
|
+
const domainMatch = brainYaml.match(/^domain:\s*(.+)$/m);
|
|
14
|
+
const brainDomain = domainMatch?.[1]?.trim().replace(/^['"]|['"]$/g, "") ?? "";
|
|
15
|
+
|
|
16
|
+
if (!brainDomain) {
|
|
17
|
+
throw new Error(`Missing domain in ${brainYamlPath}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const outputs: Record<string, string> = {
|
|
21
|
+
brain_version: envEntries["BRAIN_VERSION"] ?? "",
|
|
22
|
+
ai_api_key_secret_name: envEntries["AI_API_KEY_SECRET"] ?? "",
|
|
23
|
+
git_sync_token_secret_name: envEntries["GIT_SYNC_TOKEN_SECRET"] ?? "",
|
|
24
|
+
mcp_auth_token_secret_name: envEntries["MCP_AUTH_TOKEN_SECRET"] ?? "",
|
|
25
|
+
discord_bot_token_secret_name: envEntries["DISCORD_BOT_TOKEN_SECRET"] ?? "",
|
|
26
|
+
content_repo: envEntries["CONTENT_REPO"] ?? "",
|
|
27
|
+
brain_domain: brainDomain,
|
|
28
|
+
brain_yaml_path: brainYamlPath,
|
|
29
|
+
instance_name: `rover-${handle}`,
|
|
30
|
+
image_repository: `ghcr.io/${repository}`,
|
|
31
|
+
registry_username: repositoryOwner,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const required = [
|
|
35
|
+
"brain_version",
|
|
36
|
+
"ai_api_key_secret_name",
|
|
37
|
+
"git_sync_token_secret_name",
|
|
38
|
+
"mcp_auth_token_secret_name",
|
|
39
|
+
"registry_username",
|
|
40
|
+
];
|
|
41
|
+
for (const key of required) {
|
|
42
|
+
if (!outputs[key]) {
|
|
43
|
+
throw new Error(`Missing ${key} (derived from ${envPath})`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const [key, value] of Object.entries(outputs)) {
|
|
48
|
+
writeGitHubOutput(key, value);
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Onboarding Checklist
|
|
2
|
+
|
|
3
|
+
1. Run `bun install` so the repo uses its pinned `@rizom/ops` version.
|
|
4
|
+
2. Fill in `pilot.yaml`.
|
|
5
|
+
3. Add or edit `users/<handle>.yaml`.
|
|
6
|
+
4. Add the user to a cohort in `cohorts/*.yaml`.
|
|
7
|
+
5. Run `bunx brains-ops render <repo>`.
|
|
8
|
+
6. Run `bunx brains-ops onboard <repo> <handle>`.
|
|
9
|
+
7. For fleet upgrades, edit `pilot.yaml.brainVersion` and push once; CI rebuilds the shared image tag, refreshes generated user env files, and redeploys affected users.
|
|
10
|
+
8. Hand the MCP connection details to the user.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Operator Playbook
|
|
2
|
+
|
|
3
|
+
## Deploy contract files
|
|
4
|
+
|
|
5
|
+
Treat these as checked-in deploy artifacts in the pilot repo:
|
|
6
|
+
|
|
7
|
+
- `.env.schema`
|
|
8
|
+
- `deploy/kamal/deploy.yml`
|
|
9
|
+
- `deploy/scripts/`
|
|
10
|
+
- `.github/workflows/build.yml`
|
|
11
|
+
- `.github/workflows/deploy.yml`
|
|
12
|
+
- `.github/workflows/reconcile.yml`
|
|
13
|
+
|
|
14
|
+
`.env.schema` is the single source of truth for required and sensitive deploy vars.
|
|
15
|
+
The deploy scripts and workflows should read from that contract instead of inventing a second list.
|
|
16
|
+
|
|
17
|
+
The shared pilot image tag is `brain-${brainVersion}`:
|
|
18
|
+
|
|
19
|
+
- build publishes `brain-${brainVersion}`
|
|
20
|
+
- generated `users/<handle>/.env` carries `BRAIN_VERSION=<brainVersion>`
|
|
21
|
+
- deploy sets `VERSION=brain-${brainVersion}`
|
|
22
|
+
|
|
23
|
+
## Version bump flow
|
|
24
|
+
|
|
25
|
+
When `pilot.yaml.brainVersion` changes and you push:
|
|
26
|
+
|
|
27
|
+
1. build publishes the new shared image tag
|
|
28
|
+
2. reconcile refreshes generated `users/<handle>/.env`
|
|
29
|
+
3. deploy runs for handles whose generated config changed
|
|
30
|
+
4. generated file commits happen once in a final aggregation step after the deploy matrix finishes
|
|
31
|
+
|
|
32
|
+
When a push changes only deploy contract files and no generated `users/<handle>/.env` or `users/<handle>/brain.yaml` files, the deploy workflow exits through its explicit no-op path and prints `No affected user configs; skipping deploy.`
|
|
33
|
+
|
|
34
|
+
They are scaffolded from `@rizom/ops`, then versioned in this repo like any other deploy contract.
|
|
35
|
+
|
|
36
|
+
## Upgrading operator behavior
|
|
37
|
+
|
|
38
|
+
When `@rizom/ops` changes the scaffolded deploy contract:
|
|
39
|
+
|
|
40
|
+
1. bump `@rizom/ops` in `package.json`
|
|
41
|
+
2. rerun the relevant scaffold/reconcile flow
|
|
42
|
+
3. review the resulting changes to `.env.schema`, `deploy/scripts/`, and workflows in git
|
|
43
|
+
4. commit the updated deploy artifacts together
|
|
44
|
+
|
|
45
|
+
## Recovery notes
|
|
46
|
+
|
|
47
|
+
Document known failure modes, recovery steps, and operator notes here.
|