@rizom/ops 0.2.0-alpha.37 → 0.2.0-alpha.370
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 +66 -2
- package/dist/brains-ops.js +737 -332
- package/dist/capability-bundle-migration.d.ts +22 -0
- 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 +98 -166
- package/dist/directory-sync-stress-system.d.ts +75 -0
- package/dist/directory-sync-stress.d.ts +117 -0
- package/dist/entries/deploy.d.ts +3 -2
- package/dist/health-watchdog-smoke.d.ts +54 -0
- package/dist/images.d.ts +84 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +725 -320
- package/dist/legacy-pilot-migration.d.ts +11 -0
- package/dist/legacy-projection-job-recovery.d.ts +29 -0
- package/dist/load-registry.d.ts +60 -6
- package/dist/observed-status.d.ts +1 -1
- package/dist/origin-ca.d.ts +2 -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 +17 -3
- package/dist/run-subprocess.d.ts +1 -6
- package/dist/schema.d.ts +124 -161
- package/dist/secrets-encrypt.d.ts +7 -13
- package/dist/ssh-key-bootstrap.d.ts +1 -26
- package/dist/stage-legacy-crossover.d.ts +25 -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/verify-user.d.ts +22 -0
- package/package.json +50 -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 +70 -17
- package/templates/rover-pilot/.github/workflows/deploy.yml +71 -57
- package/templates/rover-pilot/.github/workflows/directory-sync-stress.yml +119 -0
- package/templates/rover-pilot/.github/workflows/health-watchdog-smoke.yml +95 -0
- package/templates/rover-pilot/.github/workflows/reconcile.yml +10 -4
- package/templates/rover-pilot/.github/workflows/upgrade.yml +104 -0
- package/templates/rover-pilot/README.md +16 -6
- package/templates/rover-pilot/deploy/scripts/create-predeploy-backup.ts +894 -0
- package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +80 -24
- package/templates/rover-pilot/deploy/scripts/helpers.ts +2 -0
- package/templates/rover-pilot/deploy/scripts/install-health-watchdog.ts +144 -0
- package/templates/rover-pilot/deploy/scripts/provision-server.ts +51 -21
- package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
- package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +40 -9
- package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +51 -47
- package/templates/rover-pilot/deploy/scripts/update-dns.ts +72 -16
- package/templates/rover-pilot/deploy/scripts/validate-secrets.ts +12 -1
- package/templates/rover-pilot/docs/canonical-crossover-record.md +107 -0
- package/templates/rover-pilot/docs/onboarding-checklist.md +28 -17
- package/templates/rover-pilot/docs/operator-playbook.md +311 -29
- package/templates/rover-pilot/docs/user-onboarding.md +48 -463
- package/templates/rover-pilot/package.json +1 -0
- package/templates/rover-pilot/pilot.yaml +7 -4
- package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
|
@@ -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:
|
|
@@ -82,51 +87,29 @@ jobs:
|
|
|
82
87
|
- name: Install operator tooling
|
|
83
88
|
run: bun install
|
|
84
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
|
+
|
|
85
95
|
- name: Decrypt user secrets
|
|
86
96
|
id: user_secrets
|
|
87
|
-
env:
|
|
88
|
-
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
|
89
97
|
run: bun deploy/scripts/decrypt-user-secrets.ts "$HANDLE"
|
|
90
98
|
|
|
91
99
|
- name: Reconcile selected user config
|
|
92
|
-
|
|
93
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
94
|
-
run: |
|
|
95
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
96
|
-
bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
100
|
+
run: bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
97
101
|
|
|
98
102
|
- name: Resolve generated user config
|
|
99
103
|
id: user_config
|
|
100
104
|
run: bun deploy/scripts/resolve-user-config.ts
|
|
101
105
|
|
|
102
106
|
- name: Validate selected secrets
|
|
103
|
-
|
|
104
|
-
SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
|
|
105
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
106
|
-
SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
|
|
107
|
-
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
108
|
-
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
109
|
-
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
110
|
-
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
111
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
112
|
-
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
113
|
-
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
114
|
-
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
115
|
-
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
116
|
-
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
117
|
-
run: |
|
|
118
|
-
export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
|
|
119
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
120
|
-
export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
|
|
121
|
-
bun deploy/scripts/validate-secrets.ts
|
|
107
|
+
run: bun deploy/scripts/validate-secrets.ts
|
|
122
108
|
|
|
123
109
|
- name: Seed content repo
|
|
124
110
|
env:
|
|
125
111
|
CONTENT_REPO: ${{ steps.user_config.outputs.content_repo }}
|
|
126
|
-
|
|
127
|
-
run: |
|
|
128
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
129
|
-
bun deploy/scripts/sync-content-repo.ts
|
|
112
|
+
run: bun deploy/scripts/sync-content-repo.ts
|
|
130
113
|
|
|
131
114
|
- name: Log in to GHCR
|
|
132
115
|
uses: docker/login-action@v3
|
|
@@ -135,15 +118,18 @@ jobs:
|
|
|
135
118
|
username: ${{ github.actor }}
|
|
136
119
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
137
120
|
|
|
138
|
-
- name: Wait for
|
|
121
|
+
- name: Wait for image tag
|
|
139
122
|
run: |
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
142
128
|
if docker manifest inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
|
143
129
|
exit 0
|
|
144
130
|
fi
|
|
145
|
-
echo "
|
|
146
|
-
sleep
|
|
131
|
+
echo "Image tag not ready yet ($attempt/60): $IMAGE_TAG"
|
|
132
|
+
sleep 20
|
|
147
133
|
done
|
|
148
134
|
echo "Timed out waiting for $IMAGE_TAG" >&2
|
|
149
135
|
exit 1
|
|
@@ -154,8 +140,6 @@ jobs:
|
|
|
154
140
|
ruby -r rubygems -e 'puts Gem.user_dir + "/bin"' >> "$GITHUB_PATH"
|
|
155
141
|
|
|
156
142
|
- name: Write Kamal SSH key
|
|
157
|
-
env:
|
|
158
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
159
143
|
run: bun deploy/scripts/write-ssh-key.ts
|
|
160
144
|
|
|
161
145
|
- name: Configure SSH client
|
|
@@ -172,38 +156,26 @@ jobs:
|
|
|
172
156
|
chmod 600 ~/.ssh/config
|
|
173
157
|
|
|
174
158
|
- name: Write .kamal/secrets
|
|
175
|
-
|
|
176
|
-
SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
|
|
177
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
178
|
-
SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
|
|
179
|
-
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
180
|
-
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
181
|
-
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
182
|
-
run: |
|
|
183
|
-
export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
|
|
184
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
185
|
-
export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
|
|
186
|
-
bun deploy/scripts/write-kamal-secrets.ts
|
|
159
|
+
run: bun deploy/scripts/write-kamal-secrets.ts
|
|
187
160
|
|
|
188
161
|
- name: Provision server
|
|
189
162
|
id: provision
|
|
190
163
|
env:
|
|
191
|
-
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
192
|
-
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
193
|
-
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
194
|
-
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
195
164
|
INSTANCE_NAME: ${{ steps.user_config.outputs.instance_name }}
|
|
196
165
|
run: bun deploy/scripts/provision-server.ts
|
|
197
166
|
|
|
198
167
|
- name: Update Cloudflare DNS
|
|
199
168
|
env:
|
|
200
|
-
|
|
201
|
-
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
169
|
+
CF_ZONE_ID: ${{ steps.user_config.outputs.cloudflare_zone_id }}
|
|
202
170
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
171
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
203
172
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
204
173
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
205
174
|
run: |
|
|
206
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
|
|
207
179
|
BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
208
180
|
|
|
209
181
|
- name: Validate SSH key
|
|
@@ -224,26 +196,62 @@ jobs:
|
|
|
224
196
|
echo "SSH never became ready for $SSH_USER@$SERVER_IP" >&2
|
|
225
197
|
exit 1
|
|
226
198
|
|
|
199
|
+
- name: Create verified predeploy backup
|
|
200
|
+
env:
|
|
201
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
202
|
+
TARGET_HANDLE: ${{ matrix.handle }}
|
|
203
|
+
TARGET_VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
204
|
+
SERVICE_NAME: rover
|
|
205
|
+
run: bun deploy/scripts/create-predeploy-backup.ts
|
|
206
|
+
|
|
207
|
+
- name: Release operator-confirmed stale deploy lock
|
|
208
|
+
if: ${{ inputs.release_stale_lock }}
|
|
209
|
+
env:
|
|
210
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
211
|
+
VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
212
|
+
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
213
|
+
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
214
|
+
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
215
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
216
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
217
|
+
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
218
|
+
run: kamal lock release -c deploy/kamal/deploy.yml
|
|
219
|
+
|
|
227
220
|
- name: Deploy
|
|
228
221
|
env:
|
|
229
222
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
230
|
-
VERSION:
|
|
223
|
+
VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
231
224
|
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
232
225
|
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
233
226
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
227
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
234
228
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
235
229
|
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
236
230
|
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
237
231
|
|
|
232
|
+
- name: Install container health watchdog
|
|
233
|
+
env:
|
|
234
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
235
|
+
run: |
|
|
236
|
+
SSH_USER="$(ruby -e 'require "yaml"; config = YAML.load_file("deploy/kamal/deploy.yml") || {}; puts(config.dig("ssh", "user") || "root")')" \
|
|
237
|
+
bun deploy/scripts/install-health-watchdog.ts
|
|
238
|
+
|
|
238
239
|
- name: Verify origin TLS
|
|
239
240
|
env:
|
|
240
241
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
241
242
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
243
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
242
244
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
243
245
|
run: |
|
|
244
|
-
curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
|
|
246
|
+
curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health/live"
|
|
247
|
+
if [ -n "$WWW_DOMAIN" ]; then
|
|
248
|
+
curl -I -k --max-time 20 --resolve "$WWW_DOMAIN:443:$SERVER_IP" "https://$WWW_DOMAIN/"
|
|
249
|
+
fi
|
|
245
250
|
curl -I -k --max-time 20 --resolve "$PREVIEW_DOMAIN:443:$SERVER_IP" "https://$PREVIEW_DOMAIN/"
|
|
246
251
|
|
|
252
|
+
- name: Verify operational health
|
|
253
|
+
run: bunx brains-ops verify-user "$GITHUB_WORKSPACE" "$HANDLE"
|
|
254
|
+
|
|
247
255
|
- name: Upload generated config
|
|
248
256
|
uses: actions/upload-artifact@v4
|
|
249
257
|
with:
|
|
@@ -297,6 +305,12 @@ jobs:
|
|
|
297
305
|
|
|
298
306
|
- name: Commit generated config
|
|
299
307
|
run: |
|
|
308
|
+
# A newly added user's generated directory is untracked, and
|
|
309
|
+
# `git diff` cannot see untracked files — without intent-to-add
|
|
310
|
+
# the new config is silently dropped here and every later deploy
|
|
311
|
+
# skips that user (their files never appear in a commit range).
|
|
312
|
+
git add --intent-to-add -- users views
|
|
313
|
+
|
|
300
314
|
if git diff --quiet -- users views; then
|
|
301
315
|
exit 0
|
|
302
316
|
fi
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
name: Directory Sync Stress
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "23 4 * * 1"
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
handle:
|
|
9
|
+
description: Smoke handle only
|
|
10
|
+
required: true
|
|
11
|
+
type: string
|
|
12
|
+
default: smoke
|
|
13
|
+
profile:
|
|
14
|
+
description: Workload profile
|
|
15
|
+
required: true
|
|
16
|
+
type: choice
|
|
17
|
+
default: regression
|
|
18
|
+
options:
|
|
19
|
+
- regression
|
|
20
|
+
- load
|
|
21
|
+
- stress
|
|
22
|
+
confirm:
|
|
23
|
+
description: Type stress:<handle> to confirm reversible content writes
|
|
24
|
+
required: true
|
|
25
|
+
type: string
|
|
26
|
+
verify_only:
|
|
27
|
+
description: Verify the content credential path without remote writes or a workload
|
|
28
|
+
required: false
|
|
29
|
+
type: boolean
|
|
30
|
+
default: false
|
|
31
|
+
|
|
32
|
+
permissions:
|
|
33
|
+
contents: read
|
|
34
|
+
|
|
35
|
+
concurrency:
|
|
36
|
+
group: directory-sync-stress-${{ github.event_name == 'schedule' && 'smoke' || inputs.handle }}
|
|
37
|
+
cancel-in-progress: false
|
|
38
|
+
|
|
39
|
+
jobs:
|
|
40
|
+
stress:
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
timeout-minutes: 75
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v5
|
|
45
|
+
|
|
46
|
+
- uses: oven-sh/setup-bun@v2
|
|
47
|
+
|
|
48
|
+
- name: Install operator tooling
|
|
49
|
+
run: bun install
|
|
50
|
+
|
|
51
|
+
- name: Load Bitwarden environment through Varlock
|
|
52
|
+
uses: ./.github/actions/varlock-env
|
|
53
|
+
env:
|
|
54
|
+
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
|
|
55
|
+
|
|
56
|
+
- name: Verify smoke directory-sync credential path
|
|
57
|
+
if: ${{ github.event_name != 'schedule' && inputs.verify_only }}
|
|
58
|
+
env:
|
|
59
|
+
HANDLE_INPUT: ${{ inputs.handle }}
|
|
60
|
+
CONFIRM_INPUT: ${{ inputs.confirm }}
|
|
61
|
+
run: |
|
|
62
|
+
bunx brains-ops stress:directory-sync:verify-access "$GITHUB_WORKSPACE" "$HANDLE_INPUT" \
|
|
63
|
+
--confirm "$CONFIRM_INPUT" \
|
|
64
|
+
--artifacts-dir "$RUNNER_TEMP/directory-sync-stress"
|
|
65
|
+
|
|
66
|
+
- name: Run smoke directory-sync stress profile
|
|
67
|
+
if: ${{ github.event_name == 'schedule' || !inputs.verify_only }}
|
|
68
|
+
env:
|
|
69
|
+
HANDLE_INPUT: ${{ github.event_name == 'schedule' && 'smoke' || inputs.handle }}
|
|
70
|
+
PROFILE_INPUT: ${{ github.event_name == 'schedule' && 'regression' || inputs.profile }}
|
|
71
|
+
CONFIRM_INPUT: ${{ github.event_name == 'schedule' && 'stress:smoke' || inputs.confirm }}
|
|
72
|
+
run: |
|
|
73
|
+
bunx brains-ops stress:directory-sync "$GITHUB_WORKSPACE" "$HANDLE_INPUT" \
|
|
74
|
+
--profile "$PROFILE_INPUT" \
|
|
75
|
+
--confirm "$CONFIRM_INPUT" \
|
|
76
|
+
--artifacts-dir "$RUNNER_TEMP/directory-sync-stress"
|
|
77
|
+
|
|
78
|
+
- name: Upload stress evidence
|
|
79
|
+
if: always()
|
|
80
|
+
uses: actions/upload-artifact@v4
|
|
81
|
+
with:
|
|
82
|
+
name: directory-sync-stress-${{ github.event_name == 'schedule' && 'smoke' || inputs.handle }}-${{ github.event_name == 'schedule' && 'regression' || inputs.profile }}-${{ github.run_id }}
|
|
83
|
+
path: ${{ runner.temp }}/directory-sync-stress
|
|
84
|
+
if-no-files-found: warn
|
|
85
|
+
|
|
86
|
+
cleanup:
|
|
87
|
+
needs: stress
|
|
88
|
+
if: ${{ always() && (github.event_name == 'schedule' || !inputs.verify_only) }}
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
timeout-minutes: 20
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/checkout@v5
|
|
93
|
+
|
|
94
|
+
- uses: oven-sh/setup-bun@v2
|
|
95
|
+
|
|
96
|
+
- name: Install operator tooling
|
|
97
|
+
run: bun install
|
|
98
|
+
|
|
99
|
+
- name: Load Bitwarden environment through Varlock
|
|
100
|
+
uses: ./.github/actions/varlock-env
|
|
101
|
+
env:
|
|
102
|
+
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
|
|
103
|
+
|
|
104
|
+
- name: Remove residual stress probes
|
|
105
|
+
env:
|
|
106
|
+
HANDLE_INPUT: ${{ github.event_name == 'schedule' && 'smoke' || inputs.handle }}
|
|
107
|
+
CONFIRM_INPUT: ${{ github.event_name == 'schedule' && 'stress:smoke' || inputs.confirm }}
|
|
108
|
+
run: |
|
|
109
|
+
bunx brains-ops stress:directory-sync:cleanup "$GITHUB_WORKSPACE" "$HANDLE_INPUT" \
|
|
110
|
+
--confirm "$CONFIRM_INPUT" \
|
|
111
|
+
--artifacts-dir "$RUNNER_TEMP/directory-sync-stress-cleanup"
|
|
112
|
+
|
|
113
|
+
- name: Upload cleanup evidence
|
|
114
|
+
if: always()
|
|
115
|
+
uses: actions/upload-artifact@v4
|
|
116
|
+
with:
|
|
117
|
+
name: directory-sync-stress-cleanup-${{ github.event_name == 'schedule' && 'smoke' || inputs.handle }}-${{ github.run_id }}
|
|
118
|
+
path: ${{ runner.temp }}/directory-sync-stress-cleanup
|
|
119
|
+
if-no-files-found: warn
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
name: Health Watchdog Smoke
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
handle:
|
|
7
|
+
description: Smoke fleet handle only
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
default: smoke
|
|
11
|
+
confirm:
|
|
12
|
+
description: Type watchdog-smoke:<handle> to confirm temporary container restarts
|
|
13
|
+
required: true
|
|
14
|
+
type: string
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: deploy-${{ inputs.handle }}
|
|
21
|
+
cancel-in-progress: false
|
|
22
|
+
|
|
23
|
+
env:
|
|
24
|
+
WATCHDOG_SMOKE_RUN_ID: gha-${{ github.run_id }}-${{ github.run_attempt }}
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
smoke:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
timeout-minutes: 20
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v5
|
|
32
|
+
|
|
33
|
+
- uses: oven-sh/setup-bun@v2
|
|
34
|
+
|
|
35
|
+
- name: Install operator tooling
|
|
36
|
+
run: bun install
|
|
37
|
+
|
|
38
|
+
- name: Load Bitwarden environment through Varlock
|
|
39
|
+
uses: ./.github/actions/varlock-env
|
|
40
|
+
env:
|
|
41
|
+
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
|
|
42
|
+
|
|
43
|
+
- name: Run fleet watchdog isolation smoke
|
|
44
|
+
env:
|
|
45
|
+
HANDLE_INPUT: ${{ inputs.handle }}
|
|
46
|
+
CONFIRM_INPUT: ${{ inputs.confirm }}
|
|
47
|
+
run: |
|
|
48
|
+
bunx brains-ops smoke:health-watchdog "$GITHUB_WORKSPACE" "$HANDLE_INPUT" \
|
|
49
|
+
--confirm "$CONFIRM_INPUT" \
|
|
50
|
+
--run-id "$WATCHDOG_SMOKE_RUN_ID" \
|
|
51
|
+
--artifacts-dir "$RUNNER_TEMP/health-watchdog-smoke"
|
|
52
|
+
|
|
53
|
+
- name: Upload watchdog smoke evidence
|
|
54
|
+
if: always()
|
|
55
|
+
uses: actions/upload-artifact@v4
|
|
56
|
+
with:
|
|
57
|
+
name: health-watchdog-smoke-${{ inputs.handle }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
58
|
+
path: ${{ runner.temp }}/health-watchdog-smoke
|
|
59
|
+
if-no-files-found: warn
|
|
60
|
+
|
|
61
|
+
cleanup:
|
|
62
|
+
needs: smoke
|
|
63
|
+
if: always()
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
timeout-minutes: 10
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v5
|
|
68
|
+
|
|
69
|
+
- uses: oven-sh/setup-bun@v2
|
|
70
|
+
|
|
71
|
+
- name: Install operator tooling
|
|
72
|
+
run: bun install
|
|
73
|
+
|
|
74
|
+
- name: Load Bitwarden environment through Varlock
|
|
75
|
+
uses: ./.github/actions/varlock-env
|
|
76
|
+
env:
|
|
77
|
+
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
|
|
78
|
+
|
|
79
|
+
- name: Remove residual watchdog smoke fixtures
|
|
80
|
+
env:
|
|
81
|
+
HANDLE_INPUT: ${{ inputs.handle }}
|
|
82
|
+
CONFIRM_INPUT: ${{ inputs.confirm }}
|
|
83
|
+
run: |
|
|
84
|
+
bunx brains-ops smoke:health-watchdog:cleanup "$GITHUB_WORKSPACE" "$HANDLE_INPUT" \
|
|
85
|
+
--confirm "$CONFIRM_INPUT" \
|
|
86
|
+
--run-id "$WATCHDOG_SMOKE_RUN_ID" \
|
|
87
|
+
--artifacts-dir "$RUNNER_TEMP/health-watchdog-smoke-cleanup"
|
|
88
|
+
|
|
89
|
+
- name: Upload watchdog smoke cleanup evidence
|
|
90
|
+
if: always()
|
|
91
|
+
uses: actions/upload-artifact@v4
|
|
92
|
+
with:
|
|
93
|
+
name: health-watchdog-smoke-cleanup-${{ inputs.handle }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
94
|
+
path: ${{ runner.temp }}/health-watchdog-smoke-cleanup
|
|
95
|
+
if-no-files-found: warn
|
|
@@ -35,21 +35,27 @@ jobs:
|
|
|
35
35
|
|
|
36
36
|
- name: Commit generated outputs
|
|
37
37
|
run: |
|
|
38
|
-
|
|
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 -- users
|
|
43
|
+
|
|
44
|
+
if git diff --quiet -- users; then
|
|
39
45
|
exit 0
|
|
40
46
|
fi
|
|
41
47
|
|
|
42
48
|
git fetch origin "${{ github.ref_name }}"
|
|
43
|
-
if git diff --quiet "origin/${{ github.ref_name }}" --
|
|
49
|
+
if git diff --quiet "origin/${{ github.ref_name }}" -- users; then
|
|
44
50
|
exit 0
|
|
45
51
|
fi
|
|
46
52
|
|
|
47
53
|
patch_file="$(mktemp)"
|
|
48
|
-
git diff --binary --
|
|
54
|
+
git diff --binary -- users > "$patch_file"
|
|
49
55
|
git reset --hard "origin/${{ github.ref_name }}"
|
|
50
56
|
git apply --3way --index "$patch_file"
|
|
51
57
|
|
|
52
|
-
if git diff --cached --quiet --
|
|
58
|
+
if git diff --cached --quiet -- users; then
|
|
53
59
|
exit 0
|
|
54
60
|
fi
|
|
55
61
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
name: Upgrade
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version:
|
|
7
|
+
description: Explicit @rizom/ops version (empty means latest)
|
|
8
|
+
required: false
|
|
9
|
+
type: string
|
|
10
|
+
schedule:
|
|
11
|
+
# Weekday morning check for new operator tooling releases.
|
|
12
|
+
- cron: "17 6 * * 1-5"
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: upgrade
|
|
19
|
+
cancel-in-progress: false
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
upgrade:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
# Check only presence here; the private key itself is never exposed to
|
|
26
|
+
# this step. A missing private-key secret fails before checkout.
|
|
27
|
+
- name: Require upgrade App configuration
|
|
28
|
+
env:
|
|
29
|
+
APP_ID: ${{ vars.OPS_UPGRADE_APP_ID }}
|
|
30
|
+
HAS_PRIVATE_KEY: ${{ secrets.OPS_UPGRADE_APP_PRIVATE_KEY != '' }}
|
|
31
|
+
run: |
|
|
32
|
+
missing=""
|
|
33
|
+
if [ -z "$APP_ID" ]; then
|
|
34
|
+
missing="OPS_UPGRADE_APP_ID"
|
|
35
|
+
fi
|
|
36
|
+
if [ "$HAS_PRIVATE_KEY" != "true" ]; then
|
|
37
|
+
missing="${missing:+$missing and }OPS_UPGRADE_APP_PRIVATE_KEY"
|
|
38
|
+
fi
|
|
39
|
+
if [ -n "$missing" ]; then
|
|
40
|
+
echo "$missing is not set for this repository." >&2
|
|
41
|
+
echo "Configure the upgrade GitHub App as described in docs/operator-playbook.md." >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# No credential is persisted in .git/config while the workflow installs
|
|
46
|
+
# and executes a freshly published @rizom/ops package.
|
|
47
|
+
- uses: actions/checkout@v5
|
|
48
|
+
with:
|
|
49
|
+
persist-credentials: false
|
|
50
|
+
|
|
51
|
+
- uses: oven-sh/setup-bun@v2
|
|
52
|
+
|
|
53
|
+
- name: Install operator tooling
|
|
54
|
+
run: bun install
|
|
55
|
+
|
|
56
|
+
- name: Upgrade operator tooling
|
|
57
|
+
env:
|
|
58
|
+
VERSION_INPUT: ${{ inputs.version }}
|
|
59
|
+
run: bunx brains-ops upgrade . --to "${VERSION_INPUT:-latest}"
|
|
60
|
+
|
|
61
|
+
- name: Detect upgrade changes
|
|
62
|
+
id: upgrade-changes
|
|
63
|
+
run: |
|
|
64
|
+
if [ -z "$(git status --porcelain)" ]; then
|
|
65
|
+
echo "Already up to date."
|
|
66
|
+
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
67
|
+
else
|
|
68
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
- name: Create repository-scoped upgrade token
|
|
72
|
+
if: steps.upgrade-changes.outputs.changed == 'true'
|
|
73
|
+
id: upgrade-token
|
|
74
|
+
uses: actions/create-github-app-token@v2
|
|
75
|
+
with:
|
|
76
|
+
app-id: ${{ vars.OPS_UPGRADE_APP_ID }}
|
|
77
|
+
private-key: ${{ secrets.OPS_UPGRADE_APP_PRIVATE_KEY }}
|
|
78
|
+
permission-contents: write
|
|
79
|
+
permission-pull-requests: write
|
|
80
|
+
permission-workflows: write
|
|
81
|
+
|
|
82
|
+
- name: Open upgrade PR
|
|
83
|
+
if: steps.upgrade-changes.outputs.changed == 'true'
|
|
84
|
+
env:
|
|
85
|
+
GH_TOKEN: ${{ steps.upgrade-token.outputs.token }}
|
|
86
|
+
run: |
|
|
87
|
+
VERSION=$(bun -e 'console.log(JSON.parse(await Bun.file("package.json").text()).devDependencies["@rizom/ops"])')
|
|
88
|
+
BRANCH="ops/upgrade-rizom-ops-$VERSION"
|
|
89
|
+
git config user.name "github-actions[bot]"
|
|
90
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
91
|
+
git checkout -B "$BRANCH"
|
|
92
|
+
git add -A
|
|
93
|
+
git commit -m "chore(ops): upgrade @rizom/ops to $VERSION"
|
|
94
|
+
git push --force \
|
|
95
|
+
"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \
|
|
96
|
+
"$BRANCH"
|
|
97
|
+
if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then
|
|
98
|
+
echo "PR already open for $BRANCH"
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
gh pr create \
|
|
102
|
+
--title "chore(ops): upgrade @rizom/ops to $VERSION" \
|
|
103
|
+
--body "Automated operator tooling upgrade. Review the scaffold diff, then merge; deploys stay operator-dispatched." \
|
|
104
|
+
--head "$BRANCH"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# private brain pilot
|
|
2
2
|
|
|
3
|
-
Private desired-state
|
|
3
|
+
Private desired-state repository for the hosted brain pilot. The generated repository/service name may remain `rover-pilot` for deployment identity compatibility.
|
|
4
4
|
|
|
5
5
|
This is a single operator-owned repo. Pilot users do not get their own brain repos.
|
|
6
6
|
Per-user deploy config lives under `users/<handle>/`, while content stays in per-user content repos.
|
|
@@ -29,18 +29,28 @@ 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
|
-
|
|
33
|
-
|
|
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.
|
|
33
|
+
The fleet publishes one `brain-${brainVersion}` image per effective Brain version and installs the union of exact site/theme package pins required by instances on that version.
|
|
34
|
+
When an effective brain version (`pilot.yaml.brainVersion`, or a cohort override) changes and you push, CI builds every missing version image, refreshes generated user env files, and redeploys affected users. Every external site and theme package keeps its own exact version pin; change that package set only together with a fresh Brain version so published tags remain immutable.
|
|
34
35
|
When a push changes only deploy contract files, CI prints `No affected user configs; skipping deploy.` and stops before Kamal.
|
|
35
36
|
|
|
36
37
|
## Commands
|
|
37
38
|
|
|
38
39
|
- `brains-ops init <repo>`
|
|
39
|
-
- `brains-ops
|
|
40
|
-
- `brains-ops
|
|
40
|
+
- `brains-ops upgrade <repo>` — bumps `@rizom/ops` (or `--to <version>`) and refreshes the generated scaffold; the Upgrade workflow runs this on a schedule and opens a PR
|
|
41
|
+
- `brains-ops render <repo>` — regenerates `views/users.md` with live DNS, `/health/ready`, and unauthenticated `/mcp` status checks
|
|
42
|
+
- `brains-ops user:add <repo> <handle> --cohort <cohort>` — scaffolds a user file, per-user secrets template, and cohort membership
|
|
43
|
+
- `brains-ops onboard <repo> <handle>` — creates/seeds the user's content repo with separate admin and sync tokens
|
|
41
44
|
- `brains-ops age-key:bootstrap <repo>`
|
|
42
45
|
- `brains-ops ssh-key:bootstrap <repo>`
|
|
43
46
|
- `brains-ops cert:bootstrap <repo>`
|
|
44
47
|
- `brains-ops secrets:encrypt <repo> <handle>`
|
|
45
48
|
- `brains-ops reconcile-cohort <repo> <cohort>`
|
|
46
49
|
- `brains-ops reconcile-all <repo>`
|
|
50
|
+
- `brains-ops reconcile-all <repo> --dry-run` (isolated, no external content-repository access; lists both passes' changed files)
|
|
51
|
+
|
|
52
|
+
`render` owns the observational `views/users.md` projection. Reconcile owns generated per-user config and never rewrites observed status rows.
|
|
53
|
+
|
|
54
|
+
The Upgrade workflow uses a repository-scoped GitHub App token because an Actions `GITHUB_TOKEN` cannot update generated files under `.github/workflows/`. Checkout persists no credential, and the App token is minted only after the freshly published tooling finishes and only when there is a change to push. Configure the App and its `OPS_UPGRADE_APP_ID` Actions variable and `OPS_UPGRADE_APP_PRIVATE_KEY` Actions secret as described in `docs/operator-playbook.md`. If token creation or the upgrade push fails, stop; do not substitute an operator's personal credentials.
|
|
55
|
+
|
|
56
|
+
Use `docs/canonical-crossover-record.md` to record exact forward and rollback artifact pins before an approved crossover window.
|