History

chore(executor): remove release build flow

Rhys Sullivan committed 8 months ago

Commit 99887e81 parent d06e5357 files · +4 −823Browse files

Unified · Split

7 files
  1. .github/workflows/release.yml +0 −168
  2. CLAUDE.md +1 −1
  3. executor/README.md +0 −13
  4. executor/install +2 −2
  5. executor/package.json +1 −10
  6. executor/scripts/e2e/install.e2e.test.ts +0 −7
  7. executor/scripts/release/build-release.ts +0 −622
.github/workflows/release.yml+0−168
@@ -1,168 +0,0 @@
1name: Release Executor
2run-name: "${{ format('release {0}', inputs.version || inputs.release_type) }}"
3
4on:
5 workflow_dispatch:
6 inputs:
7 release_type:
8 description: "Version bump"
9 required: true
10 default: patch
11 type: choice
12 options:
13 - patch
14 - minor
15 - major
16 version:
17 description: "Optional explicit version (e.g. 1.2.3 or v1.2.3)"
18 required: false
19 type: string
20
21permissions:
22 contents: write
23
24concurrency:
25 group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.release_type }}
26 cancel-in-progress: false
27
28jobs:
29 release:
30 runs-on: ubuntu-latest
31 steps:
32 - name: Checkout
33 uses: actions/checkout@v4
34 with:
35 fetch-depth: 0
36
37 - name: Setup Bun
38 uses: oven-sh/setup-bun@v2
39
40 - name: Install dependencies
41 run: bun install --frozen-lockfile
42
43 - name: Install executor dependencies
44 run: bun install --cwd executor --frozen-lockfile
45
46 - name: Resolve version
47 id: version
48 shell: bash
49 run: |
50 set -euo pipefail
51
52 requested="${{ inputs.version }}"
53 requested="${requested#v}"
54
55 if [[ -n "${requested}" ]]; then
56 next_version="${requested}"
57 else
58 latest_tag="$(git tag -l 'v*' --sort=-v:refname | head -n 1)"
59 if [[ -z "${latest_tag}" ]]; then
60 latest_version="0.0.0"
61 else
62 latest_version="${latest_tag#v}"
63 fi
64
65 if [[ ! "${latest_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
66 echo "Latest tag is not semver: ${latest_tag}" >&2
67 exit 1
68 fi
69
70 IFS='.' read -r major minor patch <<< "${latest_version}"
71
72 case "${{ inputs.release_type }}" in
73 major)
74 major=$((major + 1))
75 minor=0
76 patch=0
77 ;;
78 minor)
79 minor=$((minor + 1))
80 patch=0
81 ;;
82 patch)
83 patch=$((patch + 1))
84 ;;
85 *)
86 echo "Unsupported release_type: ${{ inputs.release_type }}" >&2
87 exit 1
88 ;;
89 esac
90
91 next_version="${major}.${minor}.${patch}"
92 fi
93
94 if [[ ! "${next_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
95 echo "Version must be semver (x.y.z). Got: ${next_version}" >&2
96 exit 1
97 fi
98
99 next_tag="v${next_version}"
100
101 if git rev-parse -q --verify "refs/tags/${next_tag}" >/dev/null; then
102 echo "Tag already exists locally: ${next_tag}" >&2
103 exit 1
104 fi
105 if git ls-remote --exit-code --tags origin "refs/tags/${next_tag}" >/dev/null 2>&1; then
106 echo "Tag already exists on origin: ${next_tag}" >&2
107 exit 1
108 fi
109
110 echo "version=${next_version}" >> "$GITHUB_OUTPUT"
111 echo "tag=${next_tag}" >> "$GITHUB_OUTPUT"
112 echo "Resolved release tag: ${next_tag}"
113
114 - name: Build release artifacts
115 run: bun run --cwd executor build:release
116
117 - name: Validate local install flow against built artifacts
118 run: |
119 bun run --cwd executor build:binary
120 EXECUTOR_INSTALL_E2E_SKIP_BUILD=1 bun test executor/scripts/e2e/install.e2e.test.ts
121
122 - name: Verify release artifacts exist
123 shell: bash
124 run: |
125 set -euo pipefail
126 expected=(
127 executor/dist/release/executor-linux-x64.tar.gz
128 executor/dist/release/executor-linux-arm64.tar.gz
129 executor/dist/release/executor-darwin-x64.tar.gz
130 executor/dist/release/executor-darwin-arm64.tar.gz
131 executor/dist/release/executor-runtime-linux-x64.tar.gz
132 executor/dist/release/executor-runtime-linux-arm64.tar.gz
133 executor/dist/release/executor-runtime-darwin-x64.tar.gz
134 executor/dist/release/executor-runtime-darwin-arm64.tar.gz
135 executor/dist/release/executor-web-linux-x64.tar.gz
136 executor/dist/release/executor-web-linux-arm64.tar.gz
137 executor/dist/release/executor-web-darwin-x64.tar.gz
138 executor/dist/release/executor-web-darwin-arm64.tar.gz
139 executor/dist/release/checksums.txt
140 )
141
142 for file in "${expected[@]}"; do
143 [[ -f "${file}" ]] || { echo "Missing artifact: ${file}" >&2; exit 1; }
144 done
145
146 - name: Create GitHub release
147 uses: softprops/action-gh-release@v2
148 with:
149 tag_name: ${{ steps.version.outputs.tag }}
150 name: ${{ steps.version.outputs.tag }}
151 target_commitish: ${{ github.sha }}
152 generate_release_notes: true
153 make_latest: true
154 fail_on_unmatched_files: true
155 files: |
156 executor/dist/release/executor-linux-x64.tar.gz
157 executor/dist/release/executor-linux-arm64.tar.gz
158 executor/dist/release/executor-darwin-x64.tar.gz
159 executor/dist/release/executor-darwin-arm64.tar.gz
160 executor/dist/release/executor-runtime-linux-x64.tar.gz
161 executor/dist/release/executor-runtime-linux-arm64.tar.gz
162 executor/dist/release/executor-runtime-darwin-x64.tar.gz
163 executor/dist/release/executor-runtime-darwin-arm64.tar.gz
164 executor/dist/release/executor-web-linux-x64.tar.gz
165 executor/dist/release/executor-web-linux-arm64.tar.gz
166 executor/dist/release/executor-web-darwin-x64.tar.gz
167 executor/dist/release/executor-web-darwin-arm64.tar.gz
168 executor/dist/release/checksums.txt
CLAUDE.md+1−1
@@ -48,7 +48,7 @@ Run from repo root unless noted. · 47 unmodified lines
48- `bun run typecheck:assistant` - checks server + bot tsconfig files.48- `bun run typecheck:assistant` - checks server + bot tsconfig files.
49- `bun run --cwd executor/apps/web lint` - ESLint for Next.js web app.49- `bun run --cwd executor/apps/web lint` - ESLint for Next.js web app.
50 50
51- `bun run --cwd executor build:binary` / `build:release` - binary + release artifacts.51- `bun run --cwd executor build:binary` - host-native binary build.
52- `bun run --cwd executor/apps/web build` - Next.js production build.52- `bun run --cwd executor/apps/web build` - Next.js production build.
53 53
54### Dev commands54### Dev commands
executor/README.md+0−13
@@ -104,27 +104,17 @@ Run these from `executor/`: · 103 unmodified lines
104 104
105```bash105```bash
106bun run doctor106bun run doctor
107bun run doctor:prod
108bun run deploy:prod
109bun run up107bun run up
110bun run backend -- --help108bun run backend -- --help
111bun run web109bun run web
112bun run codegen110bun run codegen
113bun run deploy111bun run deploy
114bun run build:binary112bun run build:binary
115bun run build:release
116```113```
117 114
118Notes:115Notes:
119 116
120- `build:binary` compiles a host-native `dist/executor` binary.117- `build:binary` compiles a host-native `dist/executor` binary.
121- `build:release` builds multi-platform binary archives and web archives for all release target names in `dist/release/`.
122
123Manual GitHub release (recommended):
124
125- Run the `Release Executor` workflow from Actions.
126- Choose `release_type` (`patch`, `minor`, `major`), or provide `version` explicitly.
127- The workflow builds artifacts, creates a new tag (`vX.Y.Z`), and publishes the GitHub release with all required assets.
128 118
129## MCP and OAuth Surface119## MCP and OAuth Surface
130 120
@@ -217,8 +207,6 @@ executor/ · 86 unmodified lines
217|- packages/runner-sandbox-host/ # Cloudflare worker sandbox runtime207|- packages/runner-sandbox-host/ # Cloudflare worker sandbox runtime
218|- packages/core/src/ # shared executor runtime/tooling core package code208|- packages/core/src/ # shared executor runtime/tooling core package code
219|- scripts/dev/ # local/dev-only helpers209|- scripts/dev/ # local/dev-only helpers
220|- scripts/prod/ # production setup and deploy scripts
221|- scripts/release/ # release artifact builder
222|- executor.ts # CLI entrypoint (compiled into binary)210|- executor.ts # CLI entrypoint (compiled into binary)
223|- install # curl install script211|- install # curl install script
224`- uninstall # uninstall script212`- uninstall # uninstall script
@@ -230,4 +218,3 @@ executor/ · 5 unmodified lines
230- `401` on `/mcp/anonymous`: pass `API_KEY` from the setup card as `Authorization: Bearer <API_KEY>` (or `x-api-key`).218- `401` on `/mcp/anonymous`: pass `API_KEY` from the setup card as `Authorization: Bearer <API_KEY>` (or `x-api-key`).
231- Web UI cannot load data: verify `CONVEX_URL` / `CONVEX_SITE_URL` and that Convex dev is running.219- Web UI cannot load data: verify `CONVEX_URL` / `CONVEX_SITE_URL` and that Convex dev is running.
232- `ENOENT` after `fs.write` on hosted Convex: `agentfs-local` is unsupported on hosted Convex because local filesystem state is not shared across workers. Use `AGENT_STORAGE_PROVIDER=agentfs-cloudflare`.220- `ENOENT` after `fs.write` on hosted Convex: `agentfs-local` is unsupported on hosted Convex because local filesystem state is not shared across workers. Use `AGENT_STORAGE_PROVIDER=agentfs-cloudflare`.
233- Release build missing managed artifacts: run `bun run build:release` and verify `executor/dist/release/` contains `executor-*.tar.gz`, `executor-web-*.tar.gz`, and `executor-runtime-*.tar.gz`.
executor/install+2−2
@@ -329,8 +329,8 @@ install_runtime_bundle() { · 328 unmodified lines
329 echo -e "${RED}Runtime bundle was not installed (missing release asset ${runtime_filename})${NC}"329 echo -e "${RED}Runtime bundle was not installed (missing release asset ${runtime_filename})${NC}"
330 echo ""330 echo ""
331 echo "To build and install it locally:"331 echo "To build and install it locally:"
332 echo " bun run --cwd executor build:release"332 echo " bun run --cwd executor build:binary"
333 echo " bash executor/install --no-modify-path"333 echo " bash executor/install --binary executor/dist/executor --no-modify-path"
334 exit 1334 exit 1
335}335}
336 336
executor/package.json+1−10
@@ -9,11 +9,6 @@ · 8 unmodified lines
9 "private": true,9 "private": true,
10 "scripts": {10 "scripts": {
11 "doctor": "bun run executor.ts doctor",11 "doctor": "bun run executor.ts doctor",
12 "doctor:prod": "bun run scripts/prod/doctor-prod.ts",
13 "deploy:prod": "bun run scripts/prod/deploy-prod.ts",
14 "setup:prod:env": "bun run scripts/prod/setup/prod-env.ts --from-env",
15 "setup:prod:cloudflare": "bun run scripts/prod/setup/prod-cloudflare.ts",
16 "setup:prod:all": "bun run setup:prod:cloudflare --deploy && bun run setup:prod:env && bun run doctor:prod",
17 "up": "bun run executor.ts up",12 "up": "bun run executor.ts up",
18 "upgrade": "bun run executor.ts upgrade",13 "upgrade": "bun run executor.ts upgrade",
19 "down": "bun run executor.ts down",14 "down": "bun run executor.ts down",
@@ -29,13 +24,9 @@ · 9 unmodified lines
29 "test:e2e:install": "bun test scripts/e2e/install.e2e.test.ts",24 "test:e2e:install": "bun test scripts/e2e/install.e2e.test.ts",
30 "test:e2e:install:sandbox": "bun test scripts/e2e/install.sandbox.e2e.test.ts",25 "test:e2e:install:sandbox": "bun test scripts/e2e/install.sandbox.e2e.test.ts",
31 "sandbox:install:session": "bun run scripts/e2e/install.sandbox.session.ts",26 "sandbox:install:session": "bun run scripts/e2e/install.sandbox.session.ts",
32 "smoke:storage:execute-flow:local": "bun run scripts/dev/storage/smoke.execute-flow.ts --runtime local-bun --deploy-convex",
33 "smoke:storage:execute-flow": "bun run scripts/dev/storage/smoke.execute-flow.ts --runtime cloudflare-worker-loader --deploy-convex --deploy-worker",
34 "smoke:storage:execute-flow:cloudflare": "bun run scripts/dev/storage/smoke.execute-flow.ts --runtime cloudflare-worker-loader --deploy-convex --deploy-worker",
35 "typecheck": "bunx --package @typescript/native-preview tsgo --noEmit -p tsconfig.json",27 "typecheck": "bunx --package @typescript/native-preview tsgo --noEmit -p tsconfig.json",
36 "typecheck:slow": "bunx tsc --noEmit -p tsconfig.json",28 "typecheck:slow": "bunx tsc --noEmit -p tsconfig.json",
37 "build:binary": "bun build executor.ts --compile --outfile dist/executor",29 "build:binary": "bun build executor.ts --compile --outfile dist/executor"
38 "build:release": "bun run scripts/release/build-release.ts"
39 },30 },
40 "type": "module"31 "type": "module"
41}32}
executor/scripts/e2e/install.e2e.test.ts+0−7
@@ -121,13 +121,6 @@ test("installer e2e: uninstall, install, deploy, and cleanup", async () => { · 120 unmodified lines
121 timeoutMs: 300_000,121 timeoutMs: 300_000,
122 });122 });
123 assertSuccess(buildBinary, "build binary");123 assertSuccess(buildBinary, "build binary");
124
125 const buildRelease = await runCommand(["bun", "run", "build:release"], {
126 cwd: repoRoot,
127 env: process.env,
128 timeoutMs: 1_200_000,
129 });
130 assertSuccess(buildRelease, "build release");
131 }124 }
132 125
133 if (!(await pathExists(binaryPath))) {126 if (!(await pathExists(binaryPath))) {
executor/scripts/release/build-release.ts+0−622
@@ -1,622 +0,0 @@
1#!/usr/bin/env bun
2
3import fs from "node:fs/promises";
4import os from "node:os";
5import path from "node:path";
6import { createHash, generateKeyPairSync } from "node:crypto";
7import net from "node:net";
8
9import { managedRuntimeVersions } from "../../packages/core/src/managed/runtime-info";
10
11type ReleaseTarget = {
12 platform: "linux" | "darwin";
13 arch: "x64" | "arm64";
14 bunTarget: string;
15};
16
17const targets: ReleaseTarget[] = [
18 { platform: "linux", arch: "x64", bunTarget: "bun-linux-x64" },
19 { platform: "linux", arch: "arm64", bunTarget: "bun-linux-arm64" },
20 { platform: "darwin", arch: "x64", bunTarget: "bun-darwin-x64" },
21 { platform: "darwin", arch: "arm64", bunTarget: "bun-darwin-arm64" },
22];
23
24const convexBackendRepo = "get-convex/convex-backend";
25const defaultManagedSitePort = 5411;
26
27function hostPlatformArch(): { platform: "linux" | "darwin"; arch: "x64" | "arm64" } {
28 if (process.platform !== "linux" && process.platform !== "darwin") {
29 throw new Error(`Unsupported host platform for release build: ${process.platform}`);
30 }
31 if (process.arch !== "x64" && process.arch !== "arm64") {
32 throw new Error(`Unsupported host architecture for release build: ${process.arch}`);
33 }
34 return { platform: process.platform, arch: process.arch };
35}
36
37function archiveName(platform: ReleaseTarget["platform"], arch: ReleaseTarget["arch"]): string {
38 return `executor-${platform}-${arch}.tar.gz`;
39}
40
41function webArchiveName(platform: ReleaseTarget["platform"], arch: ReleaseTarget["arch"]): string {
42 return `executor-web-${platform}-${arch}.tar.gz`;
43}
44
45function runtimeArchiveName(platform: ReleaseTarget["platform"], arch: ReleaseTarget["arch"]): string {
46 return `executor-runtime-${platform}-${arch}.tar.gz`;
47}
48
49function backendAssetName(target: ReleaseTarget): string {
50 if (target.platform === "linux" && target.arch === "x64") {
51 return "convex-local-backend-x86_64-unknown-linux-gnu.zip";
52 }
53 if (target.platform === "linux" && target.arch === "arm64") {
54 return "convex-local-backend-aarch64-unknown-linux-gnu.zip";
55 }
56 if (target.platform === "darwin" && target.arch === "x64") {
57 return "convex-local-backend-x86_64-apple-darwin.zip";
58 }
59 return "convex-local-backend-aarch64-apple-darwin.zip";
60}
61
62function nodeArchiveName(target: ReleaseTarget): string {
63 return `node-v${managedRuntimeVersions.nodeVersion}-${target.platform}-${target.arch}.tar.gz`;
64}
65
66function backendBinaryName(target: ReleaseTarget): string {
67 return target.platform === "linux" || target.platform === "darwin"
68 ? "convex-local-backend"
69 : "convex-local-backend.exe";
70}
71
72async function sha256(filePath: string): Promise<string> {
73 const bytes = await Bun.file(filePath).arrayBuffer();
74 return createHash("sha256").update(Buffer.from(bytes)).digest("hex");
75}
76
77async function runCommand(command: string[], options: { cwd?: string; env?: Record<string, string | undefined> } = {}): Promise<void> {
78 const proc = Bun.spawn(command, {
79 cwd: options.cwd,
80 env: options.env,
81 stdin: "inherit",
82 stdout: "inherit",
83 stderr: "inherit",
84 });
85 const code = await proc.exited;
86 if (code !== 0) {
87 throw new Error(`Command failed (${code}): ${command.join(" ")}`);
88 }
89}
90
91async function runArchiveCommand(command: string[], cwd?: string): Promise<void> {
92 await runCommand(command, { cwd });
93}
94
95async function pathExists(filePath: string): Promise<boolean> {
96 try {
97 await fs.access(filePath);
98 return true;
99 } catch {
100 return false;
101 }
102}
103
104async function downloadFile(url: string, destination: string): Promise<void> {
105 await runCommand(["curl", "-fsSL", url, "-o", destination]);
106}
107
108async function extractZipArchive(archivePath: string, destinationDir: string): Promise<void> {
109 try {
110 await runCommand(["unzip", "-o", archivePath, "-d", destinationDir]);
111 return;
112 } catch {
113 const pythonScript = [
114 "import sys, zipfile",
115 "zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])",
116 ].join(";");
117 await runCommand(["python3", "-c", pythonScript, archivePath, destinationDir]);
118 }
119}
120
121async function waitForHttp(url: string, timeoutMs: number): Promise<void> {
122 const startedAt = Date.now();
123 while (Date.now() - startedAt < timeoutMs) {
124 try {
125 const response = await fetch(url);
126 if (response.ok) {
127 return;
128 }
129 } catch {
130 // still starting
131 }
132 await Bun.sleep(500);
133 }
134 throw new Error(`Timed out waiting for ${url}`);
135}
136
137async function canBindPort(port: number): Promise<boolean> {
138 return await new Promise((resolve) => {
139 const server = net.createServer();
140 server.once("error", () => {
141 resolve(false);
142 });
143 server.listen(port, "127.0.0.1", () => {
144 server.close(() => resolve(true));
145 });
146 });
147}
148
149async function randomPortBase(): Promise<number> {
150 for (let attempt = 0; attempt < 100; attempt += 1) {
151 const base = 20000 + Math.floor(Math.random() * 20000);
152 if (await canBindPort(base) && await canBindPort(base + 1) && await canBindPort(base + 2)) {
153 return base;
154 }
155 }
156
157 throw new Error("Unable to allocate free ports for runtime seeding");
158}
159
160type AnonymousAuthSeed = {
161 privateKeyPem: string;
162 publicKeyPem: string;
163 apiKeySecret: string;
164};
165
166type RuntimeConfig = {
167 instanceName: string;
168 instanceSecret: string;
169 hostInterface: string;
170 backendPort: number;
171 siteProxyPort: number;
172};
173
174type TanstackStartOutput = {
175 outputRoot: string;
176 serverEntry: string;
177 stagedServerEntry: string;
178};
179
180function normalizePemForEnv(value: string): string {
181 return value.replace(/\r\n/g, "\n").replace(/\n/g, "\\n").trim();
182}
183
184function createAnonymousAuthSeed(): AnonymousAuthSeed {
185 const keyPair = generateKeyPairSync("ec", {
186 namedCurve: "P-256",
187 privateKeyEncoding: {
188 type: "pkcs8",
189 format: "pem",
190 },
191 publicKeyEncoding: {
192 type: "spki",
193 format: "pem",
194 },
195 });
196
197 const privateKeyPem = normalizePemForEnv(keyPair.privateKey);
198 return {
199 privateKeyPem,
200 publicKeyPem: normalizePemForEnv(keyPair.publicKey),
201 apiKeySecret: privateKeyPem,
202 };
203}
204
205async function resolveTanstackStartOutput(webAppDir: string): Promise<TanstackStartOutput> {
206 const candidates: Array<{ outputRoot: string; serverEntry: string; stagedServerEntry: string }> = [
207 {
208 outputRoot: path.join(webAppDir, ".output"),
209 serverEntry: path.join(".output", "server", "index.mjs"),
210 stagedServerEntry: path.join(".output", "server", "index.mjs"),
211 },
212 {
213 outputRoot: path.join(webAppDir, "dist"),
214 serverEntry: path.join("dist", "server", "server.mjs"),
215 stagedServerEntry: path.join("dist", "server", "server.mjs"),
216 },
217 ];
218
219 for (const candidate of candidates) {
220 const absoluteServerEntry = path.join(webAppDir, candidate.serverEntry);
221 if (await pathExists(absoluteServerEntry)) {
222 return {
223 outputRoot: candidate.outputRoot,
224 serverEntry: candidate.serverEntry,
225 stagedServerEntry: candidate.stagedServerEntry,
226 };
227 }
228 }
229
230 const checkedPaths = candidates
231 .map((candidate) => path.join(webAppDir, candidate.serverEntry))
232 .join(", ");
233
234 throw new Error(`Could not find TanStack Start server output. Checked: ${checkedPaths}`);
235}
236
237async function readRuntimeConfig(runtimeDir: string): Promise<RuntimeConfig> {
238 const configPath = path.join(runtimeDir, "convex-backend.json");
239 const raw = await fs.readFile(configPath, "utf8");
240 const parsed = JSON.parse(raw) as Partial<RuntimeConfig>;
241
242 if (
243 typeof parsed.instanceName !== "string"
244 || typeof parsed.instanceSecret !== "string"
245 || typeof parsed.hostInterface !== "string"
246 || typeof parsed.backendPort !== "number"
247 || typeof parsed.siteProxyPort !== "number"
248 ) {
249 throw new Error(`Invalid runtime config at ${configPath}`);
250 }
251
252 return {
253 instanceName: parsed.instanceName,
254 instanceSecret: parsed.instanceSecret,
255 hostInterface: parsed.hostInterface,
256 backendPort: parsed.backendPort,
257 siteProxyPort: parsed.siteProxyPort,
258 };
259}
260
261async function generateSelfHostedAdminKey(config: RuntimeConfig): Promise<string> {
262 const response = await fetch("https://api.convex.dev/api/local_deployment/generate_admin_key", {
263 method: "POST",
264 headers: {
265 "Content-Type": "application/json",
266 "Convex-Client": managedRuntimeVersions.convexClientHeader,
267 },
268 body: JSON.stringify({
269 instanceName: config.instanceName,
270 instanceSecret: config.instanceSecret,
271 }),
272 });
273
274 if (!response.ok) {
275 const text = await response.text();
276 throw new Error(`Failed generating self-hosted admin key: ${text || response.statusText}`);
277 }
278
279 const parsed = await response.json() as { adminKey?: string };
280 if (!parsed.adminKey || parsed.adminKey.trim().length === 0) {
281 throw new Error("Self-hosted admin key API did not return adminKey");
282 }
283
284 return parsed.adminKey;
285}
286
287async function assertSeededClientConfig(config: RuntimeConfig, expectedAnonymousIssuer: string): Promise<void> {
288 const response = await fetch(`http://${config.hostInterface}:${config.backendPort}/api/query`, {
289 method: "POST",
290 headers: {
291 "content-type": "application/json",
292 },
293 body: JSON.stringify({
294 path: "app:getClientConfig",
295 args: {},
296 }),
297 });
298
299 if (!response.ok) {
300 const text = await response.text();
301 throw new Error(`Failed querying app:getClientConfig during runtime seeding: ${text || response.statusText}`);
302 }
303
304 const parsed = await response.json() as {
305 status?: string;
306 value?: {
307 anonymousAuthIssuer?: unknown;
308 };
309 };
310
311 if (parsed.status !== "success") {
312 throw new Error(`Runtime seeding query returned unexpected status: ${JSON.stringify(parsed)}`);
313 }
314
315 if (parsed.value?.anonymousAuthIssuer !== expectedAnonymousIssuer) {
316 throw new Error(
317 `Runtime seeding issuer mismatch. Expected ${expectedAnonymousIssuer}, got ${String(parsed.value?.anonymousAuthIssuer)}`,
318 );
319 }
320}
321
322async function seedManagedRuntimeState(
323 rootDir: string,
324 runtimeDir: string,
325 config: RuntimeConfig,
326 anonymousAuth: AnonymousAuthSeed,
327): Promise<void> {
328 const expectedAnonymousIssuer = `http://${config.hostInterface}:${defaultManagedSitePort}`;
329 const adminKey = await generateSelfHostedAdminKey(config);
330 const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "executor-seed-env-"));
331 const envFilePath = path.join(tempDir, "selfhost.env");
332 const envContents = [
333 `CONVEX_SELF_HOSTED_URL=http://${config.hostInterface}:${config.backendPort}`,
334 `CONVEX_SELF_HOSTED_ADMIN_KEY=${adminKey}`,
335 "WORKOS_CLIENT_ID=disabled",
336 "DANGEROUSLY_ALLOW_LOCAL_VM=1",
337 ].join("\n");
338
339 await fs.writeFile(envFilePath, `${envContents}\n`, "utf8");
340
341 const envEntries: Array<{ name: string; value: string }> = [
342 { name: "WORKOS_CLIENT_ID", value: "disabled" },
343 { name: "DANGEROUSLY_ALLOW_LOCAL_VM", value: "1" },
344 { name: "ANONYMOUS_AUTH_ISSUER", value: expectedAnonymousIssuer },
345 { name: "ANONYMOUS_AUTH_PRIVATE_KEY_PEM", value: anonymousAuth.privateKeyPem },
346 { name: "ANONYMOUS_AUTH_PUBLIC_KEY_PEM", value: anonymousAuth.publicKeyPem },
347 { name: "MCP_API_KEY_SECRET", value: anonymousAuth.apiKeySecret },
348 ];
349
350 try {
351 for (const entry of envEntries) {
352 await runCommand(["bunx", "convex", "env", "set", `${entry.name}=${entry.value}`, "--env-file", envFilePath], {
353 cwd: rootDir,
354 });
355 }
356
357 await runCommand(["bunx", "convex", "deploy", "--yes", "--typecheck", "disable", "--codegen", "disable", "--env-file", envFilePath], {
358 cwd: rootDir,
359 });
360
361 await assertSeededClientConfig(config, expectedAnonymousIssuer);
362 } finally {
363 await fs.rm(tempDir, { recursive: true, force: true });
364 }
365
366 await fs.writeFile(
367 path.join(runtimeDir, "managed-anonymous-auth.json"),
368 `${JSON.stringify({
369 ANONYMOUS_AUTH_PRIVATE_KEY_PEM: anonymousAuth.privateKeyPem,
370 ANONYMOUS_AUTH_PUBLIC_KEY_PEM: anonymousAuth.publicKeyPem,
371 MCP_API_KEY_SECRET: anonymousAuth.apiKeySecret,
372 }, null, 2)}\n`,
373 { encoding: "utf8", mode: 0o600 },
374 );
375}
376
377async function seedRuntimeState(rootDir: string): Promise<{ tempRoot: string; runtimeDir: string }> {
378 const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "executor-runtime-seed-"));
379 const runtimeDir = path.join(tempRoot, "runtime");
380 const basePort = await randomPortBase();
381 const anonymousAuth = createAnonymousAuthSeed();
382 const host = hostPlatformArch();
383 const hostTarget: ReleaseTarget = {
384 platform: host.platform,
385 arch: host.arch,
386 bunTarget: host.platform === "linux"
387 ? (host.arch === "x64" ? "bun-linux-x64" : "bun-linux-arm64")
388 : (host.arch === "x64" ? "bun-darwin-x64" : "bun-darwin-arm64"),
389 };
390
391 await fs.mkdir(runtimeDir, { recursive: true });
392 const hostNodeArchive = path.join(tempRoot, nodeArchiveName(hostTarget));
393 const hostNodeUrl = `https://nodejs.org/dist/v${managedRuntimeVersions.nodeVersion}/${nodeArchiveName(hostTarget)}`;
394 await downloadFile(hostNodeUrl, hostNodeArchive);
395 await runArchiveCommand(["tar", "-xzf", hostNodeArchive, "-C", runtimeDir]);
396
397 const env = {
398 ...process.env,
399 EXECUTOR_SKIP_RUNTIME_IMAGE: "1",
400 EXECUTOR_RUNTIME_DIR: runtimeDir,
401 EXECUTOR_PROJECT_DIR: rootDir,
402 EXECUTOR_BACKEND_PORT: String(basePort),
403 EXECUTOR_BACKEND_SITE_PORT: String(basePort + 1),
404 EXECUTOR_WEB_PORT: String(basePort + 2),
405 ANONYMOUS_AUTH_PRIVATE_KEY_PEM: anonymousAuth.privateKeyPem,
406 ANONYMOUS_AUTH_PUBLIC_KEY_PEM: anonymousAuth.publicKeyPem,
407 MCP_API_KEY_SECRET: anonymousAuth.apiKeySecret,
408 };
409
410 const backendProc = Bun.spawn(["bun", "run", "executor.ts", "up", "--disable-beacon"], {
411 cwd: rootDir,
412 env,
413 stdin: "ignore",
414 stdout: "ignore",
415 stderr: "ignore",
416 });
417
418 try {
419 await waitForHttp(`http://127.0.0.1:${basePort}/version`, 120_000);
420 const config = await readRuntimeConfig(runtimeDir);
421 await seedManagedRuntimeState(rootDir, runtimeDir, config, anonymousAuth);
422 } catch (error) {
423 backendProc.kill();
424 await backendProc.exited;
425 throw error;
426 }
427
428 backendProc.kill();
429 await backendProc.exited;
430
431 const convexDataDir = path.join(runtimeDir, "convex-data");
432 if (!(await pathExists(convexDataDir))) {
433 throw new Error("Runtime seeding failed: convex-data directory missing");
434 }
435
436 return { tempRoot, runtimeDir };
437}
438
439async function buildWebArtifact(rootDir: string, releaseDir: string, checksums: string[]): Promise<void> {
440 const host = hostPlatformArch();
441 const webAppDir = path.join(rootDir, "apps", "web");
442 const webArtifactName = webArchiveName(host.platform, host.arch);
443 const archivePath = path.join(releaseDir, webArtifactName);
444 const webBuildEnv = {
445 ...process.env,
446 EXECUTOR_WEB_CONVEX_URL: "http://127.0.0.1:5410",
447 EXECUTOR_WEB_CONVEX_SITE_URL: "http://127.0.0.1:5411",
448 VITE_CONVEX_URL: "http://127.0.0.1:5410",
449 VITE_CONVEX_SITE_URL: "http://127.0.0.1:5411",
450 NODE_ENV: "production",
451 VITE_USER_NODE_ENV: "production",
452 };
453
454 await runCommand(["bunx", "vite", "build", "--mode", "production"], {
455 cwd: webAppDir,
456 env: webBuildEnv,
457 });
458
459 const tanstackStart = await resolveTanstackStartOutput(webAppDir);
460
461 const outputRoot = tanstackStart.outputRoot;
462 const outputServerEntry = path.join(webAppDir, tanstackStart.serverEntry);
463 const stageRoot = path.join(releaseDir, `executor-web-${host.platform}-${host.arch}`);
464 const stagedAppRoot = path.join(stageRoot, "executor", "apps", "web");
465 const stagedServerEntry = tanstackStart.stagedServerEntry;
466 const stagedOutputRoot = path.basename(outputRoot);
467 const stagedServerEntryWithLeadingSlash = `/${stagedServerEntry.split(path.sep).join("/")}`;
468
469 if (!(await pathExists(outputServerEntry))) {
470 throw new Error(`Missing TanStack Start output at ${outputServerEntry}. Ensure vite build completed.`);
471 }
472
473 await fs.rm(stageRoot, { recursive: true, force: true });
474 await fs.mkdir(stageRoot, { recursive: true });
475 await fs.mkdir(stagedAppRoot, { recursive: true });
476
477 await fs.cp(outputRoot, path.join(stagedAppRoot, stagedOutputRoot), { recursive: true });
478
479 const stagedServerEntryAbsolute = path.join(stagedAppRoot, stagedServerEntry);
480 if (!(await pathExists(stagedServerEntryAbsolute))) {
481 throw new Error(`Expected staged TanStack Start server entry at ${stagedServerEntryAbsolute}. Packaging failed.`);
482 }
483
484 await Bun.write(
485 path.join(stageRoot, "server.js"),
486 `process.chdir(__dirname + '/executor/apps/web');\nimport(process.cwd() + '${stagedServerEntryWithLeadingSlash}');\n`,
487 );
488
489 await runArchiveCommand(["tar", "-czf", archivePath, "-C", stageRoot, "."]);
490 const digest = await sha256(archivePath);
491 checksums.push(`${digest} ${path.basename(archivePath)}`);
492 console.log(`built ${webArtifactName}`);
493
494 for (const target of targets) {
495 if (target.platform === host.platform && target.arch === host.arch) {
496 continue;
497 }
498
499 const targetArtifactName = webArchiveName(target.platform, target.arch);
500 const targetArchivePath = path.join(releaseDir, targetArtifactName);
501 await fs.copyFile(archivePath, targetArchivePath);
502
503 const targetDigest = await sha256(targetArchivePath);
504 checksums.push(`${targetDigest} ${targetArtifactName}`);
505 console.log(`aliased ${targetArtifactName} from ${webArtifactName}`);
506 }
507}
508
509async function buildRuntimeArtifact(rootDir: string, releaseDir: string, checksums: string[]): Promise<void> {
510 const seeded = await seedRuntimeState(rootDir);
511 const convexDataDir = path.join(seeded.runtimeDir, "convex-data");
512 const managedAnonymousAuthFile = path.join(seeded.runtimeDir, "managed-anonymous-auth.json");
513
514 const tempDownloads = await fs.mkdtemp(path.join(os.tmpdir(), "executor-runtime-build-"));
515
516 try {
517 for (const target of targets) {
518 const stageRoot = path.join(releaseDir, `executor-runtime-${target.platform}-${target.arch}`);
519 const artifactPath = path.join(releaseDir, runtimeArchiveName(target.platform, target.arch));
520
521 await fs.rm(stageRoot, { recursive: true, force: true });
522 await fs.mkdir(stageRoot, { recursive: true });
523
524 await fs.cp(convexDataDir, path.join(stageRoot, "convex-data"), { recursive: true });
525 if (await pathExists(managedAnonymousAuthFile)) {
526 await fs.copyFile(managedAnonymousAuthFile, path.join(stageRoot, "managed-anonymous-auth.json"));
527 }
528
529 const backendArchive = path.join(tempDownloads, backendAssetName(target));
530 const backendUrl = `https://github.com/${convexBackendRepo}/releases/latest/download/${backendAssetName(target)}`;
531 await downloadFile(backendUrl, backendArchive);
532 await fs.mkdir(path.join(stageRoot, "convex-backend"), { recursive: true });
533 await extractZipArchive(backendArchive, path.join(stageRoot, "convex-backend"));
534
535 const backendBinary = path.join(stageRoot, "convex-backend", backendBinaryName(target));
536 if (!(await pathExists(backendBinary))) {
537 throw new Error(`Runtime artifact missing backend binary after extraction: ${backendBinary}`);
538 }
539 await fs.chmod(backendBinary, 0o755);
540
541 const nodeArchive = path.join(tempDownloads, nodeArchiveName(target));
542 const nodeUrl = `https://nodejs.org/dist/v${managedRuntimeVersions.nodeVersion}/${nodeArchiveName(target)}`;
543 await downloadFile(nodeUrl, nodeArchive);
544 await runArchiveCommand(["tar", "-xzf", nodeArchive, "-C", stageRoot]);
545
546 const nodeDir = path.join(stageRoot, `node-v${managedRuntimeVersions.nodeVersion}-${target.platform}-${target.arch}`);
547 if (!(await pathExists(path.join(nodeDir, "bin", "node")))) {
548 throw new Error(`Runtime artifact missing extracted node runtime: ${nodeDir}`);
549 }
550
551 const webArchive = path.join(releaseDir, webArchiveName(target.platform, target.arch));
552 if (!(await pathExists(webArchive))) {
553 throw new Error(`Missing web artifact for runtime packaging: ${webArchive}`);
554 }
555 const webStage = path.join(tempDownloads, `web-${target.platform}-${target.arch}`);
556 await fs.rm(webStage, { recursive: true, force: true });
557 await fs.mkdir(webStage, { recursive: true });
558 await runArchiveCommand(["tar", "-xzf", webArchive, "-C", webStage]);
559 await fs.cp(webStage, path.join(stageRoot, "web"), { recursive: true });
560
561 if (!(await pathExists(path.join(stageRoot, "web", "server.js")))) {
562 throw new Error(`Runtime artifact missing web server entry for ${target.platform}-${target.arch}`);
563 }
564
565 await runArchiveCommand(["tar", "-czf", artifactPath, "-C", stageRoot, "."]);
566 const digest = await sha256(artifactPath);
567 checksums.push(`${digest} ${path.basename(artifactPath)}`);
568 console.log(`built ${path.basename(artifactPath)}`);
569 }
570 } finally {
571 await fs.rm(tempDownloads, { recursive: true, force: true });
572 await fs.rm(seeded.tempRoot, { recursive: true, force: true });
573 }
574}
575
576async function main(): Promise<void> {
577 const rootDir = path.resolve(import.meta.dir, "..", "..");
578 const releaseDir = path.join(rootDir, "dist", "release");
579
580 await fs.rm(releaseDir, { recursive: true, force: true });
581 await fs.mkdir(releaseDir, { recursive: true });
582
583 const checksums: string[] = [];
584
585 for (const target of targets) {
586 const dirName = `executor-${target.platform}-${target.arch}`;
587 const bundleDir = path.join(releaseDir, dirName);
588 const binDir = path.join(bundleDir, "bin");
589 const binName = "executor";
590 const binPath = path.join(binDir, binName);
591
592 await fs.mkdir(binDir, { recursive: true });
593
594 const build = await Bun.build({
595 entrypoints: [path.join(rootDir, "executor.ts")],
596 compile: {
597 target: target.bunTarget as never,
598 outfile: binPath,
599 },
600 });
601
602 if (!build.success) {
603 const logs = build.logs.map((log) => log.message).join("\n");
604 throw new Error(`Failed to compile target ${target.bunTarget}\n${logs}`);
605 }
606
607 const archivePath = path.join(releaseDir, archiveName(target.platform, target.arch));
608 await runArchiveCommand(["tar", "-czf", archivePath, "-C", binDir, binName]);
609
610 const digest = await sha256(archivePath);
611 checksums.push(`${digest} ${path.basename(archivePath)}`);
612 console.log(`built ${path.basename(archivePath)}`);
613 }
614
615 await buildWebArtifact(rootDir, releaseDir, checksums);
616 await buildRuntimeArtifact(rootDir, releaseDir, checksums);
617
618 await Bun.write(path.join(releaseDir, "checksums.txt"), `${checksums.join("\n")}\n`);
619 console.log(`wrote ${path.join("dist", "release", "checksums.txt")}`);
620}
621
622await main();