@rizom/ops 0.2.0-alpha.6 → 0.2.0-alpha.600

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.
Files changed (82) hide show
  1. package/README.md +88 -4
  2. package/dist/age-key-bootstrap.d.ts +17 -0
  3. package/dist/brains-ops.js +790 -166
  4. package/dist/capability-bundle-migration.d.ts +22 -0
  5. package/dist/cert-bootstrap.d.ts +6 -4
  6. package/dist/content-repo-ref.d.ts +10 -0
  7. package/dist/content-repo.d.ts +13 -0
  8. package/dist/default-user-runner.d.ts +1 -1
  9. package/dist/deploy.js +110 -166
  10. package/dist/directory-sync-stress-system.d.ts +75 -0
  11. package/dist/directory-sync-stress.d.ts +117 -0
  12. package/dist/entries/deploy.d.ts +4 -2
  13. package/dist/health-watchdog-smoke.d.ts +54 -0
  14. package/dist/image-inventory.d.ts +4 -0
  15. package/dist/image-types.d.ts +7 -0
  16. package/dist/images.d.ts +89 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.js +802 -166
  19. package/dist/legacy-pilot-migration.d.ts +11 -0
  20. package/dist/legacy-projection-job-recovery.d.ts +29 -0
  21. package/dist/load-registry.d.ts +88 -6
  22. package/dist/observed-status.d.ts +1 -1
  23. package/dist/onboard-user.d.ts +2 -2
  24. package/dist/parse-args.d.ts +2 -10
  25. package/dist/preview-domain.d.ts +9 -0
  26. package/dist/reconcile-all.d.ts +2 -2
  27. package/dist/reconcile-cohort.d.ts +2 -2
  28. package/dist/reconcile-dry-run.d.ts +9 -0
  29. package/dist/reconcile-lib.d.ts +4 -2
  30. package/dist/run-command.d.ts +22 -6
  31. package/dist/runtime-image-lock.d.ts +12 -0
  32. package/dist/schema.d.ts +147 -69
  33. package/dist/secrets-encrypt.d.ts +26 -0
  34. package/dist/secrets-push.d.ts +2 -2
  35. package/dist/ssh-key-bootstrap.d.ts +1 -26
  36. package/dist/stage-legacy-crossover.d.ts +25 -0
  37. package/dist/stress-command.d.ts +14 -0
  38. package/dist/stress-git-checkout.d.ts +21 -0
  39. package/dist/stress-health-monitor.d.ts +46 -0
  40. package/dist/upgrade.d.ts +10 -0
  41. package/dist/user-add.d.ts +16 -0
  42. package/dist/user-offboard.d.ts +61 -0
  43. package/dist/user-runner.d.ts +5 -0
  44. package/dist/verify-user.d.ts +22 -0
  45. package/package.json +51 -40
  46. package/templates/rover-pilot/.env.schema +28 -2
  47. package/templates/rover-pilot/.github/actions/varlock-env/action.yml +47 -0
  48. package/templates/rover-pilot/.github/workflows/build.yml +72 -17
  49. package/templates/rover-pilot/.github/workflows/deploy.yml +118 -49
  50. package/templates/rover-pilot/.github/workflows/directory-sync-stress.yml +119 -0
  51. package/templates/rover-pilot/.github/workflows/health-watchdog-smoke.yml +95 -0
  52. package/templates/rover-pilot/.github/workflows/offboard.yml +107 -0
  53. package/templates/rover-pilot/.github/workflows/reconcile.yml +23 -3
  54. package/templates/rover-pilot/.github/workflows/upgrade.yml +104 -0
  55. package/templates/rover-pilot/README.md +19 -8
  56. package/templates/rover-pilot/deploy/scripts/create-predeploy-backup.ts +948 -0
  57. package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +139 -0
  58. package/templates/rover-pilot/deploy/scripts/helpers.ts +3 -0
  59. package/templates/rover-pilot/deploy/scripts/install-health-watchdog.ts +144 -0
  60. package/templates/rover-pilot/deploy/scripts/provision-server.ts +52 -22
  61. package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +19 -4
  62. package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
  63. package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +56 -14
  64. package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +183 -0
  65. package/templates/rover-pilot/deploy/scripts/update-dns.ts +72 -16
  66. package/templates/rover-pilot/deploy/scripts/validate-secrets.ts +12 -1
  67. package/templates/rover-pilot/deploy/scripts/verify-runtime-image.ts +18 -0
  68. package/templates/rover-pilot/docs/canonical-crossover-record.md +107 -0
  69. package/templates/rover-pilot/docs/onboarding-checklist.md +39 -13
  70. package/templates/rover-pilot/docs/operator-playbook.md +421 -22
  71. package/templates/rover-pilot/docs/user-onboarding.md +90 -0
  72. package/templates/rover-pilot/package.json +4 -0
  73. package/templates/rover-pilot/pilot.yaml +9 -3
  74. package/templates/rover-pilot/users/alice.yaml +5 -1
  75. package/dist/origin-ca.d.ts +0 -1
  76. package/dist/push-secrets.d.ts +0 -9
  77. package/dist/push-target.d.ts +0 -2
  78. package/dist/run-subprocess.d.ts +0 -5
  79. package/dist/user-secret-names.d.ts +0 -6
  80. package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
  81. package/templates/rover-pilot/deploy/Dockerfile +0 -15
  82. package/templates/rover-pilot/deploy/kamal/deploy.yml +0 -39
