@rizom/ops 0.2.0-alpha.26 → 0.2.0-alpha.261
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 +33 -2
- package/dist/brains-ops.js +407 -313
- package/dist/cert-bootstrap.d.ts +3 -1
- package/dist/content-repo-ref.d.ts +10 -0
- package/dist/content-repo.d.ts +1 -0
- package/dist/deploy.js +99 -166
- package/dist/directory-sync-stress-system.d.ts +63 -0
- package/dist/directory-sync-stress.d.ts +105 -0
- package/dist/entries/deploy.d.ts +3 -2
- package/dist/images.d.ts +76 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +443 -308
- package/dist/legacy-pilot-migration.d.ts +11 -0
- package/dist/load-registry.d.ts +55 -6
- package/dist/observed-status.d.ts +1 -1
- package/dist/origin-ca.d.ts +1 -1
- package/dist/parse-args.d.ts +2 -10
- package/dist/preview-domain.d.ts +9 -0
- package/dist/push-secrets.d.ts +2 -9
- package/dist/push-target.d.ts +1 -2
- package/dist/reconcile-dry-run.d.ts +9 -0
- package/dist/run-command.d.ts +8 -3
- package/dist/run-subprocess.d.ts +1 -6
- package/dist/schema.d.ts +100 -162
- package/dist/secrets-encrypt.d.ts +7 -13
- package/dist/ssh-key-bootstrap.d.ts +1 -26
- package/dist/stage-legacy-crossover.d.ts +20 -0
- package/dist/user-add.d.ts +15 -0
- package/dist/verify-user.d.ts +19 -0
- package/package.json +46 -42
- package/templates/rover-pilot/.env.schema +24 -3
- package/templates/rover-pilot/.github/actions/varlock-env/action.yml +47 -0
- package/templates/rover-pilot/.github/workflows/build.yml +64 -17
- package/templates/rover-pilot/.github/workflows/deploy.yml +70 -60
- package/templates/rover-pilot/.github/workflows/directory-sync-stress.yml +100 -0
- package/templates/rover-pilot/.github/workflows/reconcile.yml +10 -4
- package/templates/rover-pilot/README.md +13 -6
- package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +80 -24
- 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/resolve-deploy-handles.ts +12 -3
- package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
- package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +44 -9
- package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +51 -47
- 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 +29 -13
- package/templates/rover-pilot/docs/operator-playbook.md +234 -24
- package/templates/rover-pilot/docs/user-onboarding.md +49 -343
- package/templates/rover-pilot/pilot.yaml +3 -4
- package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
- package/templates/rover-pilot/deploy/Dockerfile +0 -30
- package/templates/rover-pilot/deploy/kamal/deploy.yml +0 -40
package/package.json
CHANGED
|
@@ -1,67 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rizom/ops",
|
|
3
|
+
"version": "0.2.0-alpha.261",
|
|
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/site-composition": "workspace:*",
|
|
27
|
+
"@brains/typescript-config": "workspace:*",
|
|
28
|
+
"@brains/utils": "workspace:*",
|
|
29
|
+
"@types/bun": "^1.3.14",
|
|
30
|
+
"typescript": "^7.0.2"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"bun": ">=1.3.3"
|
|
6
34
|
},
|
|
7
|
-
"version": "0.2.0-alpha.26",
|
|
8
|
-
"type": "module",
|
|
9
35
|
"exports": {
|
|
10
36
|
".": {
|
|
11
37
|
"types": "./dist/index.d.ts",
|
|
12
38
|
"import": "./dist/index.js"
|
|
13
39
|
},
|
|
14
40
|
"./deploy": {
|
|
15
|
-
"bun": "./dist/deploy.js",
|
|
16
41
|
"types": "./dist/deploy.d.ts",
|
|
17
|
-
"import": "./dist/deploy.js"
|
|
42
|
+
"import": "./dist/deploy.js",
|
|
43
|
+
"bun": "./dist/deploy.js"
|
|
18
44
|
}
|
|
19
45
|
},
|
|
20
|
-
"main": "./dist/index.js",
|
|
21
|
-
"types": "./dist/index.d.ts",
|
|
22
|
-
"bin": {
|
|
23
|
-
"brains-ops": "./dist/brains-ops.js"
|
|
24
|
-
},
|
|
25
46
|
"files": [
|
|
26
47
|
"dist",
|
|
27
48
|
"templates"
|
|
28
49
|
],
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"lint": "eslint . --ext .ts",
|
|
34
|
-
"lint:fix": "eslint . --ext .ts --fix",
|
|
35
|
-
"test": "bun test --timeout 20000"
|
|
36
|
-
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"age-encryption": "^0.3.0"
|
|
39
|
-
},
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@brains/eslint-config": "workspace:*",
|
|
42
|
-
"@brains/typescript-config": "workspace:*",
|
|
43
|
-
"@brains/utils": "workspace:*",
|
|
44
|
-
"@types/bun": "latest",
|
|
45
|
-
"typescript": "^5.3.3"
|
|
50
|
+
"homepage": "https://github.com/rizom-ai/brains/tree/main/packages/brains-ops#readme",
|
|
51
|
+
"main": "./dist/index.js",
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
46
54
|
},
|
|
47
55
|
"repository": {
|
|
48
56
|
"type": "git",
|
|
49
57
|
"url": "https://github.com/rizom-ai/brains.git",
|
|
50
58
|
"directory": "packages/brains-ops"
|
|
51
59
|
},
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "bun scripts/build.ts",
|
|
62
|
+
"lint": "eslint . --max-warnings 0",
|
|
63
|
+
"lint:fix": "eslint . --max-warnings 0 --fix",
|
|
64
|
+
"prepublishOnly": "bun scripts/build.ts",
|
|
65
|
+
"test": "bun run build && bun test --timeout 20000",
|
|
66
|
+
"test:smoke": "bun run build && RUN_SMOKE_TESTS=1 bun test --timeout 60000",
|
|
67
|
+
"typecheck": "tsc --noEmit"
|
|
58
68
|
},
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
"ops",
|
|
62
|
-
"cli",
|
|
63
|
-
"deploy",
|
|
64
|
-
"fleet",
|
|
65
|
-
"operator"
|
|
66
|
-
]
|
|
69
|
+
"type": "module",
|
|
70
|
+
"types": "./dist/index.d.ts"
|
|
67
71
|
}
|
|
@@ -14,16 +14,37 @@ AI_API_KEY=
|
|
|
14
14
|
# @required @sensitive
|
|
15
15
|
GIT_SYNC_TOKEN=
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
# Content repo administration
|
|
18
|
+
# Local/operator secret only. Used by brains-ops to create missing GitHub repos;
|
|
19
|
+
# do not deploy it into Rover runtime config.
|
|
19
20
|
# @required @sensitive
|
|
20
|
-
|
|
21
|
+
CONTENT_REPO_ADMIN_TOKEN=
|
|
21
22
|
|
|
22
23
|
# Discord (optional, per-user)
|
|
23
24
|
# Comes from the decrypted users/<handle>.secrets.yaml.age file when enabled.
|
|
24
25
|
# @sensitive
|
|
25
26
|
DISCORD_BOT_TOKEN=
|
|
26
27
|
|
|
28
|
+
# 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
|
+
|
|
27
48
|
# ---- deploy/provision vars ----
|
|
28
49
|
|
|
29
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,24 @@ 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
|
|
5
14
|
push:
|
|
6
15
|
branches: ["main"]
|
|
7
16
|
paths:
|
|
17
|
+
# The image set is a pure function of the declared fleet state: a push
|
|
18
|
+
# that changes it builds exactly what it declares, so the deploy (fired
|
|
19
|
+
# off the same push via Reconcile) finds its tag ready.
|
|
8
20
|
- pilot.yaml
|
|
21
|
+
- cohorts/**
|
|
22
|
+
- users/*.yaml
|
|
9
23
|
- deploy/**
|
|
10
24
|
- .github/workflows/build.yml
|
|
11
25
|
|
|
@@ -14,50 +28,83 @@ permissions:
|
|
|
14
28
|
packages: write
|
|
15
29
|
|
|
16
30
|
jobs:
|
|
17
|
-
|
|
31
|
+
resolve:
|
|
18
32
|
runs-on: ubuntu-latest
|
|
33
|
+
outputs:
|
|
34
|
+
images_json: ${{ steps.resolve.outputs.images_json }}
|
|
19
35
|
steps:
|
|
20
36
|
- uses: actions/checkout@v5
|
|
21
37
|
with:
|
|
22
38
|
ref: ${{ github.sha }}
|
|
23
39
|
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
- uses: oven-sh/setup-bun@v2
|
|
41
|
+
|
|
42
|
+
- name: Install operator tooling
|
|
43
|
+
run: bun install
|
|
44
|
+
|
|
45
|
+
- name: Log in to GHCR
|
|
46
|
+
uses: docker/login-action@v4
|
|
47
|
+
with:
|
|
48
|
+
registry: ghcr.io
|
|
49
|
+
username: ${{ github.actor }}
|
|
50
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
+
|
|
52
|
+
- name: Resolve images to build
|
|
53
|
+
id: resolve
|
|
54
|
+
env:
|
|
55
|
+
BRAIN_VERSION_INPUT: ${{ inputs.brain_version || '' }}
|
|
56
|
+
SITE_PACKAGES_INPUT: ${{ inputs.site_packages || '' }}
|
|
57
|
+
run: bun deploy/scripts/resolve-missing-images.ts
|
|
58
|
+
|
|
59
|
+
no_builds:
|
|
60
|
+
needs: resolve
|
|
61
|
+
if: needs.resolve.outputs.images_json == '[]'
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- name: Skip when every declared image exists
|
|
65
|
+
run: echo "All declared images already exist; nothing to build."
|
|
66
|
+
|
|
67
|
+
build:
|
|
68
|
+
needs: resolve
|
|
69
|
+
if: needs.resolve.outputs.images_json != '[]'
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
strategy:
|
|
72
|
+
fail-fast: false
|
|
73
|
+
matrix:
|
|
74
|
+
image: ${{ fromJson(needs.resolve.outputs.images_json) }}
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v5
|
|
77
|
+
with:
|
|
78
|
+
ref: ${{ github.sha }}
|
|
33
79
|
|
|
34
80
|
- name: Set up Docker Buildx
|
|
35
|
-
uses: docker/setup-buildx-action@
|
|
81
|
+
uses: docker/setup-buildx-action@v4
|
|
36
82
|
|
|
37
83
|
- name: Extract image metadata
|
|
38
84
|
id: meta
|
|
39
|
-
uses: docker/metadata-action@
|
|
85
|
+
uses: docker/metadata-action@v6
|
|
40
86
|
with:
|
|
41
|
-
images:
|
|
87
|
+
images: ghcr.io/${{ github.repository }}
|
|
42
88
|
tags: |
|
|
43
|
-
type=raw,value
|
|
89
|
+
type=raw,value=${{ matrix.image.tag }}
|
|
44
90
|
|
|
45
91
|
- name: Log in to GHCR
|
|
46
|
-
uses: docker/login-action@
|
|
92
|
+
uses: docker/login-action@v4
|
|
47
93
|
with:
|
|
48
94
|
registry: ghcr.io
|
|
49
95
|
username: ${{ github.actor }}
|
|
50
96
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
51
97
|
|
|
52
98
|
- name: Build and push image
|
|
53
|
-
uses: docker/build-push-action@
|
|
99
|
+
uses: docker/build-push-action@v7
|
|
54
100
|
with:
|
|
55
101
|
context: .
|
|
56
102
|
file: deploy/Dockerfile
|
|
57
103
|
target: fleet
|
|
58
104
|
push: true
|
|
59
105
|
build-args: |
|
|
60
|
-
BRAIN_VERSION=${{
|
|
106
|
+
BRAIN_VERSION=${{ matrix.image.brain_version }}
|
|
107
|
+
SITE_PACKAGES=${{ matrix.image.site_packages }}
|
|
61
108
|
tags: ${{ steps.meta.outputs.tags }}
|
|
62
109
|
labels: |
|
|
63
110
|
${{ steps.meta.outputs.labels }}
|
|
@@ -7,6 +7,11 @@ 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:
|
|
@@ -16,6 +21,10 @@ on:
|
|
|
16
21
|
- users/*.secrets.yaml.age
|
|
17
22
|
- deploy/**
|
|
18
23
|
- .github/workflows/deploy.yml
|
|
24
|
+
workflow_run:
|
|
25
|
+
workflows: [Reconcile]
|
|
26
|
+
types: [completed]
|
|
27
|
+
branches: [main]
|
|
19
28
|
|
|
20
29
|
permissions:
|
|
21
30
|
contents: write
|
|
@@ -23,13 +32,16 @@ permissions:
|
|
|
23
32
|
|
|
24
33
|
jobs:
|
|
25
34
|
resolve_handles:
|
|
35
|
+
if: >
|
|
36
|
+
github.event_name != 'workflow_run' ||
|
|
37
|
+
github.event.workflow_run.conclusion == 'success'
|
|
26
38
|
runs-on: ubuntu-latest
|
|
27
39
|
outputs:
|
|
28
40
|
handles_json: ${{ steps.resolve.outputs.handles_json }}
|
|
29
41
|
steps:
|
|
30
42
|
- uses: actions/checkout@v5
|
|
31
43
|
with:
|
|
32
|
-
ref: ${{ github.sha }}
|
|
44
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
33
45
|
fetch-depth: 0
|
|
34
46
|
|
|
35
47
|
- uses: oven-sh/setup-bun@v2
|
|
@@ -41,7 +53,7 @@ jobs:
|
|
|
41
53
|
id: resolve
|
|
42
54
|
env:
|
|
43
55
|
HANDLE_INPUT: ${{ inputs.handle || '' }}
|
|
44
|
-
BEFORE_SHA: ${{ github.event.before || '' }}
|
|
56
|
+
BEFORE_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.before || '' }}
|
|
45
57
|
run: bun deploy/scripts/resolve-deploy-handles.ts
|
|
46
58
|
|
|
47
59
|
no_changes:
|
|
@@ -68,58 +80,36 @@ jobs:
|
|
|
68
80
|
steps:
|
|
69
81
|
- uses: actions/checkout@v5
|
|
70
82
|
with:
|
|
71
|
-
ref: ${{ github.sha }}
|
|
83
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
72
84
|
|
|
73
85
|
- uses: oven-sh/setup-bun@v2
|
|
74
86
|
|
|
75
87
|
- name: Install operator tooling
|
|
76
88
|
run: bun install
|
|
77
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
|
+
|
|
78
95
|
- name: Decrypt user secrets
|
|
79
96
|
id: user_secrets
|
|
80
|
-
env:
|
|
81
|
-
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
|
82
97
|
run: bun deploy/scripts/decrypt-user-secrets.ts "$HANDLE"
|
|
83
98
|
|
|
84
99
|
- name: Reconcile selected user config
|
|
85
|
-
|
|
86
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
87
|
-
run: |
|
|
88
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
89
|
-
bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
100
|
+
run: bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
90
101
|
|
|
91
102
|
- name: Resolve generated user config
|
|
92
103
|
id: user_config
|
|
93
104
|
run: bun deploy/scripts/resolve-user-config.ts
|
|
94
105
|
|
|
95
106
|
- name: Validate selected secrets
|
|
96
|
-
|
|
97
|
-
SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
|
|
98
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
99
|
-
SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
|
|
100
|
-
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
101
|
-
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
102
|
-
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
103
|
-
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
104
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
105
|
-
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
106
|
-
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
107
|
-
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
108
|
-
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
109
|
-
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
110
|
-
run: |
|
|
111
|
-
export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
|
|
112
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
113
|
-
export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
|
|
114
|
-
bun deploy/scripts/validate-secrets.ts
|
|
107
|
+
run: bun deploy/scripts/validate-secrets.ts
|
|
115
108
|
|
|
116
109
|
- name: Seed content repo
|
|
117
110
|
env:
|
|
118
111
|
CONTENT_REPO: ${{ steps.user_config.outputs.content_repo }}
|
|
119
|
-
|
|
120
|
-
run: |
|
|
121
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
122
|
-
bun deploy/scripts/sync-content-repo.ts
|
|
112
|
+
run: bun deploy/scripts/sync-content-repo.ts
|
|
123
113
|
|
|
124
114
|
- name: Log in to GHCR
|
|
125
115
|
uses: docker/login-action@v3
|
|
@@ -128,15 +118,18 @@ jobs:
|
|
|
128
118
|
username: ${{ github.actor }}
|
|
129
119
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
130
120
|
|
|
131
|
-
- name: Wait for
|
|
121
|
+
- name: Wait for image tag
|
|
132
122
|
run: |
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
135
128
|
if docker manifest inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
|
136
129
|
exit 0
|
|
137
130
|
fi
|
|
138
|
-
echo "
|
|
139
|
-
sleep
|
|
131
|
+
echo "Image tag not ready yet ($attempt/60): $IMAGE_TAG"
|
|
132
|
+
sleep 20
|
|
140
133
|
done
|
|
141
134
|
echo "Timed out waiting for $IMAGE_TAG" >&2
|
|
142
135
|
exit 1
|
|
@@ -147,8 +140,6 @@ jobs:
|
|
|
147
140
|
ruby -r rubygems -e 'puts Gem.user_dir + "/bin"' >> "$GITHUB_PATH"
|
|
148
141
|
|
|
149
142
|
- name: Write Kamal SSH key
|
|
150
|
-
env:
|
|
151
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
152
143
|
run: bun deploy/scripts/write-ssh-key.ts
|
|
153
144
|
|
|
154
145
|
- name: Configure SSH client
|
|
@@ -165,38 +156,26 @@ jobs:
|
|
|
165
156
|
chmod 600 ~/.ssh/config
|
|
166
157
|
|
|
167
158
|
- name: Write .kamal/secrets
|
|
168
|
-
|
|
169
|
-
SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
|
|
170
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
171
|
-
SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
|
|
172
|
-
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
173
|
-
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
174
|
-
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
175
|
-
run: |
|
|
176
|
-
export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
|
|
177
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
178
|
-
export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
|
|
179
|
-
bun deploy/scripts/write-kamal-secrets.ts
|
|
159
|
+
run: bun deploy/scripts/write-kamal-secrets.ts
|
|
180
160
|
|
|
181
161
|
- name: Provision server
|
|
182
162
|
id: provision
|
|
183
163
|
env:
|
|
184
|
-
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
185
|
-
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
186
|
-
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
187
|
-
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
188
164
|
INSTANCE_NAME: ${{ steps.user_config.outputs.instance_name }}
|
|
189
165
|
run: bun deploy/scripts/provision-server.ts
|
|
190
166
|
|
|
191
167
|
- name: Update Cloudflare DNS
|
|
192
168
|
env:
|
|
193
|
-
|
|
194
|
-
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
169
|
+
CF_ZONE_ID: ${{ steps.user_config.outputs.cloudflare_zone_id }}
|
|
195
170
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
171
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
196
172
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
197
173
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
198
174
|
run: |
|
|
199
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
|
|
200
179
|
BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
201
180
|
|
|
202
181
|
- name: Validate SSH key
|
|
@@ -217,24 +196,49 @@ jobs:
|
|
|
217
196
|
echo "SSH never became ready for $SSH_USER@$SERVER_IP" >&2
|
|
218
197
|
exit 1
|
|
219
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
|
+
|
|
220
212
|
- name: Deploy
|
|
221
213
|
env:
|
|
222
214
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
223
|
-
VERSION:
|
|
215
|
+
VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
224
216
|
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
225
217
|
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
226
218
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
219
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
227
220
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
228
221
|
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
229
222
|
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
230
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
|
+
|
|
231
231
|
- name: Verify origin TLS
|
|
232
232
|
env:
|
|
233
233
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
234
234
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
235
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
235
236
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
236
237
|
run: |
|
|
237
238
|
curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
|
|
239
|
+
if [ -n "$WWW_DOMAIN" ]; then
|
|
240
|
+
curl -I -k --max-time 20 --resolve "$WWW_DOMAIN:443:$SERVER_IP" "https://$WWW_DOMAIN/"
|
|
241
|
+
fi
|
|
238
242
|
curl -I -k --max-time 20 --resolve "$PREVIEW_DOMAIN:443:$SERVER_IP" "https://$PREVIEW_DOMAIN/"
|
|
239
243
|
|
|
240
244
|
- name: Upload generated config
|
|
@@ -271,7 +275,7 @@ jobs:
|
|
|
271
275
|
steps:
|
|
272
276
|
- uses: actions/checkout@v5
|
|
273
277
|
with:
|
|
274
|
-
ref: ${{ github.sha }}
|
|
278
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
275
279
|
|
|
276
280
|
- uses: oven-sh/setup-bun@v2
|
|
277
281
|
|
|
@@ -290,6 +294,12 @@ jobs:
|
|
|
290
294
|
|
|
291
295
|
- name: Commit generated config
|
|
292
296
|
run: |
|
|
297
|
+
# A newly added user's generated directory is untracked, and
|
|
298
|
+
# `git diff` cannot see untracked files — without intent-to-add
|
|
299
|
+
# the new config is silently dropped here and every later deploy
|
|
300
|
+
# skips that user (their files never appear in a commit range).
|
|
301
|
+
git add --intent-to-add -- users views
|
|
302
|
+
|
|
293
303
|
if git diff --quiet -- users views; then
|
|
294
304
|
exit 0
|
|
295
305
|
fi
|