@rizom/ops 0.2.0-alpha.39 → 0.2.0-alpha.392
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 +70 -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 +110 -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 +4 -2
- package/dist/health-watchdog-smoke.d.ts +54 -0
- package/dist/image-inventory.d.ts +4 -0
- package/dist/image-types.d.ts +7 -0
- package/dist/images.d.ts +82 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +737 -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 +74 -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/deploy/scripts/verify-runtime-image.ts +18 -0
- 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 +324 -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,27 +118,31 @@ 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
|
|
150
136
|
|
|
137
|
+
- name: Verify runtime image packages
|
|
138
|
+
run: bun deploy/scripts/verify-runtime-image.ts
|
|
139
|
+
|
|
151
140
|
- name: Install Kamal
|
|
152
141
|
run: |
|
|
153
142
|
gem install --user-install kamal
|
|
154
143
|
ruby -r rubygems -e 'puts Gem.user_dir + "/bin"' >> "$GITHUB_PATH"
|
|
155
144
|
|
|
156
145
|
- name: Write Kamal SSH key
|
|
157
|
-
env:
|
|
158
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
159
146
|
run: bun deploy/scripts/write-ssh-key.ts
|
|
160
147
|
|
|
161
148
|
- name: Configure SSH client
|
|
@@ -172,38 +159,26 @@ jobs:
|
|
|
172
159
|
chmod 600 ~/.ssh/config
|
|
173
160
|
|
|
174
161
|
- 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
|
|
162
|
+
run: bun deploy/scripts/write-kamal-secrets.ts
|
|
187
163
|
|
|
188
164
|
- name: Provision server
|
|
189
165
|
id: provision
|
|
190
166
|
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
167
|
INSTANCE_NAME: ${{ steps.user_config.outputs.instance_name }}
|
|
196
168
|
run: bun deploy/scripts/provision-server.ts
|
|
197
169
|
|
|
198
170
|
- name: Update Cloudflare DNS
|
|
199
171
|
env:
|
|
200
|
-
|
|
201
|
-
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
172
|
+
CF_ZONE_ID: ${{ steps.user_config.outputs.cloudflare_zone_id }}
|
|
202
173
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
174
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
203
175
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
204
176
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
205
177
|
run: |
|
|
206
178
|
bun deploy/scripts/update-dns.ts
|
|
179
|
+
if [ -n "$WWW_DOMAIN" ]; then
|
|
180
|
+
BRAIN_DOMAIN="$WWW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
181
|
+
fi
|
|
207
182
|
BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
208
183
|
|
|
209
184
|
- name: Validate SSH key
|
|
@@ -224,26 +199,62 @@ jobs:
|
|
|
224
199
|
echo "SSH never became ready for $SSH_USER@$SERVER_IP" >&2
|
|
225
200
|
exit 1
|
|
226
201
|
|
|
202
|
+
- name: Create verified predeploy backup
|
|
203
|
+
env:
|
|
204
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
205
|
+
TARGET_HANDLE: ${{ matrix.handle }}
|
|
206
|
+
TARGET_VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
207
|
+
SERVICE_NAME: rover
|
|
208
|
+
run: bun deploy/scripts/create-predeploy-backup.ts
|
|
209
|
+
|
|
210
|
+
- name: Release operator-confirmed stale deploy lock
|
|
211
|
+
if: ${{ inputs.release_stale_lock }}
|
|
212
|
+
env:
|
|
213
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
214
|
+
VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
215
|
+
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
216
|
+
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
217
|
+
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
218
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
219
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
220
|
+
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
221
|
+
run: kamal lock release -c deploy/kamal/deploy.yml
|
|
222
|
+
|
|
227
223
|
- name: Deploy
|
|
228
224
|
env:
|
|
229
225
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
230
|
-
VERSION:
|
|
226
|
+
VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
231
227
|
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
232
228
|
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
233
229
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
230
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
234
231
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
235
232
|
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
236
233
|
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
237
234
|
|
|
235
|
+
- name: Install container health watchdog
|
|
236
|
+
env:
|
|
237
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
238
|
+
run: |
|
|
239
|
+
SSH_USER="$(ruby -e 'require "yaml"; config = YAML.load_file("deploy/kamal/deploy.yml") || {}; puts(config.dig("ssh", "user") || "root")')" \
|
|
240
|
+
bun deploy/scripts/install-health-watchdog.ts
|
|
241
|
+
|
|
238
242
|
- name: Verify origin TLS
|
|
239
243
|
env:
|
|
240
244
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
241
245
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
246
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
242
247
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
243
248
|
run: |
|
|
244
|
-
curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
|
|
249
|
+
curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health/live"
|
|
250
|
+
if [ -n "$WWW_DOMAIN" ]; then
|
|
251
|
+
curl -I -k --max-time 20 --resolve "$WWW_DOMAIN:443:$SERVER_IP" "https://$WWW_DOMAIN/"
|
|
252
|
+
fi
|
|
245
253
|
curl -I -k --max-time 20 --resolve "$PREVIEW_DOMAIN:443:$SERVER_IP" "https://$PREVIEW_DOMAIN/"
|
|
246
254
|
|
|
255
|
+
- name: Verify operational health
|
|
256
|
+
run: bunx brains-ops verify-user "$GITHUB_WORKSPACE" "$HANDLE"
|
|
257
|
+
|
|
247
258
|
- name: Upload generated config
|
|
248
259
|
uses: actions/upload-artifact@v4
|
|
249
260
|
with:
|
|
@@ -297,6 +308,12 @@ jobs:
|
|
|
297
308
|
|
|
298
309
|
- name: Commit generated config
|
|
299
310
|
run: |
|
|
311
|
+
# A newly added user's generated directory is untracked, and
|
|
312
|
+
# `git diff` cannot see untracked files — without intent-to-add
|
|
313
|
+
# the new config is silently dropped here and every later deploy
|
|
314
|
+
# skips that user (their files never appear in a commit range).
|
|
315
|
+
git add --intent-to-add -- users views
|
|
316
|
+
|
|
300
317
|
if git diff --quiet -- users views; then
|
|
301
318
|
exit 0
|
|
302
319
|
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 immutable `brain-${brainVersion}` image per effective Brain version. New images install the exact site/theme package union across the entire fleet, so smoke-to-fleet promotion reuses the tested image. Build and Deploy verify installed package versions before reusing existing images.
|
|
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.
|