@rizom/ops 0.2.0-alpha.170 → 0.2.0-alpha.172
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/dist/brains-ops.js +2 -2
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/templates/rover-pilot/.github/workflows/build.yml +48 -13
- package/templates/rover-pilot/.github/workflows/deploy.yml +67 -73
- package/templates/rover-pilot/deploy/scripts/helpers.ts +3 -0
- package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
- package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +26 -53
- package/templates/rover-pilot/deploy/scripts/resolve-build-config.ts +0 -53
package/package.json
CHANGED
|
@@ -4,17 +4,22 @@ on:
|
|
|
4
4
|
workflow_dispatch:
|
|
5
5
|
inputs:
|
|
6
6
|
brain_version:
|
|
7
|
-
description:
|
|
7
|
+
description: Force one explicit build at this brain version (skips the registry-derived resolve)
|
|
8
|
+
required: false
|
|
9
|
+
type: string
|
|
10
|
+
site_packages:
|
|
11
|
+
description: 'Space-separated site packages for a per-instance site image (e.g. "@rizom/site-rizom-ai@0.2.0-alpha.148"). Empty builds the default fleet image.'
|
|
8
12
|
required: false
|
|
9
13
|
type: string
|
|
10
14
|
push:
|
|
11
15
|
branches: ["main"]
|
|
12
16
|
paths:
|
|
17
|
+
# The image set is a pure function of the declared fleet state: a push
|
|
18
|
+
# that changes it builds exactly what it declares, so the deploy (fired
|
|
19
|
+
# off the same push via Reconcile) finds its tag ready.
|
|
13
20
|
- pilot.yaml
|
|
14
|
-
- cohorts
|
|
21
|
+
- cohorts/**
|
|
15
22
|
- users/*.yaml
|
|
16
|
-
- package.json
|
|
17
|
-
- bun.lock
|
|
18
23
|
- deploy/**
|
|
19
24
|
- .github/workflows/build.yml
|
|
20
25
|
|
|
@@ -23,8 +28,10 @@ permissions:
|
|
|
23
28
|
packages: write
|
|
24
29
|
|
|
25
30
|
jobs:
|
|
26
|
-
|
|
31
|
+
resolve:
|
|
27
32
|
runs-on: ubuntu-latest
|
|
33
|
+
outputs:
|
|
34
|
+
images_json: ${{ steps.resolve.outputs.images_json }}
|
|
28
35
|
steps:
|
|
29
36
|
- uses: actions/checkout@v5
|
|
30
37
|
with:
|
|
@@ -35,12 +42,40 @@ jobs:
|
|
|
35
42
|
- name: Install operator tooling
|
|
36
43
|
run: bun install
|
|
37
44
|
|
|
38
|
-
- name:
|
|
45
|
+
- name: Log in to GHCR
|
|
46
|
+
uses: docker/login-action@v4
|
|
47
|
+
with:
|
|
48
|
+
registry: ghcr.io
|
|
49
|
+
username: ${{ github.actor }}
|
|
50
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
+
|
|
52
|
+
- name: Resolve images to build
|
|
53
|
+
id: resolve
|
|
39
54
|
env:
|
|
40
55
|
BRAIN_VERSION_INPUT: ${{ inputs.brain_version || '' }}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
SITE_PACKAGES_INPUT: ${{ inputs.site_packages || '' }}
|
|
57
|
+
run: bun deploy/scripts/resolve-missing-images.ts
|
|
58
|
+
|
|
59
|
+
no_builds:
|
|
60
|
+
needs: resolve
|
|
61
|
+
if: needs.resolve.outputs.images_json == '[]'
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- name: Skip when every declared image exists
|
|
65
|
+
run: echo "All declared images already exist; nothing to build."
|
|
66
|
+
|
|
67
|
+
build:
|
|
68
|
+
needs: resolve
|
|
69
|
+
if: needs.resolve.outputs.images_json != '[]'
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
strategy:
|
|
72
|
+
fail-fast: false
|
|
73
|
+
matrix:
|
|
74
|
+
image: ${{ fromJson(needs.resolve.outputs.images_json) }}
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v5
|
|
77
|
+
with:
|
|
78
|
+
ref: ${{ github.sha }}
|
|
44
79
|
|
|
45
80
|
- name: Set up Docker Buildx
|
|
46
81
|
uses: docker/setup-buildx-action@v4
|
|
@@ -49,9 +84,9 @@ jobs:
|
|
|
49
84
|
id: meta
|
|
50
85
|
uses: docker/metadata-action@v6
|
|
51
86
|
with:
|
|
52
|
-
images:
|
|
87
|
+
images: ghcr.io/${{ github.repository }}
|
|
53
88
|
tags: |
|
|
54
|
-
type=raw,value=${{
|
|
89
|
+
type=raw,value=${{ matrix.image.tag }}
|
|
55
90
|
|
|
56
91
|
- name: Log in to GHCR
|
|
57
92
|
uses: docker/login-action@v4
|
|
@@ -68,8 +103,8 @@ jobs:
|
|
|
68
103
|
target: fleet
|
|
69
104
|
push: true
|
|
70
105
|
build-args: |
|
|
71
|
-
BRAIN_VERSION=${{
|
|
72
|
-
SITE_PACKAGES=${{
|
|
106
|
+
BRAIN_VERSION=${{ matrix.image.brain_version }}
|
|
107
|
+
SITE_PACKAGES=${{ matrix.image.site_packages }}
|
|
73
108
|
tags: ${{ steps.meta.outputs.tags }}
|
|
74
109
|
labels: |
|
|
75
110
|
${{ steps.meta.outputs.labels }}
|
|
@@ -10,7 +10,6 @@ on:
|
|
|
10
10
|
push:
|
|
11
11
|
branches: ["main"]
|
|
12
12
|
paths:
|
|
13
|
-
- users/*.yaml
|
|
14
13
|
- users/*/.env
|
|
15
14
|
- users/*/brain.yaml
|
|
16
15
|
- users/*/content/**
|
|
@@ -83,72 +82,99 @@ jobs:
|
|
|
83
82
|
- name: Install operator tooling
|
|
84
83
|
run: bun install
|
|
85
84
|
|
|
85
|
+
- name: Load shared env via varlock
|
|
86
|
+
env:
|
|
87
|
+
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
|
|
88
|
+
run: |
|
|
89
|
+
for attempt in 1 2 3; do
|
|
90
|
+
if bunx varlock@1.1.0 load --path .env.schema --format json --compact > /tmp/varlock-env.json; then
|
|
91
|
+
break
|
|
92
|
+
fi
|
|
93
|
+
if [ "$attempt" = "3" ]; then
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
echo "Varlock load failed (attempt $attempt/3); retrying in 5s..."
|
|
97
|
+
sleep 5
|
|
98
|
+
done
|
|
99
|
+
node <<'NODE'
|
|
100
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
101
|
+
const env = JSON.parse(readFileSync('/tmp/varlock-env.json', 'utf8'));
|
|
102
|
+
const githubEnvPath = process.env.GITHUB_ENV;
|
|
103
|
+
if (!githubEnvPath) {
|
|
104
|
+
throw new Error('Missing GITHUB_ENV');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const newline = String.fromCharCode(10);
|
|
108
|
+
const carriageReturn = String.fromCharCode(13);
|
|
109
|
+
const chunks = [];
|
|
110
|
+
for (const [key, value] of Object.entries(env)) {
|
|
111
|
+
if (key === 'BWS_ACCESS_TOKEN' || value === null || value === undefined) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const text = String(value)
|
|
116
|
+
.split(carriageReturn + newline)
|
|
117
|
+
.join(newline);
|
|
118
|
+
const escapedMask = text
|
|
119
|
+
.replace(/%/g, '%25')
|
|
120
|
+
.replace(/\r/g, '%0D')
|
|
121
|
+
.replace(/\n/g, '%0A');
|
|
122
|
+
if (escapedMask.length > 0) {
|
|
123
|
+
process.stdout.write('::add-mask::' + escapedMask + newline);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (text.includes(newline)) {
|
|
127
|
+
let delimiter = `VARLOCK_${key}_EOF`;
|
|
128
|
+
while (text.includes(delimiter)) {
|
|
129
|
+
delimiter += '_X';
|
|
130
|
+
}
|
|
131
|
+
chunks.push(`${key}<<${delimiter}${newline}${text}${newline}${delimiter}`);
|
|
132
|
+
} else {
|
|
133
|
+
chunks.push(`${key}=${text}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
appendFileSync(githubEnvPath, chunks.join(newline) + newline);
|
|
138
|
+
NODE
|
|
139
|
+
|
|
86
140
|
- name: Decrypt user secrets
|
|
87
141
|
id: user_secrets
|
|
88
|
-
env:
|
|
89
|
-
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
|
90
142
|
run: bun deploy/scripts/decrypt-user-secrets.ts "$HANDLE"
|
|
91
143
|
|
|
92
144
|
- name: Reconcile selected user config
|
|
93
|
-
|
|
94
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
95
|
-
run: |
|
|
96
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
97
|
-
bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
145
|
+
run: bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
98
146
|
|
|
99
147
|
- name: Resolve generated user config
|
|
100
148
|
id: user_config
|
|
101
149
|
run: bun deploy/scripts/resolve-user-config.ts
|
|
102
150
|
|
|
103
151
|
- name: Validate selected secrets
|
|
104
|
-
|
|
105
|
-
SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
|
|
106
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
107
|
-
SETUP_EMAIL_API_KEY: ${{ secrets.SETUP_EMAIL_API_KEY }}
|
|
108
|
-
SETUP_EMAIL_FROM: ${{ secrets.SETUP_EMAIL_FROM }}
|
|
109
|
-
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
110
|
-
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
111
|
-
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
112
|
-
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
113
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
114
|
-
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
115
|
-
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
116
|
-
CONFIG_CF_ZONE_ID: ${{ steps.user_config.outputs.cloudflare_zone_id }}
|
|
117
|
-
SHARED_CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
118
|
-
SHARED_CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
119
|
-
SHARED_PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
120
|
-
run: |
|
|
121
|
-
export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
|
|
122
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
123
|
-
export CF_ZONE_ID="${CONFIG_CF_ZONE_ID:-${CF_ZONE_ID:-$SHARED_CF_ZONE_ID}}"
|
|
124
|
-
export CERTIFICATE_PEM="${CERTIFICATE_PEM:-$SHARED_CERTIFICATE_PEM}"
|
|
125
|
-
export PRIVATE_KEY_PEM="${PRIVATE_KEY_PEM:-$SHARED_PRIVATE_KEY_PEM}"
|
|
126
|
-
bun deploy/scripts/validate-secrets.ts
|
|
152
|
+
run: bun deploy/scripts/validate-secrets.ts
|
|
127
153
|
|
|
128
154
|
- name: Seed content repo
|
|
129
155
|
env:
|
|
130
156
|
CONTENT_REPO: ${{ steps.user_config.outputs.content_repo }}
|
|
131
|
-
|
|
132
|
-
run: |
|
|
133
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
134
|
-
bun deploy/scripts/sync-content-repo.ts
|
|
157
|
+
run: bun deploy/scripts/sync-content-repo.ts
|
|
135
158
|
|
|
136
159
|
- name: Log in to GHCR
|
|
137
|
-
uses: docker/login-action@
|
|
160
|
+
uses: docker/login-action@v3
|
|
138
161
|
with:
|
|
139
162
|
registry: ghcr.io
|
|
140
163
|
username: ${{ github.actor }}
|
|
141
164
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
142
165
|
|
|
143
|
-
- name: Wait for
|
|
166
|
+
- name: Wait for image tag
|
|
144
167
|
run: |
|
|
168
|
+
# The Build workflow fires off the same config push (it derives the
|
|
169
|
+
# declared image set from the registry), so this wait must cover a
|
|
170
|
+
# full image build, not just registry propagation.
|
|
145
171
|
IMAGE_TAG="${{ steps.user_config.outputs.image_repository }}:${{ steps.user_config.outputs.image_tag }}"
|
|
146
|
-
for attempt in $(seq 1
|
|
172
|
+
for attempt in $(seq 1 60); do
|
|
147
173
|
if docker manifest inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
|
148
174
|
exit 0
|
|
149
175
|
fi
|
|
150
|
-
echo "
|
|
151
|
-
sleep
|
|
176
|
+
echo "Image tag not ready yet ($attempt/60): $IMAGE_TAG"
|
|
177
|
+
sleep 20
|
|
152
178
|
done
|
|
153
179
|
echo "Timed out waiting for $IMAGE_TAG" >&2
|
|
154
180
|
exit 1
|
|
@@ -159,8 +185,6 @@ jobs:
|
|
|
159
185
|
ruby -r rubygems -e 'puts Gem.user_dir + "/bin"' >> "$GITHUB_PATH"
|
|
160
186
|
|
|
161
187
|
- name: Write Kamal SSH key
|
|
162
|
-
env:
|
|
163
|
-
KAMAL_SSH_PRIVATE_KEY: ${{ secrets.KAMAL_SSH_PRIVATE_KEY }}
|
|
164
188
|
run: bun deploy/scripts/write-ssh-key.ts
|
|
165
189
|
|
|
166
190
|
- name: Configure SSH client
|
|
@@ -177,46 +201,21 @@ jobs:
|
|
|
177
201
|
chmod 600 ~/.ssh/config
|
|
178
202
|
|
|
179
203
|
- name: Write .kamal/secrets
|
|
180
|
-
|
|
181
|
-
SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
|
|
182
|
-
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
183
|
-
SETUP_EMAIL_API_KEY: ${{ secrets.SETUP_EMAIL_API_KEY }}
|
|
184
|
-
SETUP_EMAIL_FROM: ${{ secrets.SETUP_EMAIL_FROM }}
|
|
185
|
-
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
186
|
-
SHARED_CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
187
|
-
SHARED_PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
188
|
-
run: |
|
|
189
|
-
export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
|
|
190
|
-
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
191
|
-
export CERTIFICATE_PEM="${CERTIFICATE_PEM:-$SHARED_CERTIFICATE_PEM}"
|
|
192
|
-
export PRIVATE_KEY_PEM="${PRIVATE_KEY_PEM:-$SHARED_PRIVATE_KEY_PEM}"
|
|
193
|
-
bun deploy/scripts/write-kamal-secrets.ts
|
|
204
|
+
run: bun deploy/scripts/write-kamal-secrets.ts
|
|
194
205
|
|
|
195
206
|
- name: Provision server
|
|
196
207
|
id: provision
|
|
197
208
|
env:
|
|
198
|
-
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
199
|
-
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
200
|
-
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
201
|
-
HCLOUD_LOCATION: ${{ secrets.HCLOUD_LOCATION }}
|
|
202
209
|
INSTANCE_NAME: ${{ steps.user_config.outputs.instance_name }}
|
|
203
210
|
run: bun deploy/scripts/provision-server.ts
|
|
204
211
|
|
|
205
212
|
- name: Update Cloudflare DNS
|
|
206
213
|
env:
|
|
207
|
-
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
208
|
-
CONFIG_CF_ZONE_ID: ${{ steps.user_config.outputs.cloudflare_zone_id }}
|
|
209
|
-
SHARED_CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
210
214
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
211
|
-
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
212
215
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
213
216
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
214
217
|
run: |
|
|
215
|
-
export CF_ZONE_ID="${CONFIG_CF_ZONE_ID:-${CF_ZONE_ID:-$SHARED_CF_ZONE_ID}}"
|
|
216
218
|
bun deploy/scripts/update-dns.ts
|
|
217
|
-
if [ -n "$WWW_DOMAIN" ]; then
|
|
218
|
-
BRAIN_DOMAIN="$WWW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
219
|
-
fi
|
|
220
219
|
BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
221
220
|
|
|
222
221
|
- name: Validate SSH key
|
|
@@ -244,7 +243,6 @@ jobs:
|
|
|
244
243
|
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
245
244
|
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
246
245
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
247
|
-
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
248
246
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
249
247
|
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
250
248
|
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
@@ -253,13 +251,9 @@ jobs:
|
|
|
253
251
|
env:
|
|
254
252
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
255
253
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
256
|
-
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
257
254
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
258
255
|
run: |
|
|
259
256
|
curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
|
|
260
|
-
if [ -n "$WWW_DOMAIN" ]; then
|
|
261
|
-
curl -I -k --max-time 20 --resolve "$WWW_DOMAIN:443:$SERVER_IP" "https://$WWW_DOMAIN/"
|
|
262
|
-
fi
|
|
263
257
|
curl -I -k --max-time 20 --resolve "$PREVIEW_DOMAIN:443:$SERVER_IP" "https://$PREVIEW_DOMAIN/"
|
|
264
258
|
|
|
265
259
|
- name: Upload generated config
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requireEnv,
|
|
3
|
+
runResolveMissingImages,
|
|
4
|
+
writeGitHubOutput,
|
|
5
|
+
} from "./helpers";
|
|
6
|
+
|
|
7
|
+
// The Build workflow's resolve step — the logic lives in @rizom/ops; this
|
|
8
|
+
// pilot repo only supplies its own paths and repository.
|
|
9
|
+
await runResolveMissingImages({
|
|
10
|
+
rootDir: process.cwd(),
|
|
11
|
+
imageRepository: `ghcr.io/${requireEnv("GITHUB_REPOSITORY")}`,
|
|
12
|
+
writeOutput: writeGitHubOutput,
|
|
13
|
+
});
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
3
|
import { loadPilotRegistry } from "@rizom/ops";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
parseEnvFile,
|
|
7
|
+
requireEnv,
|
|
8
|
+
sitePackagesFor,
|
|
9
|
+
siteImageTag,
|
|
10
|
+
writeGitHubOutput,
|
|
11
|
+
} from "./helpers";
|
|
6
12
|
|
|
7
13
|
const handle = requireEnv("HANDLE");
|
|
8
14
|
const envPath = `users/${handle}/.env`;
|
|
9
15
|
const brainYamlPath = `users/${handle}/brain.yaml`;
|
|
10
16
|
|
|
11
17
|
const envEntries = parseEnvFile(envPath);
|
|
12
|
-
const registry = await loadPilotRegistry(process.cwd());
|
|
13
|
-
const user = registry.users.find((candidate) => candidate.handle === handle);
|
|
14
|
-
if (!user) {
|
|
15
|
-
throw new Error(`Unknown user handle: ${handle}`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const brainVersion = envEntries["BRAIN_VERSION"] ?? "";
|
|
19
|
-
const imageTag = resolveImageTag(registry, brainVersion);
|
|
20
18
|
const repository = process.env["GITHUB_REPOSITORY"] ?? "";
|
|
21
19
|
const repositoryOwner = repository.split("/")[0] ?? "";
|
|
22
20
|
|
|
@@ -27,24 +25,31 @@ if (!brainDomain) {
|
|
|
27
25
|
throw new Error(`Missing domain in ${brainYamlPath}`);
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
brainDomain.length > pilotSubdomainPrefix.length
|
|
34
|
-
? brainDomain.slice(pilotSubdomainPrefix.length)
|
|
28
|
+
const zone =
|
|
29
|
+
brainDomain.startsWith(`${handle}.`) && brainDomain.length > handle.length + 1
|
|
30
|
+
? brainDomain.slice(handle.length + 1)
|
|
35
31
|
: "";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
32
|
+
if (!zone) {
|
|
33
|
+
throw new Error(`Could not derive preview domain from ${brainDomain}`);
|
|
34
|
+
}
|
|
35
|
+
const previewDomain = `${handle}-preview.${zone}`;
|
|
36
|
+
|
|
37
|
+
const brainVersion = envEntries["BRAIN_VERSION"] ?? "";
|
|
38
|
+
|
|
39
|
+
// The image tag is a pure function of this instance's own config: plain
|
|
40
|
+
// `brain-{version}` for a default instance, or its own `brain-{version}-sites-
|
|
41
|
+
// {hash}` when it declares a siteOverride. Resolved through the same helper the
|
|
42
|
+
// build uses so the tag we wait for and run matches exactly what was pushed.
|
|
43
|
+
const registry = await loadPilotRegistry(process.cwd());
|
|
44
|
+
const user = registry.users.find((entry) => entry.handle === handle);
|
|
45
|
+
const sitePackages = sitePackagesFor(user?.siteOverride);
|
|
46
|
+
const imageTag = siteImageTag(brainVersion, sitePackages);
|
|
40
47
|
|
|
41
48
|
const outputs: Record<string, string> = {
|
|
42
49
|
brain_version: brainVersion,
|
|
43
50
|
content_repo: envEntries["CONTENT_REPO"] ?? "",
|
|
44
51
|
brain_domain: brainDomain,
|
|
45
52
|
preview_domain: previewDomain,
|
|
46
|
-
www_domain: wwwDomain,
|
|
47
|
-
cloudflare_zone_id: user.cloudflareZoneId ?? "",
|
|
48
53
|
brain_yaml_path: brainYamlPath,
|
|
49
54
|
instance_name: `rover-${handle}`,
|
|
50
55
|
image_repository: `ghcr.io/${repository}`,
|
|
@@ -62,35 +67,3 @@ for (const key of required) {
|
|
|
62
67
|
for (const [key, value] of Object.entries(outputs)) {
|
|
63
68
|
writeGitHubOutput(key, value);
|
|
64
69
|
}
|
|
65
|
-
|
|
66
|
-
function resolveImageTag(
|
|
67
|
-
registry: Awaited<ReturnType<typeof loadPilotRegistry>>,
|
|
68
|
-
brainVersion: string,
|
|
69
|
-
): string {
|
|
70
|
-
if (!brainVersion) {
|
|
71
|
-
return "";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const sitePackages = [
|
|
75
|
-
...new Set(
|
|
76
|
-
registry.users
|
|
77
|
-
.filter((user) => user.brainVersion === brainVersion)
|
|
78
|
-
.flatMap((user) =>
|
|
79
|
-
user.siteOverride
|
|
80
|
-
? [`${user.siteOverride.package}@${user.siteOverride.version}`]
|
|
81
|
-
: [],
|
|
82
|
-
),
|
|
83
|
-
),
|
|
84
|
-
].sort();
|
|
85
|
-
|
|
86
|
-
if (sitePackages.length === 0) {
|
|
87
|
-
return `brain-${brainVersion}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const siteHash = createHash("sha256")
|
|
91
|
-
.update(sitePackages.join("\n"))
|
|
92
|
-
.digest("hex")
|
|
93
|
-
.slice(0, 12);
|
|
94
|
-
|
|
95
|
-
return `brain-${brainVersion}-sites-${siteHash}`;
|
|
96
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { loadPilotRegistry } from "@rizom/ops";
|
|
4
|
-
import { writeGitHubEnv, writeGitHubOutput } from "./helpers";
|
|
5
|
-
|
|
6
|
-
const requestedBrainVersion = process.env["BRAIN_VERSION_INPUT"]?.trim();
|
|
7
|
-
const registry = await loadPilotRegistry(process.cwd());
|
|
8
|
-
const brainVersion =
|
|
9
|
-
requestedBrainVersion && requestedBrainVersion.length > 0
|
|
10
|
-
? requestedBrainVersion
|
|
11
|
-
: registry.pilot.brainVersion;
|
|
12
|
-
|
|
13
|
-
const sitePackages = [
|
|
14
|
-
...new Set(
|
|
15
|
-
registry.users
|
|
16
|
-
.filter((user) => user.brainVersion === brainVersion)
|
|
17
|
-
.flatMap((user) =>
|
|
18
|
-
user.siteOverride
|
|
19
|
-
? [`${user.siteOverride.package}@${user.siteOverride.version}`]
|
|
20
|
-
: [],
|
|
21
|
-
),
|
|
22
|
-
),
|
|
23
|
-
].sort();
|
|
24
|
-
const sitePackagesValue = sitePackages.join(" ");
|
|
25
|
-
const imageTag = buildImageTag(brainVersion, sitePackages);
|
|
26
|
-
|
|
27
|
-
writeGitHubEnv("BRAIN_VERSION", brainVersion);
|
|
28
|
-
writeGitHubEnv("SITE_PACKAGES", sitePackagesValue);
|
|
29
|
-
writeGitHubEnv("IMAGE_TAG", imageTag);
|
|
30
|
-
writeGitHubOutput("brain_version", brainVersion);
|
|
31
|
-
writeGitHubOutput("site_packages", sitePackagesValue);
|
|
32
|
-
writeGitHubOutput("image_tag", imageTag);
|
|
33
|
-
|
|
34
|
-
console.log(`BRAIN_VERSION=${brainVersion}`);
|
|
35
|
-
console.log(`IMAGE_TAG=${imageTag}`);
|
|
36
|
-
console.log(
|
|
37
|
-
sitePackages.length > 0
|
|
38
|
-
? `SITE_PACKAGES=${sitePackagesValue}`
|
|
39
|
-
: "SITE_PACKAGES=(none)",
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
function buildImageTag(brainVersion: string, sitePackages: string[]): string {
|
|
43
|
-
if (sitePackages.length === 0) {
|
|
44
|
-
return `brain-${brainVersion}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const siteHash = createHash("sha256")
|
|
48
|
-
.update(sitePackages.join("\n"))
|
|
49
|
-
.digest("hex")
|
|
50
|
-
.slice(0, 12);
|
|
51
|
-
|
|
52
|
-
return `brain-${brainVersion}-sites-${siteHash}`;
|
|
53
|
-
}
|