@rizom/ops 0.2.0-alpha.3 → 0.2.0-alpha.300
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 +44 -3
- package/dist/age-key-bootstrap.d.ts +17 -0
- package/dist/brains-ops.js +453 -173
- package/dist/cert-bootstrap.d.ts +24 -0
- package/dist/content-repo-ref.d.ts +10 -0
- package/dist/content-repo.d.ts +13 -0
- package/dist/default-user-runner.d.ts +1 -1
- package/dist/deploy.js +99 -166
- package/dist/directory-sync-stress-system.d.ts +75 -0
- package/dist/directory-sync-stress.d.ts +107 -0
- package/dist/entries/deploy.d.ts +3 -2
- package/dist/images.d.ts +79 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +523 -152
- package/dist/legacy-pilot-migration.d.ts +11 -0
- package/dist/load-registry.d.ts +74 -7
- package/dist/observed-status.d.ts +12 -0
- package/dist/onboard-user.d.ts +2 -2
- package/dist/origin-ca.d.ts +1 -0
- package/dist/parse-args.d.ts +2 -9
- package/dist/preview-domain.d.ts +9 -0
- package/dist/push-secrets.d.ts +2 -0
- package/dist/push-target.d.ts +1 -0
- package/dist/reconcile-all.d.ts +2 -2
- package/dist/reconcile-cohort.d.ts +2 -2
- package/dist/reconcile-dry-run.d.ts +9 -0
- package/dist/reconcile-lib.d.ts +4 -2
- package/dist/run-command.d.ts +19 -4
- package/dist/run-subprocess.d.ts +1 -0
- package/dist/schema.d.ts +119 -70
- package/dist/secrets-encrypt.d.ts +26 -0
- package/dist/secrets-push.d.ts +2 -5
- package/dist/ssh-key-bootstrap.d.ts +1 -0
- package/dist/stage-legacy-crossover.d.ts +20 -0
- package/dist/stress-command.d.ts +14 -0
- package/dist/stress-git-checkout.d.ts +21 -0
- package/dist/stress-health-monitor.d.ts +46 -0
- package/dist/upgrade.d.ts +10 -0
- package/dist/user-add.d.ts +15 -0
- package/dist/user-runner.d.ts +5 -0
- package/dist/verify-user.d.ts +22 -0
- package/package.json +47 -40
- package/templates/rover-pilot/.env.schema +28 -2
- package/templates/rover-pilot/.github/actions/varlock-env/action.yml +47 -0
- package/templates/rover-pilot/.github/workflows/build.yml +71 -17
- package/templates/rover-pilot/.github/workflows/deploy.yml +110 -45
- package/templates/rover-pilot/.github/workflows/directory-sync-stress.yml +119 -0
- package/templates/rover-pilot/.github/workflows/reconcile.yml +23 -3
- package/templates/rover-pilot/.github/workflows/upgrade.yml +104 -0
- package/templates/rover-pilot/README.md +20 -7
- package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +139 -0
- package/templates/rover-pilot/deploy/scripts/helpers.ts +3 -0
- package/templates/rover-pilot/deploy/scripts/install-health-watchdog.ts +144 -0
- package/templates/rover-pilot/deploy/scripts/provision-server.ts +1 -1
- package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +15 -4
- package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
- package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +49 -14
- package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +183 -0
- package/templates/rover-pilot/deploy/scripts/update-dns.ts +14 -4
- package/templates/rover-pilot/deploy/scripts/validate-secrets.ts +12 -1
- package/templates/rover-pilot/docs/canonical-crossover-record.md +104 -0
- package/templates/rover-pilot/docs/onboarding-checklist.md +39 -8
- package/templates/rover-pilot/docs/operator-playbook.md +298 -9
- package/templates/rover-pilot/docs/user-onboarding.md +90 -0
- package/templates/rover-pilot/package.json +3 -0
- package/templates/rover-pilot/pilot.yaml +5 -3
- package/templates/rover-pilot/users/alice.yaml +5 -1
- package/dist/user-secret-names.d.ts +0 -6
- package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
- package/templates/rover-pilot/deploy/Dockerfile +0 -15
- package/templates/rover-pilot/deploy/kamal/deploy.yml +0 -39
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
operationalRetryAttempts?: number;
|
|
7
|
+
operationalRetryDelayMs?: number;
|
|
8
|
+
sleep?: (delayMs: number) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface FailedCheck {
|
|
11
|
+
name: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
export interface VerifyPilotUserResult {
|
|
15
|
+
handle: string;
|
|
16
|
+
bundles: ResolvedUser["bundles"];
|
|
17
|
+
domain: string;
|
|
18
|
+
contentRepo: string;
|
|
19
|
+
checks: string[];
|
|
20
|
+
failedChecks: FailedCheck[];
|
|
21
|
+
}
|
|
22
|
+
export declare function verifyPilotUser(rootDir: string, handle: string, options?: VerifyPilotUserOptions): Promise<VerifyPilotUserResult>;
|
package/package.json
CHANGED
|
@@ -1,65 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rizom/ops",
|
|
3
|
+
"version": "0.2.0-alpha.300",
|
|
3
4
|
"description": "Operator CLI for managing private brain fleet registry repos",
|
|
4
|
-
"
|
|
5
|
-
"
|
|
5
|
+
"keywords": [
|
|
6
|
+
"brains",
|
|
7
|
+
"ops",
|
|
8
|
+
"cli",
|
|
9
|
+
"deploy",
|
|
10
|
+
"fleet",
|
|
11
|
+
"operator"
|
|
12
|
+
],
|
|
13
|
+
"license": "AGPL-3.0-only",
|
|
14
|
+
"author": "Yeehaa <yeehaa@rizom.ai> (https://rizom.ai)",
|
|
15
|
+
"bin": {
|
|
16
|
+
"brains-ops": "./dist/brains-ops.js"
|
|
17
|
+
},
|
|
18
|
+
"bugs": "https://github.com/rizom-ai/brains/issues",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"age-encryption": "^0.3.0",
|
|
21
|
+
"yaml": "^2.9.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@brains/deploy-support": "workspace:*",
|
|
25
|
+
"@brains/eslint-config": "workspace:*",
|
|
26
|
+
"@brains/profile": "workspace:*",
|
|
27
|
+
"@brains/site-composition": "workspace:*",
|
|
28
|
+
"@brains/typescript-config": "workspace:*",
|
|
29
|
+
"@brains/utils": "workspace:*",
|
|
30
|
+
"@types/bun": "^1.3.14",
|
|
31
|
+
"typescript": "^7.0.2"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"bun": ">=1.3.3"
|
|
6
35
|
},
|
|
7
|
-
"version": "0.2.0-alpha.3",
|
|
8
|
-
"type": "module",
|
|
9
36
|
"exports": {
|
|
10
37
|
".": {
|
|
11
38
|
"types": "./dist/index.d.ts",
|
|
12
39
|
"import": "./dist/index.js"
|
|
13
40
|
},
|
|
14
41
|
"./deploy": {
|
|
15
|
-
"bun": "./dist/deploy.js",
|
|
16
42
|
"types": "./dist/deploy.d.ts",
|
|
17
|
-
"import": "./dist/deploy.js"
|
|
43
|
+
"import": "./dist/deploy.js",
|
|
44
|
+
"bun": "./dist/deploy.js"
|
|
18
45
|
}
|
|
19
46
|
},
|
|
20
|
-
"main": "./dist/index.js",
|
|
21
|
-
"types": "./dist/index.d.ts",
|
|
22
|
-
"bin": {
|
|
23
|
-
"brains-ops": "./dist/brains-ops.js"
|
|
24
|
-
},
|
|
25
47
|
"files": [
|
|
26
48
|
"dist",
|
|
27
49
|
"templates"
|
|
28
50
|
],
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
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"
|
|
51
|
+
"homepage": "https://github.com/rizom-ai/brains/tree/main/packages/brains-ops#readme",
|
|
52
|
+
"main": "./dist/index.js",
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
44
55
|
},
|
|
45
56
|
"repository": {
|
|
46
57
|
"type": "git",
|
|
47
58
|
"url": "https://github.com/rizom-ai/brains.git",
|
|
48
59
|
"directory": "packages/brains-ops"
|
|
49
60
|
},
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "bun scripts/build.ts",
|
|
63
|
+
"lint": "eslint . --max-warnings 0",
|
|
64
|
+
"lint:fix": "eslint . --max-warnings 0 --fix",
|
|
65
|
+
"prepublishOnly": "bun scripts/build.ts",
|
|
66
|
+
"test": "bun run build && bun test --timeout 20000",
|
|
67
|
+
"test:smoke": "bun run build && RUN_SMOKE_TESTS=1 bun test --timeout 60000",
|
|
68
|
+
"typecheck": "tsc --noEmit"
|
|
56
69
|
},
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"ops",
|
|
60
|
-
"cli",
|
|
61
|
-
"deploy",
|
|
62
|
-
"fleet",
|
|
63
|
-
"operator"
|
|
64
|
-
]
|
|
70
|
+
"type": "module",
|
|
71
|
+
"types": "./dist/index.d.ts"
|
|
65
72
|
}
|
|
@@ -4,21 +4,47 @@
|
|
|
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
|
|
|
14
|
-
#
|
|
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.
|
|
15
20
|
# @required @sensitive
|
|
16
|
-
|
|
21
|
+
CONTENT_REPO_ADMIN_TOKEN=
|
|
17
22
|
|
|
18
23
|
# Discord (optional, per-user)
|
|
24
|
+
# Comes from the decrypted users/<handle>.secrets.yaml.age file when enabled.
|
|
19
25
|
# @sensitive
|
|
20
26
|
DISCORD_BOT_TOKEN=
|
|
21
27
|
|
|
28
|
+
# Stored with the per-user Discord credentials and injected at deploy time.
|
|
29
|
+
# @sensitive
|
|
30
|
+
DISCORD_PUBLIC_KEY=
|
|
31
|
+
|
|
32
|
+
# @sensitive
|
|
33
|
+
DISCORD_APPLICATION_ID=
|
|
34
|
+
|
|
35
|
+
# AT Protocol publishing/discovery (optional, per-user)
|
|
36
|
+
# Comes from the decrypted users/<handle>.secrets.yaml.age file when configured.
|
|
37
|
+
# @sensitive
|
|
38
|
+
ATPROTO_APP_PASSWORD=
|
|
39
|
+
|
|
40
|
+
# Setup email delivery (optional, shared)
|
|
41
|
+
# Used when a user file declares setup.delivery: email.
|
|
42
|
+
# @sensitive
|
|
43
|
+
SETUP_EMAIL_API_KEY=
|
|
44
|
+
|
|
45
|
+
# @sensitive
|
|
46
|
+
SETUP_EMAIL_FROM=
|
|
47
|
+
|
|
22
48
|
# ---- deploy/provision vars ----
|
|
23
49
|
|
|
24
50
|
# @required @sensitive
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Load Bitwarden environment through Varlock
|
|
2
|
+
description: Load, mask, and export the pilot environment from Varlock
|
|
3
|
+
|
|
4
|
+
runs:
|
|
5
|
+
using: composite
|
|
6
|
+
steps:
|
|
7
|
+
- name: Load shared environment
|
|
8
|
+
shell: bash
|
|
9
|
+
run: |
|
|
10
|
+
for attempt in 1 2 3; do
|
|
11
|
+
if bunx varlock@1.1.0 load --path .env.schema --format json --compact > /tmp/varlock-env.json; then
|
|
12
|
+
break
|
|
13
|
+
fi
|
|
14
|
+
if [ "$attempt" = "3" ]; then
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
echo "Varlock load failed (attempt $attempt/3); retrying in 5s..."
|
|
18
|
+
sleep 5
|
|
19
|
+
done
|
|
20
|
+
node <<'NODE'
|
|
21
|
+
import { appendFileSync, readFileSync, rmSync } from "node:fs";
|
|
22
|
+
const env = JSON.parse(readFileSync("/tmp/varlock-env.json", "utf8"));
|
|
23
|
+
const output = process.env.GITHUB_ENV;
|
|
24
|
+
if (!output) throw new Error("Missing GITHUB_ENV");
|
|
25
|
+
const newline = "\n";
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for (const [key, value] of Object.entries(env)) {
|
|
28
|
+
if (key === "BWS_ACCESS_TOKEN" || value === null || value === undefined) continue;
|
|
29
|
+
const text = String(value).replace(/\r\n/g, newline);
|
|
30
|
+
const escapedMask = text
|
|
31
|
+
.replace(/%/g, "%25")
|
|
32
|
+
.replace(/\r/g, "%0D")
|
|
33
|
+
.replace(/\n/g, "%0A");
|
|
34
|
+
if (escapedMask.length > 0) {
|
|
35
|
+
process.stdout.write(`::add-mask::${escapedMask}${newline}`);
|
|
36
|
+
}
|
|
37
|
+
if (text.includes(newline)) {
|
|
38
|
+
let delimiter = `VARLOCK_${key}_EOF`;
|
|
39
|
+
while (text.includes(delimiter)) delimiter += "_X";
|
|
40
|
+
chunks.push(`${key}<<${delimiter}${newline}${text}${newline}${delimiter}`);
|
|
41
|
+
} else {
|
|
42
|
+
chunks.push(`${key}=${text}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
appendFileSync(output, `${chunks.join(newline)}${newline}`);
|
|
46
|
+
rmSync("/tmp/varlock-env.json", { force: true });
|
|
47
|
+
NODE
|
|
@@ -2,10 +2,29 @@ name: Build
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
brain_version:
|
|
7
|
+
description: Force one explicit build at this brain version (skips the registry-derived resolve)
|
|
8
|
+
required: false
|
|
9
|
+
type: string
|
|
10
|
+
site_packages:
|
|
11
|
+
description: 'Space-separated site packages for a per-instance site image (e.g. "@rizom/site-rizom-ai@0.2.0-alpha.148"). Empty builds the default fleet image.'
|
|
12
|
+
required: false
|
|
13
|
+
type: string
|
|
14
|
+
overwrite:
|
|
15
|
+
description: Rebuild an existing tag in place. Published tags are immutable; confirm only when replacing a tag whose containers were never deployed.
|
|
16
|
+
required: false
|
|
17
|
+
type: boolean
|
|
18
|
+
default: false
|
|
5
19
|
push:
|
|
6
20
|
branches: ["main"]
|
|
7
21
|
paths:
|
|
22
|
+
# The image set is a pure function of the declared fleet state: a push
|
|
23
|
+
# that changes it builds exactly what it declares, so the deploy (fired
|
|
24
|
+
# off the same push via Reconcile) finds its tag ready.
|
|
8
25
|
- pilot.yaml
|
|
26
|
+
- cohorts/**
|
|
27
|
+
- users/*.yaml
|
|
9
28
|
- deploy/**
|
|
10
29
|
- .github/workflows/build.yml
|
|
11
30
|
|
|
@@ -14,49 +33,84 @@ permissions:
|
|
|
14
33
|
packages: write
|
|
15
34
|
|
|
16
35
|
jobs:
|
|
17
|
-
|
|
36
|
+
resolve:
|
|
18
37
|
runs-on: ubuntu-latest
|
|
38
|
+
outputs:
|
|
39
|
+
images_json: ${{ steps.resolve.outputs.images_json }}
|
|
19
40
|
steps:
|
|
20
41
|
- uses: actions/checkout@v5
|
|
21
42
|
with:
|
|
22
43
|
ref: ${{ github.sha }}
|
|
23
44
|
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
- uses: oven-sh/setup-bun@v2
|
|
46
|
+
|
|
47
|
+
- name: Install operator tooling
|
|
48
|
+
run: bun install
|
|
49
|
+
|
|
50
|
+
- name: Log in to GHCR
|
|
51
|
+
uses: docker/login-action@v4
|
|
52
|
+
with:
|
|
53
|
+
registry: ghcr.io
|
|
54
|
+
username: ${{ github.actor }}
|
|
55
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
56
|
+
|
|
57
|
+
- name: Resolve images to build
|
|
58
|
+
id: resolve
|
|
59
|
+
env:
|
|
60
|
+
BRAIN_VERSION_INPUT: ${{ inputs.brain_version || '' }}
|
|
61
|
+
SITE_PACKAGES_INPUT: ${{ inputs.site_packages || '' }}
|
|
62
|
+
ALLOW_TAG_OVERWRITE: ${{ inputs.overwrite && 'true' || 'false' }}
|
|
63
|
+
run: bun deploy/scripts/resolve-missing-images.ts
|
|
64
|
+
|
|
65
|
+
no_builds:
|
|
66
|
+
needs: resolve
|
|
67
|
+
if: needs.resolve.outputs.images_json == '[]'
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
steps:
|
|
70
|
+
- name: Skip when every declared image exists
|
|
71
|
+
run: echo "All declared images already exist; nothing to build."
|
|
72
|
+
|
|
73
|
+
build:
|
|
74
|
+
needs: resolve
|
|
75
|
+
if: needs.resolve.outputs.images_json != '[]'
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
strategy:
|
|
78
|
+
fail-fast: false
|
|
79
|
+
matrix:
|
|
80
|
+
image: ${{ fromJson(needs.resolve.outputs.images_json) }}
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/checkout@v5
|
|
83
|
+
with:
|
|
84
|
+
ref: ${{ github.sha }}
|
|
33
85
|
|
|
34
86
|
- name: Set up Docker Buildx
|
|
35
|
-
uses: docker/setup-buildx-action@
|
|
87
|
+
uses: docker/setup-buildx-action@v4
|
|
36
88
|
|
|
37
89
|
- name: Extract image metadata
|
|
38
90
|
id: meta
|
|
39
|
-
uses: docker/metadata-action@
|
|
91
|
+
uses: docker/metadata-action@v6
|
|
40
92
|
with:
|
|
41
|
-
images:
|
|
93
|
+
images: ghcr.io/${{ github.repository }}
|
|
42
94
|
tags: |
|
|
43
|
-
type=raw,value
|
|
95
|
+
type=raw,value=${{ matrix.image.tag }}
|
|
44
96
|
|
|
45
97
|
- name: Log in to GHCR
|
|
46
|
-
uses: docker/login-action@
|
|
98
|
+
uses: docker/login-action@v4
|
|
47
99
|
with:
|
|
48
100
|
registry: ghcr.io
|
|
49
101
|
username: ${{ github.actor }}
|
|
50
102
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
51
103
|
|
|
52
104
|
- name: Build and push image
|
|
53
|
-
uses: docker/build-push-action@
|
|
105
|
+
uses: docker/build-push-action@v7
|
|
54
106
|
with:
|
|
55
107
|
context: .
|
|
56
108
|
file: deploy/Dockerfile
|
|
109
|
+
target: fleet
|
|
57
110
|
push: true
|
|
58
111
|
build-args: |
|
|
59
|
-
BRAIN_VERSION=${{
|
|
112
|
+
BRAIN_VERSION=${{ matrix.image.brain_version }}
|
|
113
|
+
SITE_PACKAGES=${{ matrix.image.site_packages }}
|
|
60
114
|
tags: ${{ steps.meta.outputs.tags }}
|
|
61
115
|
labels: |
|
|
62
116
|
${{ steps.meta.outputs.labels }}
|
|
@@ -7,13 +7,24 @@ on:
|
|
|
7
7
|
description: User handle
|
|
8
8
|
required: true
|
|
9
9
|
type: string
|
|
10
|
+
release_stale_lock:
|
|
11
|
+
description: Release an operator-confirmed stale Kamal deploy lock before retrying
|
|
12
|
+
required: false
|
|
13
|
+
type: boolean
|
|
14
|
+
default: false
|
|
10
15
|
push:
|
|
11
16
|
branches: ["main"]
|
|
12
17
|
paths:
|
|
13
18
|
- users/*/.env
|
|
14
19
|
- users/*/brain.yaml
|
|
20
|
+
- users/*/content/**
|
|
21
|
+
- users/*.secrets.yaml.age
|
|
15
22
|
- deploy/**
|
|
16
23
|
- .github/workflows/deploy.yml
|
|
24
|
+
workflow_run:
|
|
25
|
+
workflows: [Reconcile]
|
|
26
|
+
types: [completed]
|
|
27
|
+
branches: [main]
|
|
17
28
|
|
|
18
29
|
permissions:
|
|
19
30
|
contents: write
|
|
@@ -21,13 +32,16 @@ permissions:
|
|
|
21
32
|
|
|
22
33
|
jobs:
|
|
23
34
|
resolve_handles:
|
|
35
|
+
if: >
|
|
36
|
+
github.event_name != 'workflow_run' ||
|
|
37
|
+
github.event.workflow_run.conclusion == 'success'
|
|
24
38
|
runs-on: ubuntu-latest
|
|
25
39
|
outputs:
|
|
26
40
|
handles_json: ${{ steps.resolve.outputs.handles_json }}
|
|
27
41
|
steps:
|
|
28
42
|
- uses: actions/checkout@v5
|
|
29
43
|
with:
|
|
30
|
-
ref: ${{ github.sha }}
|
|
44
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
31
45
|
fetch-depth: 0
|
|
32
46
|
|
|
33
47
|
- uses: oven-sh/setup-bun@v2
|
|
@@ -39,7 +53,7 @@ jobs:
|
|
|
39
53
|
id: resolve
|
|
40
54
|
env:
|
|
41
55
|
HANDLE_INPUT: ${{ inputs.handle || '' }}
|
|
42
|
-
BEFORE_SHA: ${{ github.event.before || '' }}
|
|
56
|
+
BEFORE_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.before || '' }}
|
|
43
57
|
run: bun deploy/scripts/resolve-deploy-handles.ts
|
|
44
58
|
|
|
45
59
|
no_changes:
|
|
@@ -66,13 +80,22 @@ jobs:
|
|
|
66
80
|
steps:
|
|
67
81
|
- uses: actions/checkout@v5
|
|
68
82
|
with:
|
|
69
|
-
ref: ${{ github.sha }}
|
|
83
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
70
84
|
|
|
71
85
|
- uses: oven-sh/setup-bun@v2
|
|
72
86
|
|
|
73
87
|
- name: Install operator tooling
|
|
74
88
|
run: bun install
|
|
75
89
|
|
|
90
|
+
- name: Load shared env via Varlock
|
|
91
|
+
uses: ./.github/actions/varlock-env
|
|
92
|
+
env:
|
|
93
|
+
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
|
|
94
|
+
|
|
95
|
+
- name: Decrypt user secrets
|
|
96
|
+
id: user_secrets
|
|
97
|
+
run: bun deploy/scripts/decrypt-user-secrets.ts "$HANDLE"
|
|
98
|
+
|
|
76
99
|
- name: Reconcile selected user config
|
|
77
100
|
run: bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
78
101
|
|
|
@@ -81,23 +104,13 @@ jobs:
|
|
|
81
104
|
run: bun deploy/scripts/resolve-user-config.ts
|
|
82
105
|
|
|
83
106
|
- name: Validate selected secrets
|
|
84
|
-
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] || '' }}
|
|
89
|
-
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
90
|
-
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
91
|
-
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
92
|
-
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
93
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
94
|
-
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
95
|
-
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
96
|
-
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
97
|
-
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
98
|
-
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
99
107
|
run: bun deploy/scripts/validate-secrets.ts
|
|
100
108
|
|
|
109
|
+
- name: Seed content repo
|
|
110
|
+
env:
|
|
111
|
+
CONTENT_REPO: ${{ steps.user_config.outputs.content_repo }}
|
|
112
|
+
run: bun deploy/scripts/sync-content-repo.ts
|
|
113
|
+
|
|
101
114
|
- name: Log in to GHCR
|
|
102
115
|
uses: docker/login-action@v3
|
|
103
116
|
with:
|
|
@@ -105,15 +118,18 @@ jobs:
|
|
|
105
118
|
username: ${{ github.actor }}
|
|
106
119
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
107
120
|
|
|
108
|
-
- name: Wait for
|
|
121
|
+
- name: Wait for image tag
|
|
109
122
|
run: |
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
# The Build workflow fires off the same config push (it derives the
|
|
124
|
+
# declared image set from the registry), so this wait must cover a
|
|
125
|
+
# full image build, not just registry propagation.
|
|
126
|
+
IMAGE_TAG="${{ steps.user_config.outputs.image_repository }}:${{ steps.user_config.outputs.image_tag }}"
|
|
127
|
+
for attempt in $(seq 1 60); do
|
|
112
128
|
if docker manifest inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
|
113
129
|
exit 0
|
|
114
130
|
fi
|
|
115
|
-
echo "
|
|
116
|
-
sleep
|
|
131
|
+
echo "Image tag not ready yet ($attempt/60): $IMAGE_TAG"
|
|
132
|
+
sleep 20
|
|
117
133
|
done
|
|
118
134
|
echo "Timed out waiting for $IMAGE_TAG" >&2
|
|
119
135
|
exit 1
|
|
@@ -124,8 +140,6 @@ jobs:
|
|
|
124
140
|
ruby -r rubygems -e 'puts Gem.user_dir + "/bin"' >> "$GITHUB_PATH"
|
|
125
141
|
|
|
126
142
|
- name: Write Kamal SSH key
|
|
127
|
-
env:
|
|
128
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
129
143
|
run: bun deploy/scripts/write-ssh-key.ts
|
|
130
144
|
|
|
131
145
|
- name: Configure SSH client
|
|
@@ -142,33 +156,27 @@ jobs:
|
|
|
142
156
|
chmod 600 ~/.ssh/config
|
|
143
157
|
|
|
144
158
|
- name: Write .kamal/secrets
|
|
145
|
-
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] || '' }}
|
|
150
|
-
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
151
|
-
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
152
|
-
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
153
159
|
run: bun deploy/scripts/write-kamal-secrets.ts
|
|
154
160
|
|
|
155
161
|
- name: Provision server
|
|
156
162
|
id: provision
|
|
157
163
|
env:
|
|
158
|
-
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
159
|
-
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
160
|
-
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
161
|
-
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
162
164
|
INSTANCE_NAME: ${{ steps.user_config.outputs.instance_name }}
|
|
163
165
|
run: bun deploy/scripts/provision-server.ts
|
|
164
166
|
|
|
165
167
|
- name: Update Cloudflare DNS
|
|
166
168
|
env:
|
|
167
|
-
|
|
168
|
-
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
169
|
+
CF_ZONE_ID: ${{ steps.user_config.outputs.cloudflare_zone_id }}
|
|
169
170
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
171
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
172
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
170
173
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
171
|
-
run:
|
|
174
|
+
run: |
|
|
175
|
+
bun deploy/scripts/update-dns.ts
|
|
176
|
+
if [ -n "$WWW_DOMAIN" ]; then
|
|
177
|
+
BRAIN_DOMAIN="$WWW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
178
|
+
fi
|
|
179
|
+
BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
172
180
|
|
|
173
181
|
- name: Validate SSH key
|
|
174
182
|
run: ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null
|
|
@@ -188,17 +196,53 @@ jobs:
|
|
|
188
196
|
echo "SSH never became ready for $SSH_USER@$SERVER_IP" >&2
|
|
189
197
|
exit 1
|
|
190
198
|
|
|
199
|
+
- name: Release operator-confirmed stale deploy lock
|
|
200
|
+
if: ${{ inputs.release_stale_lock }}
|
|
201
|
+
env:
|
|
202
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
203
|
+
VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
204
|
+
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
205
|
+
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
206
|
+
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
207
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
208
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
209
|
+
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
210
|
+
run: kamal lock release -c deploy/kamal/deploy.yml
|
|
211
|
+
|
|
191
212
|
- name: Deploy
|
|
192
213
|
env:
|
|
193
214
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
194
|
-
VERSION:
|
|
215
|
+
VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
216
|
+
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
217
|
+
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
218
|
+
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
219
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
220
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
221
|
+
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
195
222
|
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
196
223
|
|
|
224
|
+
- name: Install container health watchdog
|
|
225
|
+
env:
|
|
226
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
227
|
+
run: |
|
|
228
|
+
SSH_USER="$(ruby -e 'require "yaml"; config = YAML.load_file("deploy/kamal/deploy.yml") || {}; puts(config.dig("ssh", "user") || "root")')" \
|
|
229
|
+
bun deploy/scripts/install-health-watchdog.ts
|
|
230
|
+
|
|
197
231
|
- name: Verify origin TLS
|
|
198
232
|
env:
|
|
199
233
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
200
234
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
201
|
-
|
|
235
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
236
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
237
|
+
run: |
|
|
238
|
+
curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health/live"
|
|
239
|
+
if [ -n "$WWW_DOMAIN" ]; then
|
|
240
|
+
curl -I -k --max-time 20 --resolve "$WWW_DOMAIN:443:$SERVER_IP" "https://$WWW_DOMAIN/"
|
|
241
|
+
fi
|
|
242
|
+
curl -I -k --max-time 20 --resolve "$PREVIEW_DOMAIN:443:$SERVER_IP" "https://$PREVIEW_DOMAIN/"
|
|
243
|
+
|
|
244
|
+
- name: Verify operational health
|
|
245
|
+
run: bunx brains-ops verify-user "$GITHUB_WORKSPACE" "$HANDLE"
|
|
202
246
|
|
|
203
247
|
- name: Upload generated config
|
|
204
248
|
uses: actions/upload-artifact@v4
|
|
@@ -207,6 +251,7 @@ jobs:
|
|
|
207
251
|
path: |
|
|
208
252
|
users/${{ matrix.handle }}/brain.yaml
|
|
209
253
|
users/${{ matrix.handle }}/.env
|
|
254
|
+
users/${{ matrix.handle }}/content
|
|
210
255
|
|
|
211
256
|
- name: Dump remote proxy diagnostics
|
|
212
257
|
if: failure()
|
|
@@ -233,7 +278,7 @@ jobs:
|
|
|
233
278
|
steps:
|
|
234
279
|
- uses: actions/checkout@v5
|
|
235
280
|
with:
|
|
236
|
-
ref: ${{ github.sha }}
|
|
281
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
237
282
|
|
|
238
283
|
- uses: oven-sh/setup-bun@v2
|
|
239
284
|
|
|
@@ -252,11 +297,31 @@ jobs:
|
|
|
252
297
|
|
|
253
298
|
- name: Commit generated config
|
|
254
299
|
run: |
|
|
300
|
+
# A newly added user's generated directory is untracked, and
|
|
301
|
+
# `git diff` cannot see untracked files — without intent-to-add
|
|
302
|
+
# the new config is silently dropped here and every later deploy
|
|
303
|
+
# skips that user (their files never appear in a commit range).
|
|
304
|
+
git add --intent-to-add -- users views
|
|
305
|
+
|
|
255
306
|
if git diff --quiet -- users views; then
|
|
256
307
|
exit 0
|
|
257
308
|
fi
|
|
309
|
+
|
|
310
|
+
git fetch origin "${{ github.ref_name }}"
|
|
311
|
+
if git diff --quiet "origin/${{ github.ref_name }}" -- users views; then
|
|
312
|
+
exit 0
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
patch_file="$(mktemp)"
|
|
316
|
+
git diff --binary -- users views > "$patch_file"
|
|
317
|
+
git reset --hard "origin/${{ github.ref_name }}"
|
|
318
|
+
git apply --3way --index "$patch_file"
|
|
319
|
+
|
|
320
|
+
if git diff --cached --quiet -- users views; then
|
|
321
|
+
exit 0
|
|
322
|
+
fi
|
|
323
|
+
|
|
258
324
|
git config user.name "github-actions[bot]"
|
|
259
325
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
260
|
-
git add users views
|
|
261
326
|
git commit -m "chore(ops): reconcile generated config"
|
|
262
|
-
git push
|
|
327
|
+
git push origin HEAD:${{ github.ref_name }}
|