@rizom/ops 0.2.0-alpha.44 → 0.2.0-alpha.441

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 (70) hide show
  1. package/README.md +82 -1
  2. package/dist/age-key-bootstrap.d.ts +1 -1
  3. package/dist/brains-ops.js +736 -335
  4. package/dist/capability-bundle-migration.d.ts +22 -0
  5. package/dist/cert-bootstrap.d.ts +4 -2
  6. package/dist/content-repo-ref.d.ts +10 -0
  7. package/dist/content-repo.d.ts +1 -1
  8. package/dist/deploy.js +110 -166
  9. package/dist/directory-sync-stress-system.d.ts +75 -0
  10. package/dist/directory-sync-stress.d.ts +117 -0
  11. package/dist/entries/deploy.d.ts +4 -2
  12. package/dist/health-watchdog-smoke.d.ts +54 -0
  13. package/dist/image-inventory.d.ts +4 -0
  14. package/dist/image-types.d.ts +7 -0
  15. package/dist/images.d.ts +82 -0
  16. package/dist/index.d.ts +8 -0
  17. package/dist/index.js +736 -323
  18. package/dist/legacy-pilot-migration.d.ts +11 -0
  19. package/dist/legacy-projection-job-recovery.d.ts +29 -0
  20. package/dist/load-registry.d.ts +61 -6
  21. package/dist/observed-status.d.ts +1 -1
  22. package/dist/parse-args.d.ts +2 -12
  23. package/dist/preview-domain.d.ts +9 -0
  24. package/dist/reconcile-dry-run.d.ts +9 -0
  25. package/dist/run-command.d.ts +20 -4
  26. package/dist/schema.d.ts +126 -163
  27. package/dist/secrets-encrypt.d.ts +7 -13
  28. package/dist/secrets-push.d.ts +1 -1
  29. package/dist/ssh-key-bootstrap.d.ts +1 -26
  30. package/dist/stage-legacy-crossover.d.ts +25 -0
  31. package/dist/stress-command.d.ts +14 -0
  32. package/dist/stress-git-checkout.d.ts +21 -0
  33. package/dist/stress-health-monitor.d.ts +46 -0
  34. package/dist/upgrade.d.ts +10 -0
  35. package/dist/user-offboard.d.ts +61 -0
  36. package/dist/verify-user.d.ts +22 -0
  37. package/package.json +50 -42
  38. package/templates/rover-pilot/.env.schema +20 -5
  39. package/templates/rover-pilot/.github/actions/varlock-env/action.yml +47 -0
  40. package/templates/rover-pilot/.github/workflows/build.yml +70 -17
  41. package/templates/rover-pilot/.github/workflows/deploy.yml +74 -57
  42. package/templates/rover-pilot/.github/workflows/directory-sync-stress.yml +119 -0
  43. package/templates/rover-pilot/.github/workflows/health-watchdog-smoke.yml +95 -0
  44. package/templates/rover-pilot/.github/workflows/offboard.yml +107 -0
  45. package/templates/rover-pilot/.github/workflows/reconcile.yml +10 -4
  46. package/templates/rover-pilot/.github/workflows/upgrade.yml +104 -0
  47. package/templates/rover-pilot/README.md +13 -5
  48. package/templates/rover-pilot/deploy/scripts/create-predeploy-backup.ts +936 -0
  49. package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +80 -24
  50. package/templates/rover-pilot/deploy/scripts/helpers.ts +2 -0
  51. package/templates/rover-pilot/deploy/scripts/install-health-watchdog.ts +144 -0
  52. package/templates/rover-pilot/deploy/scripts/provision-server.ts +51 -21
  53. package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +4 -0
  54. package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
  55. package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +40 -9
  56. package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +51 -47
  57. package/templates/rover-pilot/deploy/scripts/update-dns.ts +72 -16
  58. package/templates/rover-pilot/deploy/scripts/validate-secrets.ts +12 -1
  59. package/templates/rover-pilot/deploy/scripts/verify-runtime-image.ts +18 -0
  60. package/templates/rover-pilot/docs/canonical-crossover-record.md +107 -0
  61. package/templates/rover-pilot/docs/onboarding-checklist.md +23 -14
  62. package/templates/rover-pilot/docs/operator-playbook.md +357 -29
  63. package/templates/rover-pilot/docs/user-onboarding.md +48 -463
  64. package/templates/rover-pilot/package.json +1 -0
  65. package/templates/rover-pilot/pilot.yaml +6 -4
  66. package/dist/origin-ca.d.ts +0 -1
  67. package/dist/push-secrets.d.ts +0 -9
  68. package/dist/push-target.d.ts +0 -2
  69. package/dist/run-subprocess.d.ts +0 -6
  70. 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
- env:
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
- env:
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
- SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
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 shared image tag
121
+ - name: Wait for image tag
139
122
  run: |
140
- IMAGE_TAG="${{ steps.user_config.outputs.image_repository }}:brain-${{ steps.user_config.outputs.brain_version }}"
141
- 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
142
128
  if docker manifest inspect "$IMAGE_TAG" >/dev/null 2>&1; then
143
129
  exit 0
144
130
  fi
145
- echo "Shared image tag not ready yet ($attempt/24): $IMAGE_TAG"
146
- sleep 10
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
- env:
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
- CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
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: brain-${{ steps.user_config.outputs.brain_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
@@ -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,21 +35,27 @@ 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
41
47
 
42
48
  git fetch origin "${{ github.ref_name }}"
43
- if git diff --quiet "origin/${{ github.ref_name }}" -- views users; then
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 -- views users > "$patch_file"
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 -- views users; then
58
+ if git diff --cached --quiet -- users; then
53
59
  exit 0
54
60
  fi
55
61