@rizom/ops 0.2.0-alpha.18 → 0.2.0-alpha.181
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 +3 -1
- package/dist/brains-ops.js +282 -195
- 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/entries/deploy.d.ts +3 -2
- package/dist/images.d.ts +76 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +283 -195
- package/dist/load-registry.d.ts +21 -2
- package/dist/observed-status.d.ts +1 -1
- package/dist/origin-ca.d.ts +1 -1
- package/dist/parse-args.d.ts +3 -0
- package/dist/push-secrets.d.ts +2 -9
- package/dist/push-target.d.ts +1 -2
- package/dist/run-command.d.ts +1 -1
- package/dist/run-subprocess.d.ts +1 -6
- package/dist/schema.d.ts +62 -155
- package/dist/secrets-encrypt.d.ts +5 -13
- package/dist/ssh-key-bootstrap.d.ts +1 -26
- package/dist/user-add.d.ts +15 -0
- package/dist/verify-user.d.ts +19 -0
- package/package.json +44 -42
- package/templates/rover-pilot/.env.schema +17 -3
- package/templates/rover-pilot/.github/workflows/build.yml +64 -17
- package/templates/rover-pilot/.github/workflows/deploy.yml +100 -61
- package/templates/rover-pilot/.github/workflows/reconcile.yml +21 -1
- package/templates/rover-pilot/README.md +3 -1
- package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +34 -6
- package/templates/rover-pilot/deploy/scripts/helpers.ts +3 -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 +22 -2
- 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 +1 -1
- package/templates/rover-pilot/docs/onboarding-checklist.md +32 -12
- package/templates/rover-pilot/docs/operator-playbook.md +117 -6
- package/templates/rover-pilot/docs/user-onboarding.md +67 -332
- package/templates/rover-pilot/pilot.yaml +1 -1
- package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
- package/templates/rover-pilot/deploy/Caddyfile +0 -72
- package/templates/rover-pilot/deploy/Dockerfile +0 -38
- package/templates/rover-pilot/deploy/kamal/deploy.yml +0 -40
|
@@ -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 }}
|
|
@@ -16,6 +16,10 @@ on:
|
|
|
16
16
|
- users/*.secrets.yaml.age
|
|
17
17
|
- deploy/**
|
|
18
18
|
- .github/workflows/deploy.yml
|
|
19
|
+
workflow_run:
|
|
20
|
+
workflows: [Reconcile]
|
|
21
|
+
types: [completed]
|
|
22
|
+
branches: [main]
|
|
19
23
|
|
|
20
24
|
permissions:
|
|
21
25
|
contents: write
|
|
@@ -23,13 +27,16 @@ permissions:
|
|
|
23
27
|
|
|
24
28
|
jobs:
|
|
25
29
|
resolve_handles:
|
|
30
|
+
if: >
|
|
31
|
+
github.event_name != 'workflow_run' ||
|
|
32
|
+
github.event.workflow_run.conclusion == 'success'
|
|
26
33
|
runs-on: ubuntu-latest
|
|
27
34
|
outputs:
|
|
28
35
|
handles_json: ${{ steps.resolve.outputs.handles_json }}
|
|
29
36
|
steps:
|
|
30
37
|
- uses: actions/checkout@v5
|
|
31
38
|
with:
|
|
32
|
-
ref: ${{ github.sha }}
|
|
39
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
33
40
|
fetch-depth: 0
|
|
34
41
|
|
|
35
42
|
- uses: oven-sh/setup-bun@v2
|
|
@@ -41,7 +48,7 @@ jobs:
|
|
|
41
48
|
id: resolve
|
|
42
49
|
env:
|
|
43
50
|
HANDLE_INPUT: ${{ inputs.handle || '' }}
|
|
44
|
-
BEFORE_SHA: ${{ github.event.before || '' }}
|
|
51
|
+
BEFORE_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.before || '' }}
|
|
45
52
|
run: bun deploy/scripts/resolve-deploy-handles.ts
|
|
46
53
|
|
|
47
54
|
no_changes:
|
|
@@ -68,58 +75,86 @@ jobs:
|
|
|
68
75
|
steps:
|
|
69
76
|
- uses: actions/checkout@v5
|
|
70
77
|
with:
|
|
71
|
-
ref: ${{ github.sha }}
|
|
78
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
72
79
|
|
|
73
80
|
- uses: oven-sh/setup-bun@v2
|
|
74
81
|
|
|
75
82
|
- name: Install operator tooling
|
|
76
83
|
run: bun install
|
|
77
84
|
|
|
85
|
+
- name: Load shared env via varlock
|
|
86
|
+
env:
|
|
87
|
+
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
|
|
88
|
+
run: |
|
|
89
|
+
for attempt in 1 2 3; do
|
|
90
|
+
if bunx varlock@1.1.0 load --path .env.schema --format json --compact > /tmp/varlock-env.json; then
|
|
91
|
+
break
|
|
92
|
+
fi
|
|
93
|
+
if [ "$attempt" = "3" ]; then
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
echo "Varlock load failed (attempt $attempt/3); retrying in 5s..."
|
|
97
|
+
sleep 5
|
|
98
|
+
done
|
|
99
|
+
node <<'NODE'
|
|
100
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
101
|
+
const env = JSON.parse(readFileSync('/tmp/varlock-env.json', 'utf8'));
|
|
102
|
+
const githubEnvPath = process.env.GITHUB_ENV;
|
|
103
|
+
if (!githubEnvPath) {
|
|
104
|
+
throw new Error('Missing GITHUB_ENV');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const newline = String.fromCharCode(10);
|
|
108
|
+
const carriageReturn = String.fromCharCode(13);
|
|
109
|
+
const chunks = [];
|
|
110
|
+
for (const [key, value] of Object.entries(env)) {
|
|
111
|
+
if (key === 'BWS_ACCESS_TOKEN' || value === null || value === undefined) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const text = String(value)
|
|
116
|
+
.split(carriageReturn + newline)
|
|
117
|
+
.join(newline);
|
|
118
|
+
const escapedMask = text
|
|
119
|
+
.replace(/%/g, '%25')
|
|
120
|
+
.replace(/\r/g, '%0D')
|
|
121
|
+
.replace(/\n/g, '%0A');
|
|
122
|
+
if (escapedMask.length > 0) {
|
|
123
|
+
process.stdout.write('::add-mask::' + escapedMask + newline);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (text.includes(newline)) {
|
|
127
|
+
let delimiter = `VARLOCK_${key}_EOF`;
|
|
128
|
+
while (text.includes(delimiter)) {
|
|
129
|
+
delimiter += '_X';
|
|
130
|
+
}
|
|
131
|
+
chunks.push(`${key}<<${delimiter}${newline}${text}${newline}${delimiter}`);
|
|
132
|
+
} else {
|
|
133
|
+
chunks.push(`${key}=${text}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
appendFileSync(githubEnvPath, chunks.join(newline) + newline);
|
|
138
|
+
NODE
|
|
139
|
+
|
|
78
140
|
- name: Decrypt user secrets
|
|
79
141
|
id: user_secrets
|
|
80
|
-
env:
|
|
81
|
-
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
|
82
142
|
run: bun deploy/scripts/decrypt-user-secrets.ts "$HANDLE"
|
|
83
143
|
|
|
84
144
|
- 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"
|
|
145
|
+
run: bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
90
146
|
|
|
91
147
|
- name: Resolve generated user config
|
|
92
148
|
id: user_config
|
|
93
149
|
run: bun deploy/scripts/resolve-user-config.ts
|
|
94
150
|
|
|
95
151
|
- 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
|
|
152
|
+
run: bun deploy/scripts/validate-secrets.ts
|
|
115
153
|
|
|
116
154
|
- name: Seed content repo
|
|
117
155
|
env:
|
|
118
156
|
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
|
|
157
|
+
run: bun deploy/scripts/sync-content-repo.ts
|
|
123
158
|
|
|
124
159
|
- name: Log in to GHCR
|
|
125
160
|
uses: docker/login-action@v3
|
|
@@ -128,15 +163,18 @@ jobs:
|
|
|
128
163
|
username: ${{ github.actor }}
|
|
129
164
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
130
165
|
|
|
131
|
-
- name: Wait for
|
|
166
|
+
- name: Wait for image tag
|
|
132
167
|
run: |
|
|
133
|
-
|
|
134
|
-
|
|
168
|
+
# The Build workflow fires off the same config push (it derives the
|
|
169
|
+
# declared image set from the registry), so this wait must cover a
|
|
170
|
+
# full image build, not just registry propagation.
|
|
171
|
+
IMAGE_TAG="${{ steps.user_config.outputs.image_repository }}:${{ steps.user_config.outputs.image_tag }}"
|
|
172
|
+
for attempt in $(seq 1 60); do
|
|
135
173
|
if docker manifest inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
|
136
174
|
exit 0
|
|
137
175
|
fi
|
|
138
|
-
echo "
|
|
139
|
-
sleep
|
|
176
|
+
echo "Image tag not ready yet ($attempt/60): $IMAGE_TAG"
|
|
177
|
+
sleep 20
|
|
140
178
|
done
|
|
141
179
|
echo "Timed out waiting for $IMAGE_TAG" >&2
|
|
142
180
|
exit 1
|
|
@@ -147,8 +185,6 @@ jobs:
|
|
|
147
185
|
ruby -r rubygems -e 'puts Gem.user_dir + "/bin"' >> "$GITHUB_PATH"
|
|
148
186
|
|
|
149
187
|
- name: Write Kamal SSH key
|
|
150
|
-
env:
|
|
151
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
152
188
|
run: bun deploy/scripts/write-ssh-key.ts
|
|
153
189
|
|
|
154
190
|
- name: Configure SSH client
|
|
@@ -165,33 +201,16 @@ jobs:
|
|
|
165
201
|
chmod 600 ~/.ssh/config
|
|
166
202
|
|
|
167
203
|
- 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
|
|
204
|
+
run: bun deploy/scripts/write-kamal-secrets.ts
|
|
180
205
|
|
|
181
206
|
- name: Provision server
|
|
182
207
|
id: provision
|
|
183
208
|
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
209
|
INSTANCE_NAME: ${{ steps.user_config.outputs.instance_name }}
|
|
189
210
|
run: bun deploy/scripts/provision-server.ts
|
|
190
211
|
|
|
191
212
|
- name: Update Cloudflare DNS
|
|
192
213
|
env:
|
|
193
|
-
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
194
|
-
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
195
214
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
196
215
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
197
216
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
@@ -220,7 +239,7 @@ jobs:
|
|
|
220
239
|
- name: Deploy
|
|
221
240
|
env:
|
|
222
241
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
223
|
-
VERSION:
|
|
242
|
+
VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
224
243
|
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
225
244
|
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
226
245
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
@@ -271,7 +290,7 @@ jobs:
|
|
|
271
290
|
steps:
|
|
272
291
|
- uses: actions/checkout@v5
|
|
273
292
|
with:
|
|
274
|
-
ref: ${{ github.sha }}
|
|
293
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
275
294
|
|
|
276
295
|
- uses: oven-sh/setup-bun@v2
|
|
277
296
|
|
|
@@ -290,11 +309,31 @@ jobs:
|
|
|
290
309
|
|
|
291
310
|
- name: Commit generated config
|
|
292
311
|
run: |
|
|
312
|
+
# A newly added user's generated directory is untracked, and
|
|
313
|
+
# `git diff` cannot see untracked files — without intent-to-add
|
|
314
|
+
# the new config is silently dropped here and every later deploy
|
|
315
|
+
# skips that user (their files never appear in a commit range).
|
|
316
|
+
git add --intent-to-add -- users views
|
|
317
|
+
|
|
293
318
|
if git diff --quiet -- users views; then
|
|
294
319
|
exit 0
|
|
295
320
|
fi
|
|
321
|
+
|
|
322
|
+
git fetch origin "${{ github.ref_name }}"
|
|
323
|
+
if git diff --quiet "origin/${{ github.ref_name }}" -- users views; then
|
|
324
|
+
exit 0
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
patch_file="$(mktemp)"
|
|
328
|
+
git diff --binary -- users views > "$patch_file"
|
|
329
|
+
git reset --hard "origin/${{ github.ref_name }}"
|
|
330
|
+
git apply --3way --index "$patch_file"
|
|
331
|
+
|
|
332
|
+
if git diff --cached --quiet -- users views; then
|
|
333
|
+
exit 0
|
|
334
|
+
fi
|
|
335
|
+
|
|
296
336
|
git config user.name "github-actions[bot]"
|
|
297
337
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
298
|
-
git add users views
|
|
299
338
|
git commit -m "chore(ops): reconcile generated config"
|
|
300
339
|
git push origin HEAD:${{ github.ref_name }}
|
|
@@ -35,11 +35,31 @@ jobs:
|
|
|
35
35
|
|
|
36
36
|
- name: Commit generated outputs
|
|
37
37
|
run: |
|
|
38
|
+
# A newly added user's generated directory is untracked, and
|
|
39
|
+
# `git diff` cannot see untracked files — without intent-to-add
|
|
40
|
+
# the new config is silently dropped here and every later deploy
|
|
41
|
+
# skips that user (their files never appear in a commit range).
|
|
42
|
+
git add --intent-to-add -- views users
|
|
43
|
+
|
|
38
44
|
if git diff --quiet -- views users; then
|
|
39
45
|
exit 0
|
|
40
46
|
fi
|
|
47
|
+
|
|
48
|
+
git fetch origin "${{ github.ref_name }}"
|
|
49
|
+
if git diff --quiet "origin/${{ github.ref_name }}" -- views users; then
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
patch_file="$(mktemp)"
|
|
54
|
+
git diff --binary -- views users > "$patch_file"
|
|
55
|
+
git reset --hard "origin/${{ github.ref_name }}"
|
|
56
|
+
git apply --3way --index "$patch_file"
|
|
57
|
+
|
|
58
|
+
if git diff --cached --quiet -- views users; then
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
41
62
|
git config user.name "github-actions[bot]"
|
|
42
63
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
43
|
-
git add views users
|
|
44
64
|
git commit -m "chore(ops): reconcile pilot outputs"
|
|
45
65
|
git push origin HEAD:${{ github.ref_name }}
|
|
@@ -29,6 +29,7 @@ The repo also checks in its deploy contract:
|
|
|
29
29
|
- `.github/workflows/*`
|
|
30
30
|
|
|
31
31
|
`.env.schema` is the single source of truth for required and sensitive deploy vars.
|
|
32
|
+
Use separate GitHub tokens: `CONTENT_REPO_ADMIN_TOKEN` for operator-side content repo creation/checks, and `GIT_SYNC_TOKEN` for runtime directory-sync git access.
|
|
32
33
|
The shared pilot image tag is `brain-${brainVersion}` end to end.
|
|
33
34
|
When `pilot.yaml.brainVersion` changes and you push, CI rebuilds the shared tag, refreshes generated user env files, and redeploys affected users.
|
|
34
35
|
When a push changes only deploy contract files, CI prints `No affected user configs; skipping deploy.` and stops before Kamal.
|
|
@@ -37,7 +38,8 @@ When a push changes only deploy contract files, CI prints `No affected user conf
|
|
|
37
38
|
|
|
38
39
|
- `brains-ops init <repo>`
|
|
39
40
|
- `brains-ops render <repo>` — regenerates `views/users.md` with live DNS, `/health`, and unauthenticated `/mcp` status checks
|
|
40
|
-
- `brains-ops
|
|
41
|
+
- `brains-ops user:add <repo> <handle> --cohort <cohort>` — scaffolds a user file, per-user secrets template, and cohort membership
|
|
42
|
+
- `brains-ops onboard <repo> <handle>` — creates/seeds the user's content repo with separate admin and sync tokens
|
|
41
43
|
- `brains-ops age-key:bootstrap <repo>`
|
|
42
44
|
- `brains-ops ssh-key:bootstrap <repo>`
|
|
43
45
|
- `brains-ops cert:bootstrap <repo>`
|
|
@@ -18,10 +18,19 @@ const plaintext = await decrypter.decrypt(decoded, "text");
|
|
|
18
18
|
const secrets = parseFlatYaml(plaintext);
|
|
19
19
|
const pilot = parseFlatYaml(readFileSync("pilot.yaml", "utf8"));
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
writeSecretGitHubEnv("AI_API_KEY", secrets["aiApiKey"]);
|
|
22
|
+
writeSecretGitHubEnv("GIT_SYNC_TOKEN", secrets["gitSyncToken"]);
|
|
23
|
+
writeSecretGitHubEnv(
|
|
24
|
+
"CMS_CONTENT_REPO_PAT",
|
|
25
|
+
secrets["cmsContentRepoPat"] ?? secrets["gitSyncToken"],
|
|
26
|
+
);
|
|
27
|
+
writeSecretGitHubEnv("DISCORD_BOT_TOKEN", secrets["discordBotToken"]);
|
|
28
|
+
// Per-user AT Protocol publishing credential (optional; from the user's
|
|
29
|
+
// encrypted secrets file). The scaffold wires this so a pilot can publish its
|
|
30
|
+
// brain's agent card to its PDS; a deployment that doesn't publish simply
|
|
31
|
+
// leaves it unset. TLS material (CERTIFICATE_PEM/PRIVATE_KEY_PEM) is handled by
|
|
32
|
+
// the kamal proxy block via shared env, not here.
|
|
33
|
+
writeSecretGitHubEnv("ATPROTO_APP_PASSWORD", secrets["atprotoAppPassword"]);
|
|
25
34
|
|
|
26
35
|
writeGitHubOutput(
|
|
27
36
|
"shared_ai_api_key_secret_name",
|
|
@@ -32,10 +41,29 @@ writeGitHubOutput(
|
|
|
32
41
|
requireFlatValue(pilot, "gitSyncToken", "pilot.yaml"),
|
|
33
42
|
);
|
|
34
43
|
writeGitHubOutput(
|
|
35
|
-
"
|
|
36
|
-
requireFlatValue(pilot, "
|
|
44
|
+
"shared_content_repo_admin_token_secret_name",
|
|
45
|
+
requireFlatValue(pilot, "contentRepoAdminToken", "pilot.yaml"),
|
|
37
46
|
);
|
|
38
47
|
|
|
48
|
+
function writeSecretGitHubEnv(name: string, value: string | undefined): void {
|
|
49
|
+
if (!value || value.trim().length === 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
maskGitHubSecret(value);
|
|
54
|
+
writeGitHubEnv(name, value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function maskGitHubSecret(value: string): void {
|
|
58
|
+
const escaped = value
|
|
59
|
+
.replace(/%/g, "%25")
|
|
60
|
+
.replace(/\r/g, "%0D")
|
|
61
|
+
.replace(/\n/g, "%0A");
|
|
62
|
+
if (escaped.length > 0 && process.env["GITHUB_ACTIONS"] === "true") {
|
|
63
|
+
console.log(`::add-mask::${escaped}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
39
67
|
function extractAgeIdentity(contents: string): string {
|
|
40
68
|
const line = contents
|
|
41
69
|
.split(/\r?\n/)
|
|
@@ -9,14 +9,23 @@ if (eventName === "workflow_dispatch") {
|
|
|
9
9
|
process.exit(0);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
if (eventName !== "push") {
|
|
12
|
+
if (eventName !== "push" && eventName !== "workflow_run") {
|
|
13
13
|
throw new Error(`Unsupported GITHUB_EVENT_NAME: ${eventName}`);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const beforeSha = requireEnv("BEFORE_SHA");
|
|
17
|
-
const currentSha =
|
|
17
|
+
const currentSha =
|
|
18
|
+
eventName === "workflow_run"
|
|
19
|
+
? execFileSync("git", ["rev-parse", "HEAD"], {
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
}).trim()
|
|
22
|
+
: requireEnv("GITHUB_SHA");
|
|
18
23
|
|
|
19
|
-
if (
|
|
24
|
+
if (
|
|
25
|
+
!isUsableGitRevision(beforeSha) ||
|
|
26
|
+
!isUsableGitRevision(currentSha) ||
|
|
27
|
+
beforeSha === currentSha
|
|
28
|
+
) {
|
|
20
29
|
writeGitHubOutput("handles_json", JSON.stringify([]));
|
|
21
30
|
process.exit(0);
|
|
22
31
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requireEnv,
|
|
3
|
+
runResolveMissingImages,
|
|
4
|
+
writeGitHubOutput,
|
|
5
|
+
} from "./helpers";
|
|
6
|
+
|
|
7
|
+
// The Build workflow's resolve step — the logic lives in @rizom/ops; this
|
|
8
|
+
// pilot repo only supplies its own paths and repository.
|
|
9
|
+
await runResolveMissingImages({
|
|
10
|
+
rootDir: process.cwd(),
|
|
11
|
+
imageRepository: `ghcr.io/${requireEnv("GITHUB_REPOSITORY")}`,
|
|
12
|
+
writeOutput: writeGitHubOutput,
|
|
13
|
+
});
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { loadPilotRegistry } from "@rizom/ops";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
parseEnvFile,
|
|
7
|
+
requireEnv,
|
|
8
|
+
sitePackagesFor,
|
|
9
|
+
siteImageTag,
|
|
10
|
+
writeGitHubOutput,
|
|
11
|
+
} from "./helpers";
|
|
4
12
|
|
|
5
13
|
const handle = requireEnv("HANDLE");
|
|
6
14
|
const envPath = `users/${handle}/.env`;
|
|
@@ -26,14 +34,26 @@ if (!zone) {
|
|
|
26
34
|
}
|
|
27
35
|
const previewDomain = `${handle}-preview.${zone}`;
|
|
28
36
|
|
|
37
|
+
const brainVersion = envEntries["BRAIN_VERSION"] ?? "";
|
|
38
|
+
|
|
39
|
+
// The image tag is a pure function of this instance's own config: plain
|
|
40
|
+
// `brain-{version}` for a default instance, or its own `brain-{version}-sites-
|
|
41
|
+
// {hash}` when it declares a siteOverride. Resolved through the same helper the
|
|
42
|
+
// build uses so the tag we wait for and run matches exactly what was pushed.
|
|
43
|
+
const registry = await loadPilotRegistry(process.cwd());
|
|
44
|
+
const user = registry.users.find((entry) => entry.handle === handle);
|
|
45
|
+
const sitePackages = sitePackagesFor(user?.siteOverride);
|
|
46
|
+
const imageTag = siteImageTag(brainVersion, sitePackages);
|
|
47
|
+
|
|
29
48
|
const outputs: Record<string, string> = {
|
|
30
|
-
brain_version:
|
|
49
|
+
brain_version: brainVersion,
|
|
31
50
|
content_repo: envEntries["CONTENT_REPO"] ?? "",
|
|
32
51
|
brain_domain: brainDomain,
|
|
33
52
|
preview_domain: previewDomain,
|
|
34
53
|
brain_yaml_path: brainYamlPath,
|
|
35
54
|
instance_name: `rover-${handle}`,
|
|
36
55
|
image_repository: `ghcr.io/${repository}`,
|
|
56
|
+
image_tag: imageTag,
|
|
37
57
|
registry_username: repositoryOwner,
|
|
38
58
|
};
|
|
39
59
|
|