@rizom/ops 0.2.0-alpha.29 → 0.2.0-alpha.290
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 +39 -2
- package/dist/brains-ops.js +404 -314
- package/dist/cert-bootstrap.d.ts +3 -1
- package/dist/content-repo-ref.d.ts +10 -0
- package/dist/content-repo.d.ts +1 -0
- package/dist/deploy.js +99 -166
- package/dist/directory-sync-stress-system.d.ts +74 -0
- package/dist/directory-sync-stress.d.ts +106 -0
- package/dist/entries/deploy.d.ts +3 -2
- package/dist/images.d.ts +79 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +479 -298
- package/dist/legacy-pilot-migration.d.ts +11 -0
- package/dist/load-registry.d.ts +57 -6
- package/dist/observed-status.d.ts +1 -1
- package/dist/origin-ca.d.ts +1 -1
- package/dist/parse-args.d.ts +2 -10
- package/dist/preview-domain.d.ts +9 -0
- package/dist/push-secrets.d.ts +2 -9
- package/dist/push-target.d.ts +1 -2
- package/dist/reconcile-dry-run.d.ts +9 -0
- package/dist/run-command.d.ts +12 -3
- package/dist/run-subprocess.d.ts +1 -6
- package/dist/schema.d.ts +114 -162
- package/dist/secrets-encrypt.d.ts +7 -13
- package/dist/ssh-key-bootstrap.d.ts +1 -26
- package/dist/stage-legacy-crossover.d.ts +20 -0
- package/dist/stress-command.d.ts +14 -0
- package/dist/stress-git-checkout.d.ts +21 -0
- package/dist/stress-health-monitor.d.ts +46 -0
- package/dist/upgrade.d.ts +10 -0
- package/dist/user-add.d.ts +15 -0
- package/dist/verify-user.d.ts +22 -0
- package/package.json +47 -42
- package/templates/rover-pilot/.env.schema +24 -3
- package/templates/rover-pilot/.github/actions/varlock-env/action.yml +47 -0
- package/templates/rover-pilot/.github/workflows/build.yml +70 -17
- package/templates/rover-pilot/.github/workflows/deploy.yml +74 -61
- package/templates/rover-pilot/.github/workflows/directory-sync-stress.yml +119 -0
- package/templates/rover-pilot/.github/workflows/reconcile.yml +10 -4
- package/templates/rover-pilot/.github/workflows/upgrade.yml +104 -0
- package/templates/rover-pilot/README.md +16 -6
- package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +80 -24
- 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/resolve-deploy-handles.ts +12 -3
- package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
- package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +44 -9
- package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +51 -47
- package/templates/rover-pilot/deploy/scripts/update-dns.ts +14 -4
- package/templates/rover-pilot/deploy/scripts/validate-secrets.ts +12 -1
- package/templates/rover-pilot/docs/canonical-crossover-record.md +104 -0
- package/templates/rover-pilot/docs/onboarding-checklist.md +28 -17
- package/templates/rover-pilot/docs/operator-playbook.md +259 -29
- package/templates/rover-pilot/docs/user-onboarding.md +47 -462
- package/templates/rover-pilot/pilot.yaml +3 -4
- package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
- package/templates/rover-pilot/deploy/Dockerfile +0 -30
- package/templates/rover-pilot/deploy/kamal/deploy.yml +0 -40
|
@@ -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:
|
|
@@ -16,6 +21,10 @@ on:
|
|
|
16
21
|
- users/*.secrets.yaml.age
|
|
17
22
|
- deploy/**
|
|
18
23
|
- .github/workflows/deploy.yml
|
|
24
|
+
workflow_run:
|
|
25
|
+
workflows: [Reconcile]
|
|
26
|
+
types: [completed]
|
|
27
|
+
branches: [main]
|
|
19
28
|
|
|
20
29
|
permissions:
|
|
21
30
|
contents: write
|
|
@@ -23,13 +32,16 @@ permissions:
|
|
|
23
32
|
|
|
24
33
|
jobs:
|
|
25
34
|
resolve_handles:
|
|
35
|
+
if: >
|
|
36
|
+
github.event_name != 'workflow_run' ||
|
|
37
|
+
github.event.workflow_run.conclusion == 'success'
|
|
26
38
|
runs-on: ubuntu-latest
|
|
27
39
|
outputs:
|
|
28
40
|
handles_json: ${{ steps.resolve.outputs.handles_json }}
|
|
29
41
|
steps:
|
|
30
42
|
- uses: actions/checkout@v5
|
|
31
43
|
with:
|
|
32
|
-
ref: ${{ github.sha }}
|
|
44
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
33
45
|
fetch-depth: 0
|
|
34
46
|
|
|
35
47
|
- uses: oven-sh/setup-bun@v2
|
|
@@ -41,7 +53,7 @@ jobs:
|
|
|
41
53
|
id: resolve
|
|
42
54
|
env:
|
|
43
55
|
HANDLE_INPUT: ${{ inputs.handle || '' }}
|
|
44
|
-
BEFORE_SHA: ${{ github.event.before || '' }}
|
|
56
|
+
BEFORE_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.before || '' }}
|
|
45
57
|
run: bun deploy/scripts/resolve-deploy-handles.ts
|
|
46
58
|
|
|
47
59
|
no_changes:
|
|
@@ -68,58 +80,36 @@ jobs:
|
|
|
68
80
|
steps:
|
|
69
81
|
- uses: actions/checkout@v5
|
|
70
82
|
with:
|
|
71
|
-
ref: ${{ github.sha }}
|
|
83
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
72
84
|
|
|
73
85
|
- uses: oven-sh/setup-bun@v2
|
|
74
86
|
|
|
75
87
|
- name: Install operator tooling
|
|
76
88
|
run: bun install
|
|
77
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
|
+
|
|
78
95
|
- name: Decrypt user secrets
|
|
79
96
|
id: user_secrets
|
|
80
|
-
env:
|
|
81
|
-
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
|
82
97
|
run: bun deploy/scripts/decrypt-user-secrets.ts "$HANDLE"
|
|
83
98
|
|
|
84
99
|
- name: Reconcile selected user config
|
|
85
|
-
|
|
86
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
87
|
-
run: |
|
|
88
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
89
|
-
bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
100
|
+
run: bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
90
101
|
|
|
91
102
|
- name: Resolve generated user config
|
|
92
103
|
id: user_config
|
|
93
104
|
run: bun deploy/scripts/resolve-user-config.ts
|
|
94
105
|
|
|
95
106
|
- name: Validate selected secrets
|
|
96
|
-
|
|
97
|
-
SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
|
|
98
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
99
|
-
SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
|
|
100
|
-
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
101
|
-
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
102
|
-
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
103
|
-
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
104
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
105
|
-
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
106
|
-
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
107
|
-
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
108
|
-
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
109
|
-
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
110
|
-
run: |
|
|
111
|
-
export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
|
|
112
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
113
|
-
export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
|
|
114
|
-
bun deploy/scripts/validate-secrets.ts
|
|
107
|
+
run: bun deploy/scripts/validate-secrets.ts
|
|
115
108
|
|
|
116
109
|
- name: Seed content repo
|
|
117
110
|
env:
|
|
118
111
|
CONTENT_REPO: ${{ steps.user_config.outputs.content_repo }}
|
|
119
|
-
|
|
120
|
-
run: |
|
|
121
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
122
|
-
bun deploy/scripts/sync-content-repo.ts
|
|
112
|
+
run: bun deploy/scripts/sync-content-repo.ts
|
|
123
113
|
|
|
124
114
|
- name: Log in to GHCR
|
|
125
115
|
uses: docker/login-action@v3
|
|
@@ -128,15 +118,18 @@ jobs:
|
|
|
128
118
|
username: ${{ github.actor }}
|
|
129
119
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
130
120
|
|
|
131
|
-
- name: Wait for
|
|
121
|
+
- name: Wait for image tag
|
|
132
122
|
run: |
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
135
128
|
if docker manifest inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
|
136
129
|
exit 0
|
|
137
130
|
fi
|
|
138
|
-
echo "
|
|
139
|
-
sleep
|
|
131
|
+
echo "Image tag not ready yet ($attempt/60): $IMAGE_TAG"
|
|
132
|
+
sleep 20
|
|
140
133
|
done
|
|
141
134
|
echo "Timed out waiting for $IMAGE_TAG" >&2
|
|
142
135
|
exit 1
|
|
@@ -147,8 +140,6 @@ jobs:
|
|
|
147
140
|
ruby -r rubygems -e 'puts Gem.user_dir + "/bin"' >> "$GITHUB_PATH"
|
|
148
141
|
|
|
149
142
|
- name: Write Kamal SSH key
|
|
150
|
-
env:
|
|
151
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
152
143
|
run: bun deploy/scripts/write-ssh-key.ts
|
|
153
144
|
|
|
154
145
|
- name: Configure SSH client
|
|
@@ -165,38 +156,26 @@ jobs:
|
|
|
165
156
|
chmod 600 ~/.ssh/config
|
|
166
157
|
|
|
167
158
|
- name: Write .kamal/secrets
|
|
168
|
-
|
|
169
|
-
SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
|
|
170
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
171
|
-
SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
|
|
172
|
-
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
173
|
-
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
174
|
-
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
175
|
-
run: |
|
|
176
|
-
export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
|
|
177
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
178
|
-
export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
|
|
179
|
-
bun deploy/scripts/write-kamal-secrets.ts
|
|
159
|
+
run: bun deploy/scripts/write-kamal-secrets.ts
|
|
180
160
|
|
|
181
161
|
- name: Provision server
|
|
182
162
|
id: provision
|
|
183
163
|
env:
|
|
184
|
-
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
185
|
-
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
186
|
-
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
187
|
-
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
188
164
|
INSTANCE_NAME: ${{ steps.user_config.outputs.instance_name }}
|
|
189
165
|
run: bun deploy/scripts/provision-server.ts
|
|
190
166
|
|
|
191
167
|
- name: Update Cloudflare DNS
|
|
192
168
|
env:
|
|
193
|
-
|
|
194
|
-
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
169
|
+
CF_ZONE_ID: ${{ steps.user_config.outputs.cloudflare_zone_id }}
|
|
195
170
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
171
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
196
172
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
197
173
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
198
174
|
run: |
|
|
199
175
|
bun deploy/scripts/update-dns.ts
|
|
176
|
+
if [ -n "$WWW_DOMAIN" ]; then
|
|
177
|
+
BRAIN_DOMAIN="$WWW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
178
|
+
fi
|
|
200
179
|
BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
201
180
|
|
|
202
181
|
- name: Validate SSH key
|
|
@@ -217,26 +196,54 @@ jobs:
|
|
|
217
196
|
echo "SSH never became ready for $SSH_USER@$SERVER_IP" >&2
|
|
218
197
|
exit 1
|
|
219
198
|
|
|
199
|
+
- name: Release operator-confirmed stale deploy lock
|
|
200
|
+
if: ${{ inputs.release_stale_lock }}
|
|
201
|
+
env:
|
|
202
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
203
|
+
VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
204
|
+
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
205
|
+
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
206
|
+
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
207
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
208
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
209
|
+
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
210
|
+
run: kamal lock release -c deploy/kamal/deploy.yml
|
|
211
|
+
|
|
220
212
|
- name: Deploy
|
|
221
213
|
env:
|
|
222
214
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
223
|
-
VERSION:
|
|
215
|
+
VERSION: ${{ steps.user_config.outputs.image_tag }}
|
|
224
216
|
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
225
217
|
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
226
218
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
219
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
227
220
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
228
221
|
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
229
222
|
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
230
223
|
|
|
224
|
+
- name: Install container health watchdog
|
|
225
|
+
env:
|
|
226
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
227
|
+
run: |
|
|
228
|
+
SSH_USER="$(ruby -e 'require "yaml"; config = YAML.load_file("deploy/kamal/deploy.yml") || {}; puts(config.dig("ssh", "user") || "root")')" \
|
|
229
|
+
bun deploy/scripts/install-health-watchdog.ts
|
|
230
|
+
|
|
231
231
|
- name: Verify origin TLS
|
|
232
232
|
env:
|
|
233
233
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
234
234
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
235
|
+
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
235
236
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
236
237
|
run: |
|
|
237
|
-
curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
|
|
238
|
+
curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health/live"
|
|
239
|
+
if [ -n "$WWW_DOMAIN" ]; then
|
|
240
|
+
curl -I -k --max-time 20 --resolve "$WWW_DOMAIN:443:$SERVER_IP" "https://$WWW_DOMAIN/"
|
|
241
|
+
fi
|
|
238
242
|
curl -I -k --max-time 20 --resolve "$PREVIEW_DOMAIN:443:$SERVER_IP" "https://$PREVIEW_DOMAIN/"
|
|
239
243
|
|
|
244
|
+
- name: Verify operational health
|
|
245
|
+
run: bunx brains-ops verify-user "$GITHUB_WORKSPACE" "$HANDLE"
|
|
246
|
+
|
|
240
247
|
- name: Upload generated config
|
|
241
248
|
uses: actions/upload-artifact@v4
|
|
242
249
|
with:
|
|
@@ -271,7 +278,7 @@ jobs:
|
|
|
271
278
|
steps:
|
|
272
279
|
- uses: actions/checkout@v5
|
|
273
280
|
with:
|
|
274
|
-
ref: ${{ github.sha }}
|
|
281
|
+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.sha }}
|
|
275
282
|
|
|
276
283
|
- uses: oven-sh/setup-bun@v2
|
|
277
284
|
|
|
@@ -290,6 +297,12 @@ jobs:
|
|
|
290
297
|
|
|
291
298
|
- name: Commit generated config
|
|
292
299
|
run: |
|
|
300
|
+
# A newly added user's generated directory is untracked, and
|
|
301
|
+
# `git diff` cannot see untracked files — without intent-to-add
|
|
302
|
+
# the new config is silently dropped here and every later deploy
|
|
303
|
+
# skips that user (their files never appear in a commit range).
|
|
304
|
+
git add --intent-to-add -- users views
|
|
305
|
+
|
|
293
306
|
if git diff --quiet -- users views; then
|
|
294
307
|
exit 0
|
|
295
308
|
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
|
|
@@ -35,21 +35,27 @@ jobs:
|
|
|
35
35
|
|
|
36
36
|
- name: Commit generated outputs
|
|
37
37
|
run: |
|
|
38
|
-
|
|
38
|
+
# A newly added user's generated directory is untracked, and
|
|
39
|
+
# `git diff` cannot see untracked files — without intent-to-add
|
|
40
|
+
# the new config is silently dropped here and every later deploy
|
|
41
|
+
# skips that user (their files never appear in a commit range).
|
|
42
|
+
git add --intent-to-add -- users
|
|
43
|
+
|
|
44
|
+
if git diff --quiet -- users; then
|
|
39
45
|
exit 0
|
|
40
46
|
fi
|
|
41
47
|
|
|
42
48
|
git fetch origin "${{ github.ref_name }}"
|
|
43
|
-
if git diff --quiet "origin/${{ github.ref_name }}" --
|
|
49
|
+
if git diff --quiet "origin/${{ github.ref_name }}" -- users; then
|
|
44
50
|
exit 0
|
|
45
51
|
fi
|
|
46
52
|
|
|
47
53
|
patch_file="$(mktemp)"
|
|
48
|
-
git diff --binary --
|
|
54
|
+
git diff --binary -- users > "$patch_file"
|
|
49
55
|
git reset --hard "origin/${{ github.ref_name }}"
|
|
50
56
|
git apply --3way --index "$patch_file"
|
|
51
57
|
|
|
52
|
-
if git diff --cached --quiet --
|
|
58
|
+
if git diff --cached --quiet -- users; then
|
|
53
59
|
exit 0
|
|
54
60
|
fi
|
|
55
61
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
name: Upgrade
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version:
|
|
7
|
+
description: Explicit @rizom/ops version (empty means latest)
|
|
8
|
+
required: false
|
|
9
|
+
type: string
|
|
10
|
+
schedule:
|
|
11
|
+
# Weekday morning check for new operator tooling releases.
|
|
12
|
+
- cron: "17 6 * * 1-5"
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: upgrade
|
|
19
|
+
cancel-in-progress: false
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
upgrade:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
# Check only presence here; the private key itself is never exposed to
|
|
26
|
+
# this step. A missing private-key secret fails before checkout.
|
|
27
|
+
- name: Require upgrade App configuration
|
|
28
|
+
env:
|
|
29
|
+
APP_ID: ${{ vars.OPS_UPGRADE_APP_ID }}
|
|
30
|
+
HAS_PRIVATE_KEY: ${{ secrets.OPS_UPGRADE_APP_PRIVATE_KEY != '' }}
|
|
31
|
+
run: |
|
|
32
|
+
missing=""
|
|
33
|
+
if [ -z "$APP_ID" ]; then
|
|
34
|
+
missing="OPS_UPGRADE_APP_ID"
|
|
35
|
+
fi
|
|
36
|
+
if [ "$HAS_PRIVATE_KEY" != "true" ]; then
|
|
37
|
+
missing="${missing:+$missing and }OPS_UPGRADE_APP_PRIVATE_KEY"
|
|
38
|
+
fi
|
|
39
|
+
if [ -n "$missing" ]; then
|
|
40
|
+
echo "$missing is not set for this repository." >&2
|
|
41
|
+
echo "Configure the upgrade GitHub App as described in docs/operator-playbook.md." >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# No credential is persisted in .git/config while the workflow installs
|
|
46
|
+
# and executes a freshly published @rizom/ops package.
|
|
47
|
+
- uses: actions/checkout@v5
|
|
48
|
+
with:
|
|
49
|
+
persist-credentials: false
|
|
50
|
+
|
|
51
|
+
- uses: oven-sh/setup-bun@v2
|
|
52
|
+
|
|
53
|
+
- name: Install operator tooling
|
|
54
|
+
run: bun install
|
|
55
|
+
|
|
56
|
+
- name: Upgrade operator tooling
|
|
57
|
+
env:
|
|
58
|
+
VERSION_INPUT: ${{ inputs.version }}
|
|
59
|
+
run: bunx brains-ops upgrade . --to "${VERSION_INPUT:-latest}"
|
|
60
|
+
|
|
61
|
+
- name: Detect upgrade changes
|
|
62
|
+
id: upgrade-changes
|
|
63
|
+
run: |
|
|
64
|
+
if [ -z "$(git status --porcelain)" ]; then
|
|
65
|
+
echo "Already up to date."
|
|
66
|
+
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
67
|
+
else
|
|
68
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
- name: Create repository-scoped upgrade token
|
|
72
|
+
if: steps.upgrade-changes.outputs.changed == 'true'
|
|
73
|
+
id: upgrade-token
|
|
74
|
+
uses: actions/create-github-app-token@v2
|
|
75
|
+
with:
|
|
76
|
+
app-id: ${{ vars.OPS_UPGRADE_APP_ID }}
|
|
77
|
+
private-key: ${{ secrets.OPS_UPGRADE_APP_PRIVATE_KEY }}
|
|
78
|
+
permission-contents: write
|
|
79
|
+
permission-pull-requests: write
|
|
80
|
+
permission-workflows: write
|
|
81
|
+
|
|
82
|
+
- name: Open upgrade PR
|
|
83
|
+
if: steps.upgrade-changes.outputs.changed == 'true'
|
|
84
|
+
env:
|
|
85
|
+
GH_TOKEN: ${{ steps.upgrade-token.outputs.token }}
|
|
86
|
+
run: |
|
|
87
|
+
VERSION=$(bun -e 'console.log(JSON.parse(await Bun.file("package.json").text()).devDependencies["@rizom/ops"])')
|
|
88
|
+
BRANCH="ops/upgrade-rizom-ops-$VERSION"
|
|
89
|
+
git config user.name "github-actions[bot]"
|
|
90
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
91
|
+
git checkout -B "$BRANCH"
|
|
92
|
+
git add -A
|
|
93
|
+
git commit -m "chore(ops): upgrade @rizom/ops to $VERSION"
|
|
94
|
+
git push --force \
|
|
95
|
+
"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \
|
|
96
|
+
"$BRANCH"
|
|
97
|
+
if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then
|
|
98
|
+
echo "PR already open for $BRANCH"
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
gh pr create \
|
|
102
|
+
--title "chore(ops): upgrade @rizom/ops to $VERSION" \
|
|
103
|
+
--body "Automated operator tooling upgrade. Review the scaffold diff, then merge; deploys stay operator-dispatched." \
|
|
104
|
+
--head "$BRANCH"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# private brain pilot
|
|
2
2
|
|
|
3
|
-
Private desired-state
|
|
3
|
+
Private desired-state repository for the hosted brain pilot. The generated repository/service name may remain `rover-pilot` for deployment identity compatibility.
|
|
4
4
|
|
|
5
5
|
This is a single operator-owned repo. Pilot users do not get their own brain repos.
|
|
6
6
|
Per-user deploy config lives under `users/<handle>/`, while content stays in per-user content repos.
|
|
@@ -29,18 +29,28 @@ The repo also checks in its deploy contract:
|
|
|
29
29
|
- `.github/workflows/*`
|
|
30
30
|
|
|
31
31
|
`.env.schema` is the single source of truth for required and sensitive deploy vars.
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
Use separate GitHub tokens: `CONTENT_REPO_ADMIN_TOKEN` for operator-side content repo creation/checks, and `GIT_SYNC_TOKEN` for runtime directory-sync git access.
|
|
33
|
+
The default pilot image tag is `brain-${brainVersion}` end to end. A user with `siteOverride` gets an isolated `brain-${brainVersion}-sites-${packageHash}` image instead.
|
|
34
|
+
When the effective brain version (`pilot.yaml.brainVersion`, or a cohort override) changes and you push, CI rebuilds the required default/site tags, refreshes generated user env files, and redeploys affected users. Every external site and theme package uses its own required exact version pin; ops never infers either version from the brain or from another package.
|
|
34
35
|
When a push changes only deploy contract files, CI prints `No affected user configs; skipping deploy.` and stops before Kamal.
|
|
35
36
|
|
|
36
37
|
## Commands
|
|
37
38
|
|
|
38
39
|
- `brains-ops init <repo>`
|
|
39
|
-
- `brains-ops
|
|
40
|
-
- `brains-ops
|
|
40
|
+
- `brains-ops upgrade <repo>` — bumps `@rizom/ops` (or `--to <version>`) and refreshes the generated scaffold; the Upgrade workflow runs this on a schedule and opens a PR
|
|
41
|
+
- `brains-ops render <repo>` — regenerates `views/users.md` with live DNS, `/health/ready`, and unauthenticated `/mcp` status checks
|
|
42
|
+
- `brains-ops user:add <repo> <handle> --cohort <cohort>` — scaffolds a user file, per-user secrets template, and cohort membership
|
|
43
|
+
- `brains-ops onboard <repo> <handle>` — creates/seeds the user's content repo with separate admin and sync tokens
|
|
41
44
|
- `brains-ops age-key:bootstrap <repo>`
|
|
42
45
|
- `brains-ops ssh-key:bootstrap <repo>`
|
|
43
46
|
- `brains-ops cert:bootstrap <repo>`
|
|
44
47
|
- `brains-ops secrets:encrypt <repo> <handle>`
|
|
45
48
|
- `brains-ops reconcile-cohort <repo> <cohort>`
|
|
46
49
|
- `brains-ops reconcile-all <repo>`
|
|
50
|
+
- `brains-ops reconcile-all <repo> --dry-run` (isolated, no external content-repository access; lists both passes' changed files)
|
|
51
|
+
|
|
52
|
+
`render` owns the observational `views/users.md` projection. Reconcile owns generated per-user config and never rewrites observed status rows.
|
|
53
|
+
|
|
54
|
+
The Upgrade workflow uses a repository-scoped GitHub App token because an Actions `GITHUB_TOKEN` cannot update generated files under `.github/workflows/`. Checkout persists no credential, and the App token is minted only after the freshly published tooling finishes and only when there is a change to push. Configure the App and its `OPS_UPGRADE_APP_ID` Actions variable and `OPS_UPGRADE_APP_PRIVATE_KEY` Actions secret as described in `docs/operator-playbook.md`. If token creation or the upgrade push fails, stop; do not substitute an operator's personal credentials.
|
|
55
|
+
|
|
56
|
+
Use `docs/canonical-crossover-record.md` to record exact forward and rollback artifact pins before an approved crossover window.
|