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 @@
1−name: Release Executor
2−run-name: "${{ format('release {0}', inputs.version || inputs.release_type) }}"
3−
4−on:
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−
21−permissions:
22− contents: write
23−
24−concurrency:
25− group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.release_type }}
26− cancel-in-progress: false
27−
28−jobs:
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
4848 - `bun run typecheck:assistant` - checks server + bot tsconfig files.
4949 - `bun run --cwd executor/apps/web lint` - ESLint for Next.js web app.
5050
51−- `bun run --cwd executor build:binary` / `build:release` - binary + release artifacts.
51+- `bun run --cwd executor build:binary` - host-native binary build.
5252 - `bun run --cwd executor/apps/web build` - Next.js production build.
5353
5454 ### Dev commands
executor/README.md+0−13
@@ -104,27 +104,17 @@ Run these from `executor/`: · 103 unmodified lines
104104
105105 ```bash
106106 bun run doctor
107−bun run doctor:prod
108−bun run deploy:prod
109107 bun run up
110108 bun run backend -- --help
111109 bun run web
112110 bun run codegen
113111 bun run deploy
114112 bun run build:binary
115−bun run build:release
116113 ```
117114
118115 Notes:
119116
120117 - `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−
123−Manual 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.
128118
129119 ## MCP and OAuth Surface
130120
@@ -217,8 +207,6 @@ executor/ · 86 unmodified lines
217207 |- packages/runner-sandbox-host/ # Cloudflare worker sandbox runtime
218208 |- packages/core/src/ # shared executor runtime/tooling core package code
219209 |- scripts/dev/ # local/dev-only helpers
220−|- scripts/prod/ # production setup and deploy scripts
221−|- scripts/release/ # release artifact builder
222210 |- executor.ts # CLI entrypoint (compiled into binary)
223211 |- install # curl install script
224212 `- uninstall # uninstall script
@@ -230,4 +218,3 @@ executor/ · 5 unmodified lines
230218 - `401` on `/mcp/anonymous`: pass `API_KEY` from the setup card as `Authorization: Bearer <API_KEY>` (or `x-api-key`).
231219 - Web UI cannot load data: verify `CONVEX_URL` / `CONVEX_SITE_URL` and that Convex dev is running.
232220 - `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
329329 echo -e "${RED}Runtime bundle was not installed (missing release asset ${runtime_filename})${NC}"
330330 echo ""
331331 echo "To build and install it locally:"
332− echo " bun run --cwd executor build:release"
333− echo " bash executor/install --no-modify-path"
332+ echo " bun run --cwd executor build:binary"
333+ echo " bash executor/install --binary executor/dist/executor --no-modify-path"
334334 exit 1
335335 }
336336
executor/package.json+1−10
@@ -9,11 +9,6 @@ · 8 unmodified lines
99 "private": true,
1010 "scripts": {
1111 "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",
1712 "up": "bun run executor.ts up",
1813 "upgrade": "bun run executor.ts upgrade",
1914 "down": "bun run executor.ts down",
@@ -29,13 +24,9 @@ · 9 unmodified lines
2924 "test:e2e:install": "bun test scripts/e2e/install.e2e.test.ts",
3025 "test:e2e:install:sandbox": "bun test scripts/e2e/install.sandbox.e2e.test.ts",
3126 "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",
3527 "typecheck": "bunx --package @typescript/native-preview tsgo --noEmit -p tsconfig.json",
3628 "typecheck:slow": "bunx tsc --noEmit -p tsconfig.json",
37− "build:binary": "bun build executor.ts --compile --outfile dist/executor",
38− "build:release": "bun run scripts/release/build-release.ts"
29+ "build:binary": "bun build executor.ts --compile --outfile dist/executor"
3930 },
4031 "type": "module"
4132 }
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
121121 timeoutMs: 300_000,
122122 });
123123 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");
131124 }
132125
133126 if (!(await pathExists(binaryPath))) {
executor/scripts/release/build-release.ts+0−622
@@ -1,622 +0,0 @@
1−#!/usr/bin/env bun
2−
3−import fs from "node:fs/promises";
4−import os from "node:os";
5−import path from "node:path";
6−import { createHash, generateKeyPairSync } from "node:crypto";
7−import net from "node:net";
8−
9−import { managedRuntimeVersions } from "../../packages/core/src/managed/runtime-info";
10−
11−type ReleaseTarget = {
12− platform: "linux" | "darwin";
13− arch: "x64" | "arm64";
14− bunTarget: string;
15−};
16−
17−const 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−
24−const convexBackendRepo = "get-convex/convex-backend";
25−const defaultManagedSitePort = 5411;
26−
27−function 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−
37−function archiveName(platform: ReleaseTarget["platform"], arch: ReleaseTarget["arch"]): string {
38− return `executor-${platform}-${arch}.tar.gz`;
39−}
40−
41−function webArchiveName(platform: ReleaseTarget["platform"], arch: ReleaseTarget["arch"]): string {
42− return `executor-web-${platform}-${arch}.tar.gz`;
43−}
44−
45−function runtimeArchiveName(platform: ReleaseTarget["platform"], arch: ReleaseTarget["arch"]): string {
46− return `executor-runtime-${platform}-${arch}.tar.gz`;
47−}
48−
49−function 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−
62−function nodeArchiveName(target: ReleaseTarget): string {
63− return `node-v${managedRuntimeVersions.nodeVersion}-${target.platform}-${target.arch}.tar.gz`;
64−}
65−
66−function backendBinaryName(target: ReleaseTarget): string {
67− return target.platform === "linux" || target.platform === "darwin"
68− ? "convex-local-backend"
69− : "convex-local-backend.exe";
70−}
71−
72−async 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−
77−async 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−
91−async function runArchiveCommand(command: string[], cwd?: string): Promise<void> {
92− await runCommand(command, { cwd });
93−}
94−
95−async function pathExists(filePath: string): Promise<boolean> {
96− try {
97− await fs.access(filePath);
98− return true;
99− } catch {
100− return false;
101− }
102−}
103−
104−async function downloadFile(url: string, destination: string): Promise<void> {
105− await runCommand(["curl", "-fsSL", url, "-o", destination]);
106−}
107−
108−async 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−
121−async 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−
137−async 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−
149−async 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−
160−type AnonymousAuthSeed = {
161− privateKeyPem: string;
162− publicKeyPem: string;
163− apiKeySecret: string;
164−};
165−
166−type RuntimeConfig = {
167− instanceName: string;
168− instanceSecret: string;
169− hostInterface: string;
170− backendPort: number;
171− siteProxyPort: number;
172−};
173−
174−type TanstackStartOutput = {
175− outputRoot: string;
176− serverEntry: string;
177− stagedServerEntry: string;
178−};
179−
180−function normalizePemForEnv(value: string): string {
181− return value.replace(/\r\n/g, "\n").replace(/\n/g, "\\n").trim();
182−}
183−
184−function 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−
205−async 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−
237−async 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−
261−async 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−
287−async 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−
322−async 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−
377−async 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−
439−async 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−
509−async 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−
576−async 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−
622−await main();