@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.
- package/README.md +88 -4
- package/dist/age-key-bootstrap.d.ts +17 -0
- package/dist/brains-ops.js +790 -166
- package/dist/capability-bundle-migration.d.ts +22 -0
- package/dist/cert-bootstrap.d.ts +6 -4
- package/dist/content-repo-ref.d.ts +10 -0
- package/dist/content-repo.d.ts +13 -0
- package/dist/default-user-runner.d.ts +1 -1
- 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 +89 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +802 -166
- 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 +88 -6
- package/dist/observed-status.d.ts +1 -1
- package/dist/onboard-user.d.ts +2 -2
- package/dist/parse-args.d.ts +2 -10
- package/dist/preview-domain.d.ts +9 -0
- package/dist/reconcile-all.d.ts +2 -2
- package/dist/reconcile-cohort.d.ts +2 -2
- package/dist/reconcile-dry-run.d.ts +9 -0
- package/dist/reconcile-lib.d.ts +4 -2
- package/dist/run-command.d.ts +22 -6
- package/dist/runtime-image-lock.d.ts +12 -0
- package/dist/schema.d.ts +147 -69
- package/dist/secrets-encrypt.d.ts +26 -0
- package/dist/secrets-push.d.ts +2 -2
- 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 +16 -0
- package/dist/user-offboard.d.ts +61 -0
- package/dist/user-runner.d.ts +5 -0
- package/dist/verify-user.d.ts +22 -0
- package/package.json +51 -40
- package/templates/rover-pilot/.env.schema +28 -2
- package/templates/rover-pilot/.github/actions/varlock-env/action.yml +47 -0
- package/templates/rover-pilot/.github/workflows/build.yml +72 -17
- package/templates/rover-pilot/.github/workflows/deploy.yml +118 -49
- 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/offboard.yml +107 -0
- package/templates/rover-pilot/.github/workflows/reconcile.yml +23 -3
- package/templates/rover-pilot/.github/workflows/upgrade.yml +104 -0
- package/templates/rover-pilot/README.md +19 -8
- package/templates/rover-pilot/deploy/scripts/create-predeploy-backup.ts +948 -0
- package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +139 -0
- package/templates/rover-pilot/deploy/scripts/helpers.ts +3 -0
- package/templates/rover-pilot/deploy/scripts/install-health-watchdog.ts +144 -0
- package/templates/rover-pilot/deploy/scripts/provision-server.ts +52 -22
- package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +19 -4
- package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
- package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +56 -14
- package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +183 -0
- 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 +39 -13
- package/templates/rover-pilot/docs/operator-playbook.md +421 -22
- package/templates/rover-pilot/docs/user-onboarding.md +90 -0
- package/templates/rover-pilot/package.json +4 -0
- package/templates/rover-pilot/pilot.yaml +9 -3
- package/templates/rover-pilot/users/alice.yaml +5 -1
- package/dist/origin-ca.d.ts +0 -1
- package/dist/push-secrets.d.ts +0 -9
- package/dist/push-target.d.ts +0 -2
- package/dist/run-subprocess.d.ts +0 -5
- package/dist/user-secret-names.d.ts +0 -6
- package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
- package/templates/rover-pilot/deploy/Dockerfile +0 -15
- 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
|
|
121
|
+
- name: Wait for image tag
|
|
109
122
|
run: |
|
|
110
|
-
|
|
111
|
-
|
|
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 "
|
|
116
|
-
sleep
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }}
|