@@ -7,13 +7,24 @@ on:
7
7
  description: User handle
8
8
  required: true
9
9
  type: string
10
+ release_stale_lock:
11
+ description: Release an operator-confirmed stale Kamal deploy lock before retrying
12
+ required: false
13
+ type: boolean
14
+ default: false
10
15
  push:
11
16
  branches: ["main"]
12
17
  paths:
13
18
  - users/*/.env
14
19
  - users/*/brain.yaml
20
+ - users/*/content/**
21
+ - users/*.secrets.yaml.age
15
22
  - deploy/**
16
23
  - .github/workflows/deploy.yml
24
+ workflow_run:
25
+ workflows: [Reconcile]
26
+ types: [completed]
27
+ branches: [main]
17
28
 
18
29
  permissions:
19
30
  contents: write
@@ -21,13 +32,16 @@ permissions:
21
32
 
22
33
  jobs:
23
34
  resolve_handles:
35
+ if: >
36
+ github.event_name != 'workflow_run' ||
37
+ github.event.workflow_run.conclusion == 'success'
24
38
  runs-on: ubuntu-latest
25
39
  outputs:
26
40
  handles_json: ${{ steps.resolve.outputs.handles_json }}
27
41
  steps:
28
42
  - uses: actions/checkout@v5
29
43
  with:
30
- ref: ${{ github.sha }}
44
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
31
45
  fetch-depth: 0
32
46
 
33
47
  - uses: oven-sh/setup-bun@v2
@@ -39,7 +53,7 @@ jobs:
39
53
  id: resolve
40
54
  env:
41
55
  HANDLE_INPUT: ${{ inputs.handle || '' }}
42
- BEFORE_SHA: ${{ github.event.before || '' }}
56
+ BEFORE_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.before || '' }}
43
57
  run: bun deploy/scripts/resolve-deploy-handles.ts
44
58
 
45
59
  no_changes:
@@ -66,13 +80,22 @@ jobs:
66
80
  steps:
67
81
  - uses: actions/checkout@v5
68
82
  with:
69
- ref: ${{ github.sha }}
83
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
70
84
 
71
85
  - uses: oven-sh/setup-bun@v2
72
86
 
73
87
  - name: Install operator tooling
74
88
  run: bun install
75
89
 
90
+ - name: Load shared env via Varlock
91
+ uses: ./.github/actions/varlock-env
92
+ env:
93
+ BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
94
+
95
+ - name: Decrypt user secrets
96
+ id: user_secrets
97
+ run: bun deploy/scripts/decrypt-user-secrets.ts "$HANDLE"
98
+
76
99
  - name: Reconcile selected user config
77
100
  run: bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
78
101
 
@@ -81,23 +104,13 @@ jobs:
81
104
  run: bun deploy/scripts/resolve-user-config.ts
82
105
 
83
106
  - name: Validate selected secrets
84
- env:
85
- AI_API_KEY: ${{ secrets[steps.user_config.outputs.ai_api_key_secret_name] }}
86
- GIT_SYNC_TOKEN: ${{ secrets[steps.user_config.outputs.git_sync_token_secret_name] }}
87
- MCP_AUTH_TOKEN: ${{ secrets[steps.user_config.outputs.mcp_auth_token_secret_name] }}
88
- DISCORD_BOT_TOKEN: ${{ steps.user_config.outputs.discord_bot_token_secret_name != '' && secrets[steps.user_config.outputs.discord_bot_token_secret_name] || '' }}
89
- HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
90
- HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
91
- HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
92
- HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
93
- KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
94
- KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
95
- CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
96
- CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
97
- CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
98
- PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
99
107
  run: bun deploy/scripts/validate-secrets.ts
100
108
 
109
+ - name: Seed content repo
110
+ env:
111
+ CONTENT_REPO: ${{ steps.user_config.outputs.content_repo }}
112
+ run: bun deploy/scripts/sync-content-repo.ts
113
+
101
114
  - name: Log in to GHCR
102
115
  uses: docker/login-action@v3
103
116
  with:
@@ -105,27 +118,31 @@ jobs:
105
118
  username: ${{ github.actor }}
106
119
  password: ${{ secrets.GITHUB_TOKEN }}
107
120
 
108
- - name: Wait for shared image tag
121
+ - name: Wait for image tag
109
122
  run: |
110
- IMAGE_TAG="${{ steps.user_config.outputs.image_repository }}:brain-${{ steps.user_config.outputs.brain_version }}"
111
- for attempt in $(seq 1 24); do
123
+ # The Build workflow fires off the same config push (it derives the
124
+ # declared image set from the registry), so this wait must cover a
125
+ # full image build, not just registry propagation.
126
+ IMAGE_TAG="${{ steps.user_config.outputs.image_repository }}:${{ steps.user_config.outputs.image_tag }}"
127
+ for attempt in $(seq 1 60); do
112
128
  if docker manifest inspect "$IMAGE_TAG" >/dev/null 2>&1; then
113
129
  exit 0
114
130
  fi
115
- echo "Shared image tag not ready yet ($attempt/24): $IMAGE_TAG"
116
- sleep 10
131
+ echo "Image tag not ready yet ($attempt/60): $IMAGE_TAG"
132
+ sleep 20
117
133
  done
118
134
  echo "Timed out waiting for $IMAGE_TAG" >&2
119
135
  exit 1
120
136
 
137
+ - name: Verify runtime image packages
138
+ run: bun deploy/scripts/verify-runtime-image.ts
139
+
121
140
  - name: Install Kamal
122
141
  run: |
123
142
  gem install --user-install kamal
124
143
  ruby -r rubygems -e 'puts Gem.user_dir + "/bin"' >> "$GITHUB_PATH"
125
144
 
126
145
  - name: Write Kamal SSH key
127
- env:
128
- KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
129
146
  run: bun deploy/scripts/write-ssh-key.ts
130
147
 
131
148
  - name: Configure SSH client
@@ -142,33 +159,27 @@ jobs:
142
159
  chmod 600 ~/.ssh/config
143
160
 
144
161
  - name: Write .kamal/secrets
145
- env:
146
- AI_API_KEY: ${{ secrets[steps.user_config.outputs.ai_api_key_secret_name] }}
147
- GIT_SYNC_TOKEN: ${{ secrets[steps.user_config.outputs.git_sync_token_secret_name] }}
148
- MCP_AUTH_TOKEN: ${{ secrets[steps.user_config.outputs.mcp_auth_token_secret_name] }}
149
- DISCORD_BOT_TOKEN: ${{ steps.user_config.outputs.discord_bot_token_secret_name != '' && secrets[steps.user_config.outputs.discord_bot_token_secret_name] || '' }}
150
- KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
151
- CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
152
- PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
153
162
  run: bun deploy/scripts/write-kamal-secrets.ts
154
163
 
155
164
  - name: Provision server
156
165
  id: provision
157
166
  env:
158
- HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
159
- HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
160
- HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
161
- HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
162
167
  INSTANCE_NAME: ${{ steps.user_config.outputs.instance_name }}
163
168
  run: bun deploy/scripts/provision-server.ts
164
169
 
165
170
  - name: Update Cloudflare DNS
166
171
  env:
167
- CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
168
- CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
172
+ CF_ZONE_ID: ${{ steps.user_config.outputs.cloudflare_zone_id }}
169
173
  BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
174
+ WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
175
+ PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
170
176
  SERVER_IP: ${{ steps.provision.outputs.server_ip }}
171
- run: bun deploy/scripts/update-dns.ts
177
+ run: |
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
182
+ BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
172
183
 
173
184
  - name: Validate SSH key
174
185
  run: ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null
@@ -188,21 +199,61 @@ jobs:
188
199
  echo "SSH never became ready for $SSH_USER@$SERVER_IP" >&2
189
200
  exit 1
190
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
+
191
223
  - name: Deploy
192
224
  env:
193
225
  SERVER_IP: ${{ steps.provision.outputs.server_ip }}
194
- VERSION: brain-${{ steps.user_config.outputs.brain_version }}
226
+ VERSION: ${{ steps.user_config.outputs.image_tag }}
195
227
  IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
196
228
  REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
197
229
  BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
230
+ WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
231
+ PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
198
232
  BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
199
233
  run: kamal setup --skip-push -c deploy/kamal/deploy.yml
200
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
+
201
242
  - name: Verify origin TLS
202
243
  env:
203
244
  SERVER_IP: ${{ steps.provision.outputs.server_ip }}
204
245
  BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
205
- run: curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
246
+ WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
247
+ PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
248
+ run: |
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
253
+ curl -I -k --max-time 20 --resolve "$PREVIEW_DOMAIN:443:$SERVER_IP" "https://$PREVIEW_DOMAIN/"
254
+
255
+ - name: Verify operational health
256
+ run: bunx brains-ops verify-user "$GITHUB_WORKSPACE" "$HANDLE"
206
257
 
207
258
  - name: Upload generated config
208
259
  uses: actions/upload-artifact@v4
@@ -211,6 +262,7 @@ jobs:
211
262
  path: |
212
263
  users/${{ matrix.handle }}/brain.yaml
213
264
  users/${{ matrix.handle }}/.env
265
+ users/${{ matrix.handle }}/content
214
266
 
215
267
  - name: Dump remote proxy diagnostics
216
268
  if: failure()
@@ -237,7 +289,7 @@ jobs:
237
289
  steps:
238
290
  - uses: actions/checkout@v5
239
291
  with:
240
- ref: ${{ github.sha }}
292
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
241
293
 
242
294
  - uses: oven-sh/setup-bun@v2
243
295
 
@@ -251,16 +303,33 @@ jobs:
251
303
  merge-multiple: true
252
304
  path: .
253
305
 
254
- - name: Regenerate users table
255
- run: bunx brains-ops render "$GITHUB_WORKSPACE"
256
-
257
306
  - name: Commit generated config
258
307
  run: |
259
- if git diff --quiet -- users views; then
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
313
+
314
+ patch_file="$(mktemp)"
315
+ git diff --binary -- users > "$patch_file"
316
+ git fetch origin "${{ github.ref_name }}"
317
+ git reset --hard "origin/${{ github.ref_name }}"
318
+ if [ -s "$patch_file" ]; then
319
+ git apply --3way --index "$patch_file"
320
+ fi
321
+
322
+ # views/users.md is generated, and a concurrent deploy may have
323
+ # rewritten it since this run's checkout; render it on fresh main
324
+ # instead of merging a stale copy.
325
+ bunx brains-ops render "$GITHUB_WORKSPACE"
326
+ git add -- views
327
+
328
+ if git diff --cached --quiet -- users views; then
260
329
  exit 0
261
330
  fi
331
+
262
332
  git config user.name "github-actions[bot]"
263
333
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
264
- git add users views
265
334
  git commit -m "chore(ops): reconcile generated config"
266
- git push
335
+ git push origin HEAD:${{ github.ref_name }}
@@ -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
@@ -0,0 +1,107 @@
1
+ name: Offboard
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ handles:
7
+ description: Comma-separated pilot handles
8
+ required: true
9
+ type: string
10
+ confirm:
11
+ description: Exact confirmation (sunset:<sorted-comma-separated-handles>)
12
+ required: true
13
+ type: string
14
+ apply:
15
+ description: Archive content repos and destroy DNS, servers, and desired state
16
+ required: true
17
+ type: boolean
18
+ default: false
19
+
20
+ permissions:
21
+ contents: write
22
+
23
+ concurrency:
24
+ group: offboard-pilot-users
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ offboard:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v5
32
+ with:
33
+ ref: main
34
+ fetch-depth: 0
35
+
36
+ - uses: oven-sh/setup-bun@v2
37
+
38
+ - name: Install operator tooling
39
+ run: bun install
40
+
41
+ - name: Load provider credentials
42
+ uses: ./.github/actions/varlock-env
43
+ env:
44
+ BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
45
+
46
+ - name: Resolve handles
47
+ id: handles
48
+ env:
49
+ OFFBOARD_HANDLES: ${{ inputs.handles }}
50
+ run: |
51
+ node <<'NODE'
52
+ const handles = [...new Set(process.env.OFFBOARD_HANDLES
53
+ .split(',')
54
+ .map((handle) => handle.trim())
55
+ .filter(Boolean))]
56
+ .sort((left, right) => left.localeCompare(right));
57
+ if (handles.length === 0 || handles.some((handle) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(handle))) {
58
+ throw new Error('Invalid offboarding handle list');
59
+ }
60
+ const output = process.env.GITHUB_OUTPUT;
61
+ require('node:fs').appendFileSync(output, `json=${JSON.stringify(handles)}\n`);
62
+ NODE
63
+
64
+ - name: Preview offboarding plan
65
+ env:
66
+ OFFBOARD_HANDLES_JSON: ${{ steps.handles.outputs.json }}
67
+ run: |
68
+ mapfile -t handles < <(node -e 'for (const handle of JSON.parse(process.env.OFFBOARD_HANDLES_JSON)) console.log(handle)')
69
+ bunx brains-ops user:offboard "$GITHUB_WORKSPACE" "${handles[@]}"
70
+
71
+ - name: Apply offboarding plan
72
+ if: inputs.apply
73
+ env:
74
+ OFFBOARD_CONFIRM: ${{ inputs.confirm }}
75
+ OFFBOARD_HANDLES_JSON: ${{ steps.handles.outputs.json }}
76
+ run: |
77
+ mapfile -t handles < <(node -e 'for (const handle of JSON.parse(process.env.OFFBOARD_HANDLES_JSON)) console.log(handle)')
78
+ bunx brains-ops user:offboard "$GITHUB_WORKSPACE" "${handles[@]}" \
79
+ --apply --confirm "$OFFBOARD_CONFIRM"
80
+
81
+ - name: Validate changed paths
82
+ if: inputs.apply
83
+ run: |
84
+ unexpected="$(git status --short | awk '{print $2}' | grep -Ev '^(cohorts/|users/|views/users\.md$)' || true)"
85
+ if [ -n "$unexpected" ]; then
86
+ echo "Offboarding changed unexpected paths:" >&2
87
+ echo "$unexpected" >&2
88
+ exit 1
89
+ fi
90
+
91
+ - name: Commit desired-state removal
92
+ if: inputs.apply
93
+ env:
94
+ OFFBOARD_HANDLES: ${{ inputs.handles }}
95
+ run: |
96
+ git add --all -- cohorts users views/users.md
97
+ if git diff --cached --quiet; then
98
+ echo "No desired-state changes remain to commit."
99
+ exit 0
100
+ fi
101
+
102
+ git config user.name "github-actions[bot]"
103
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
104
+ git commit -m "chore(ops): sunset pilot brains" \
105
+ -m "Retired handles: $OFFBOARD_HANDLES"
106
+ git pull --rebase origin main
107
+ git push origin HEAD:main
@@ -35,11 +35,31 @@ jobs:
35
35
 
36
36
  - name: Commit generated outputs
37
37
  run: |
38
- if git diff --quiet -- views users; then
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
47
+
48
+ git fetch origin "${{ github.ref_name }}"
49
+ if git diff --quiet "origin/${{ github.ref_name }}" -- users; then
50
+ exit 0
51
+ fi
52
+
53
+ patch_file="$(mktemp)"
54
+ git diff --binary -- 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 -- 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
- git push
65
+ git push origin HEAD:${{ github.ref_name }}