@rizom/ops 0.2.0-alpha.4 → 0.2.0-alpha.400

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 (81) hide show
  1. package/README.md +88 -4
  2. package/dist/age-key-bootstrap.d.ts +17 -0
  3. package/dist/brains-ops.js +824 -156
  4. package/dist/capability-bundle-migration.d.ts +22 -0
  5. package/dist/cert-bootstrap.d.ts +5 -3
  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 +82 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.js +836 -156
  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 +78 -7
  22. package/dist/observed-status.d.ts +12 -0
  23. package/dist/onboard-user.d.ts +2 -2
  24. package/dist/origin-ca.d.ts +2 -1
  25. package/dist/parse-args.d.ts +2 -10
  26. package/dist/preview-domain.d.ts +9 -0
  27. package/dist/push-secrets.d.ts +2 -9
  28. package/dist/push-target.d.ts +1 -2
  29. package/dist/reconcile-all.d.ts +2 -2
  30. package/dist/reconcile-cohort.d.ts +2 -2
  31. package/dist/reconcile-dry-run.d.ts +9 -0
  32. package/dist/reconcile-lib.d.ts +4 -2
  33. package/dist/run-command.d.ts +21 -4
  34. package/dist/run-subprocess.d.ts +1 -5
  35. package/dist/schema.d.ts +132 -69
  36. package/dist/secrets-encrypt.d.ts +26 -0
  37. package/dist/secrets-push.d.ts +1 -1
  38. package/dist/ssh-key-bootstrap.d.ts +1 -26
  39. package/dist/stage-legacy-crossover.d.ts +25 -0
  40. package/dist/stress-command.d.ts +14 -0
  41. package/dist/stress-git-checkout.d.ts +21 -0
  42. package/dist/stress-health-monitor.d.ts +46 -0
  43. package/dist/upgrade.d.ts +10 -0
  44. package/dist/user-add.d.ts +15 -0
  45. package/dist/user-offboard.d.ts +61 -0
  46. package/dist/user-runner.d.ts +5 -0
  47. package/dist/verify-user.d.ts +22 -0
  48. package/package.json +50 -40
  49. package/templates/rover-pilot/.env.schema +28 -2
  50. package/templates/rover-pilot/.github/actions/varlock-env/action.yml +47 -0
  51. package/templates/rover-pilot/.github/workflows/build.yml +71 -17
  52. package/templates/rover-pilot/.github/workflows/deploy.yml +121 -45
  53. package/templates/rover-pilot/.github/workflows/directory-sync-stress.yml +119 -0
  54. package/templates/rover-pilot/.github/workflows/health-watchdog-smoke.yml +95 -0
  55. package/templates/rover-pilot/.github/workflows/offboard.yml +107 -0
  56. package/templates/rover-pilot/.github/workflows/reconcile.yml +23 -3
  57. package/templates/rover-pilot/.github/workflows/upgrade.yml +104 -0
  58. package/templates/rover-pilot/README.md +19 -8
  59. package/templates/rover-pilot/deploy/scripts/create-predeploy-backup.ts +894 -0
  60. package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +139 -0
  61. package/templates/rover-pilot/deploy/scripts/helpers.ts +2 -0
  62. package/templates/rover-pilot/deploy/scripts/install-health-watchdog.ts +144 -0
  63. package/templates/rover-pilot/deploy/scripts/provision-server.ts +52 -22
  64. package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +19 -4
  65. package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
  66. package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +45 -14
  67. package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +183 -0
  68. package/templates/rover-pilot/deploy/scripts/update-dns.ts +72 -16
  69. package/templates/rover-pilot/deploy/scripts/validate-secrets.ts +12 -1
  70. package/templates/rover-pilot/deploy/scripts/verify-runtime-image.ts +18 -0
  71. package/templates/rover-pilot/docs/canonical-crossover-record.md +107 -0
  72. package/templates/rover-pilot/docs/onboarding-checklist.md +39 -10
  73. package/templates/rover-pilot/docs/operator-playbook.md +390 -14
  74. package/templates/rover-pilot/docs/user-onboarding.md +90 -0
  75. package/templates/rover-pilot/package.json +4 -0
  76. package/templates/rover-pilot/pilot.yaml +9 -3
  77. package/templates/rover-pilot/users/alice.yaml +5 -1
  78. package/dist/user-secret-names.d.ts +0 -6
  79. package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
  80. package/templates/rover-pilot/deploy/Dockerfile +0 -15
  81. 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,17 +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 }}
227
+ IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
228
+ REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
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 }}
232
+ BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
195
233
  run: kamal setup --skip-push -c deploy/kamal/deploy.yml
196
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
+
197
242
  - name: Verify origin TLS
198
243
  env:
199
244
  SERVER_IP: ${{ steps.provision.outputs.server_ip }}
200
245
  BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
201
- 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"
202
257
 
203
258
  - name: Upload generated config
204
259
  uses: actions/upload-artifact@v4
@@ -207,6 +262,7 @@ jobs:
207
262
  path: |
208
263
  users/${{ matrix.handle }}/brain.yaml
209
264
  users/${{ matrix.handle }}/.env
265
+ users/${{ matrix.handle }}/content
210
266
 
211
267
  - name: Dump remote proxy diagnostics
212
268
  if: failure()
@@ -233,7 +289,7 @@ jobs:
233
289
  steps:
234
290
  - uses: actions/checkout@v5
235
291
  with:
236
- ref: ${{ github.sha }}
292
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
237
293
 
238
294
  - uses: oven-sh/setup-bun@v2
239
295
 
@@ -252,11 +308,31 @@ jobs:
252
308
 
253
309
  - name: Commit generated config
254
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
+
255
317
  if git diff --quiet -- users views; then
256
318
  exit 0
257
319
  fi
320
+
321
+ git fetch origin "${{ github.ref_name }}"
322
+ if git diff --quiet "origin/${{ github.ref_name }}" -- users views; then
323
+ exit 0
324
+ fi
325
+
326
+ patch_file="$(mktemp)"
327
+ git diff --binary -- users views > "$patch_file"
328
+ git reset --hard "origin/${{ github.ref_name }}"
329
+ git apply --3way --index "$patch_file"
330
+
331
+ if git diff --cached --quiet -- users views; then
332
+ exit 0
333
+ fi
334
+
258
335
  git config user.name "github-actions[bot]"
259
336
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
260
- git add users views
261
337
  git commit -m "chore(ops): reconcile generated config"
262
- git push
338
+ 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 }}