Showing the changes to apps/executor (8 of 16 files). Show all
Unified · Split
| @@ -29,12 +29,8 @@ · 28 unmodified lines | ||
| 29 | 29 | "test": "vitest run", |
| 30 | 30 | "test:watch": "vitest", |
| 31 | 31 | "start": "bun src/cli/main.ts", |
| 32 | − "build:dist": "bun src/distribution/build-package.ts", | |
| 33 | − "pack:dist": "bun src/distribution/build-package.ts && npm pack ./dist/npm", | |
| 34 | − "publish:dist:dry-run": "bun src/distribution/build-package.ts && npm publish ./dist/npm --dry-run", | |
| 35 | − "publish:dist": "bun src/distribution/build-package.ts && npm publish ./dist/npm", | |
| 36 | − "build:portable": "bun src/distribution/build-portable.ts", | |
| 37 | − "build:release": "bun src/distribution/build-release.ts" | |
| 32 | + "release:publish:dry-run": "bun src/distribution/publish.ts --dry-run", | |
| 33 | + "release:publish": "bun src/distribution/publish.ts" | |
| 38 | 34 | }, |
| 39 | 35 | "dependencies": { |
| 40 | 36 | "@effect/cli": "catalog:", |
| @@ -7,7 +7,6 @@ import { dirname, join, resolve } from "node:path"; · 6 unmodified lines | ||
| 7 | 7 | import { readDistributionPackageMetadata, repoRoot } from "./metadata"; |
| 8 | 8 | |
| 9 | 9 | const defaultOutputDir = resolve(repoRoot, "apps/executor/dist/npm"); |
| 10 | −const DENO_INSTALL_URL = "https://docs.deno.com/runtime/getting_started/installation/"; | |
| 11 | 10 | |
| 12 | 11 | |
| 13 | 12 | export type BuildDistributionPackageOptions = { |
| @@ -72,26 +71,16 @@ const runCommand = async (input: CommandInput): Promise<void> => { · 58 unmodified lines | ||
| 72 | 71 | ); |
| 73 | 72 | }; |
| 74 | 73 | |
| 75 | −const resolvePGliteDistDir = (): string => { | |
| 76 | − const requireFromControlPlane = createRequire( | |
| 77 | − join(repoRoot, "packages/control-plane/package.json"), | |
| 78 | − ); | |
| 79 | − const entryPath = requireFromControlPlane.resolve("@electric-sql/pglite"); | |
| 80 | − const distDir = dirname(entryPath); | |
| 81 | − | |
| 82 | − if (!existsSync(distDir)) { | |
| 83 | − throw new Error(`Unable to locate PGlite dist directory at ${distDir}`); | |
| 84 | − } | |
| 85 | − | |
| 86 | − return distDir; | |
| 87 | −}; | |
| 88 | − | |
| 89 | 74 | const resolveQuickJsWasmPath = (): string => { |
| 90 | 75 | const requireFromQuickJsRuntime = createRequire( |
| 91 | 76 | join(repoRoot, "packages/runtime-quickjs/package.json"), |
| 92 | 77 | ); |
| 93 | − const wasmPath = requireFromQuickJsRuntime.resolve( | |
| 94 | − "@jitl/quickjs-wasmfile-release-sync/wasm", | |
| 78 | + const quickJsPackagePath = requireFromQuickJsRuntime.resolve( | |
| 79 | + "quickjs-emscripten/package.json", | |
| 80 | + ); | |
| 81 | + const wasmPath = resolve( | |
| 82 | + dirname(quickJsPackagePath), | |
| 83 | + "../@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.wasm", | |
| 95 | 84 | ); |
| 96 | 85 | |
| 97 | 86 | if (!existsSync(wasmPath)) { |
| @@ -131,9 +120,6 @@ const createPackageJson = (input: { · 33 unmodified lines | ||
| 131 | 120 | bin: { |
| 132 | 121 | executor: "bin/executor.js", |
| 133 | 122 | }, |
| 134 | − scripts: { | |
| 135 | − postinstall: "node ./bin/postinstall.js", | |
| 136 | − }, | |
| 137 | 123 | files: [ |
| 138 | 124 | "bin", |
| 139 | 125 | "resources", |
| @@ -154,48 +140,6 @@ const createLauncherSource = () => [ · 14 unmodified lines | ||
| 154 | 140 | "", |
| 155 | 141 | ].join("\n"); |
| 156 | 142 | |
| 157 | −const createPostinstallSource = () => [ | |
| 158 | − "#!/usr/bin/env node", | |
| 159 | − 'import { spawnSync } from "node:child_process";', | |
| 160 | − 'import { existsSync } from "node:fs";', | |
| 161 | − 'import { join } from "node:path";', | |
| 162 | − "", | |
| 163 | − `const denoInstallUrl = ${JSON.stringify(DENO_INSTALL_URL)};`, | |
| 164 | − "", | |
| 165 | − "const resolveDenoExecutable = () => {", | |
| 166 | − ' const configured = process.env.DENO_BIN?.trim();', | |
| 167 | − ' if (configured) {', | |
| 168 | − ' return configured;', | |
| 169 | − ' }', | |
| 170 | − "", | |
| 171 | − ' const home = process.env.HOME?.trim();', | |
| 172 | − ' if (home) {', | |
| 173 | − ' const installedPath = join(home, ".deno", "bin", "deno");', | |
| 174 | − ' if (existsSync(installedPath)) {', | |
| 175 | − ' return installedPath;', | |
| 176 | − ' }', | |
| 177 | − ' }', | |
| 178 | − "", | |
| 179 | − ' return "deno";', | |
| 180 | − "};", | |
| 181 | − "", | |
| 182 | − "const isDenoAvailable = (executable) => {", | |
| 183 | − ' const result = spawnSync(executable, ["--version"], {', | |
| 184 | − ' stdio: "ignore",', | |
| 185 | − ' timeout: 5000,', | |
| 186 | − ' });', | |
| 187 | − "", | |
| 188 | − ' return result.error === undefined && result.status === 0;', | |
| 189 | − "};", | |
| 190 | − "", | |
| 191 | − "const denoExecutable = resolveDenoExecutable();", | |
| 192 | − "", | |
| 193 | − "if (!isDenoAvailable(denoExecutable)) {", | |
| 194 | − ' console.warn(`Deno not found, if you want to use deno for sandboxing install it from ${denoInstallUrl} otherwise JS will execute in QuickJS`);', | |
| 195 | − "}", | |
| 196 | − "", | |
| 197 | −].join("\n"); | |
| 198 | − | |
| 199 | 143 | export const buildDistributionPackage = async ( |
| 200 | 144 | options: BuildDistributionPackageOptions = {}, |
| 201 | 145 | ): Promise<DistributionPackageArtifact> => { |
| @@ -204,13 +148,8 @@ export const buildDistributionPackage = async ( · 2 unmodified lines | ||
| 204 | 148 | const binDir = join(packageDir, "bin"); |
| 205 | 149 | const resourcesDir = join(packageDir, "resources"); |
| 206 | 150 | const webDir = join(resourcesDir, "web"); |
| 207 | − const migrationsDir = join(resourcesDir, "migrations"); | |
| 208 | 151 | const bundlePath = join(binDir, "executor.mjs"); |
| 209 | 152 | const launcherPath = join(binDir, "executor.js"); |
| 210 | − const postinstallPath = join(binDir, "postinstall.js"); | |
| 211 | − const pgliteDistDir = resolvePGliteDistDir(); | |
| 212 | − const pgliteDataPath = join(pgliteDistDir, "pglite.data"); | |
| 213 | − const pgliteWasmPath = join(pgliteDistDir, "pglite.wasm"); | |
| 214 | 153 | const quickJsWasmPath = resolveQuickJsWasmPath(); |
| 215 | 154 | const webDistDir = join(repoRoot, "apps/web/dist"); |
| 216 | 155 | const readmePath = join(repoRoot, "README.md"); |
| @@ -246,11 +185,6 @@ export const buildDistributionPackage = async ( · 29 unmodified lines | ||
| 246 | 185 | }); |
| 247 | 186 | |
| 248 | 187 | await cp(webDistDir, webDir, { recursive: true }); |
| 249 | − await cp(join(repoRoot, "packages/control-plane/drizzle"), migrationsDir, { | |
| 250 | − recursive: true, | |
| 251 | − }); | |
| 252 | − await cp(pgliteDataPath, join(binDir, "pglite.data")); | |
| 253 | − await cp(pgliteWasmPath, join(binDir, "pglite.wasm")); | |
| 254 | 188 | await cp(quickJsWasmPath, join(binDir, "emscripten-module.wasm")); |
| 255 | 189 | await mkdir(join(binDir, "openapi-extractor-wasm"), { recursive: true }); |
| 256 | 190 | await cp( |
| @@ -273,9 +207,7 @@ export const buildDistributionPackage = async ( · 16 unmodified lines | ||
| 273 | 207 | })); |
| 274 | 208 | await cp(readmePath, join(packageDir, "README.md")); |
| 275 | 209 | await writeFile(launcherPath, createLauncherSource()); |
| 276 | − await writeFile(postinstallPath, createPostinstallSource()); | |
| 277 | 210 | await chmod(launcherPath, 0o755); |
| 278 | − await chmod(postinstallPath, 0o755); | |
| 279 | 211 | |
| 280 | 212 | return { |
| 281 | 213 | packageDir, |
| @@ -1,58 +0,0 @@ | ||
| 1 | −import { buildDistributionPackage } from "./artifact"; | |
| 2 | − | |
| 3 | −type BuildPackageCliOptions = { | |
| 4 | − outputDir?: string; | |
| 5 | − packageName?: string; | |
| 6 | − packageVersion?: string; | |
| 7 | − buildWeb: boolean; | |
| 8 | −}; | |
| 9 | − | |
| 10 | −const parseArgs = (argv: ReadonlyArray<string>): BuildPackageCliOptions => { | |
| 11 | − const options: BuildPackageCliOptions = { | |
| 12 | − buildWeb: true, | |
| 13 | − }; | |
| 14 | − | |
| 15 | − for (let index = 0; index < argv.length; index += 1) { | |
| 16 | − const arg = argv[index]; | |
| 17 | − | |
| 18 | − if (arg === "--skip-web-build") { | |
| 19 | − options.buildWeb = false; | |
| 20 | − continue; | |
| 21 | − } | |
| 22 | − | |
| 23 | − const next = argv[index + 1]; | |
| 24 | − if (!next) { | |
| 25 | − throw new Error(`Missing value for ${arg}`); | |
| 26 | − } | |
| 27 | − | |
| 28 | − if (arg === "--output") { | |
| 29 | − options.outputDir = next; | |
| 30 | − index += 1; | |
| 31 | − continue; | |
| 32 | − } | |
| 33 | − | |
| 34 | − if (arg === "--package-name") { | |
| 35 | − options.packageName = next; | |
| 36 | − index += 1; | |
| 37 | − continue; | |
| 38 | − } | |
| 39 | − | |
| 40 | − if (arg === "--package-version") { | |
| 41 | − options.packageVersion = next; | |
| 42 | − index += 1; | |
| 43 | − continue; | |
| 44 | − } | |
| 45 | − | |
| 46 | − throw new Error(`Unknown argument: ${arg}`); | |
| 47 | − } | |
| 48 | − | |
| 49 | − return options; | |
| 50 | −}; | |
| 51 | − | |
| 52 | −const main = async () => { | |
| 53 | − const options = parseArgs(process.argv.slice(2)); | |
| 54 | − const artifact = await buildDistributionPackage(options); | |
| 55 | − process.stdout.write(`${artifact.packageDir}\n`); | |
| 56 | −}; | |
| 57 | − | |
| 58 | −await main(); | |
| @@ -1,78 +0,0 @@ | ||
| 1 | −import { | |
| 2 | − buildPortableDistribution, | |
| 3 | − type BuildPortableDistributionOptions, | |
| 4 | −} from "./portable"; | |
| 5 | − | |
| 6 | −type BuildPortableCliOptions = BuildPortableDistributionOptions; | |
| 7 | − | |
| 8 | −const parseArgs = (argv: ReadonlyArray<string>): BuildPortableCliOptions => { | |
| 9 | − const options: BuildPortableCliOptions = { | |
| 10 | − buildWeb: true, | |
| 11 | − createArchives: true, | |
| 12 | − }; | |
| 13 | − | |
| 14 | − for (let index = 0; index < argv.length; index += 1) { | |
| 15 | − const arg = argv[index]; | |
| 16 | − | |
| 17 | − if (arg === "--skip-web-build") { | |
| 18 | − options.buildWeb = false; | |
| 19 | − continue; | |
| 20 | − } | |
| 21 | − | |
| 22 | − if (arg === "--no-archive") { | |
| 23 | − options.createArchives = false; | |
| 24 | − continue; | |
| 25 | − } | |
| 26 | − | |
| 27 | − const next = argv[index + 1]; | |
| 28 | − if (!next) { | |
| 29 | − throw new Error(`Missing value for ${arg}`); | |
| 30 | − } | |
| 31 | − | |
| 32 | − if (arg === "--output") { | |
| 33 | − options.outputDir = next; | |
| 34 | − index += 1; | |
| 35 | − continue; | |
| 36 | − } | |
| 37 | − | |
| 38 | − if (arg === "--package-name") { | |
| 39 | − options.packageName = next; | |
| 40 | − index += 1; | |
| 41 | − continue; | |
| 42 | − } | |
| 43 | − | |
| 44 | − if (arg === "--package-version") { | |
| 45 | − options.packageVersion = next; | |
| 46 | − index += 1; | |
| 47 | − continue; | |
| 48 | − } | |
| 49 | − | |
| 50 | − if (arg === "--target") { | |
| 51 | − options.targets = [...(options.targets ?? []), next]; | |
| 52 | − index += 1; | |
| 53 | − continue; | |
| 54 | − } | |
| 55 | − | |
| 56 | − | |
| 57 | − if (arg === "--node-version") { | |
| 58 | − options.nodeVersion = next; | |
| 59 | − index += 1; | |
| 60 | − continue; | |
| 61 | − } | |
| 62 | − | |
| 63 | − throw new Error(`Unknown argument: ${arg}`); | |
| 64 | − } | |
| 65 | − | |
| 66 | − return options; | |
| 67 | −}; | |
| 68 | − | |
| 69 | −const main = async () => { | |
| 70 | − const options = parseArgs(process.argv.slice(2)); | |
| 71 | − const artifact = await buildPortableDistribution(options); | |
| 72 | − | |
| 73 | − for (const bundle of artifact.artifacts) { | |
| 74 | − process.stdout.write(`${bundle.archivePath ?? bundle.bundleDir}\n`); | |
| 75 | − } | |
| 76 | −}; | |
| 77 | − | |
| 78 | −await main(); | |
| @@ -1,137 +0,0 @@ | ||
| 1 | −import { createHash } from "node:crypto"; | |
| 2 | −import { spawn } from "node:child_process"; | |
| 3 | −import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; | |
| 4 | −import { join, relative, resolve } from "node:path"; | |
| 5 | − | |
| 6 | −import { buildDistributionPackage } from "./artifact"; | |
| 7 | −import { repoRoot } from "./metadata"; | |
| 8 | −import { buildPortableDistribution, portableTargetIds } from "./portable"; | |
| 9 | − | |
| 10 | −const defaultReleaseDir = resolve(repoRoot, "apps/executor/dist/release"); | |
| 11 | − | |
| 12 | −type CommandInput = { | |
| 13 | − command: string; | |
| 14 | − args: ReadonlyArray<string>; | |
| 15 | − cwd: string; | |
| 16 | −}; | |
| 17 | − | |
| 18 | −type CommandOutput = { | |
| 19 | − stdout: string; | |
| 20 | − stderr: string; | |
| 21 | −}; | |
| 22 | − | |
| 23 | −type PackResult = { | |
| 24 | − filename?: string; | |
| 25 | −}; | |
| 26 | − | |
| 27 | −const runCommand = async (input: CommandInput): Promise<CommandOutput> => { | |
| 28 | − const child = spawn(input.command, [...input.args], { | |
| 29 | − cwd: input.cwd, | |
| 30 | − env: process.env, | |
| 31 | − stdio: ["ignore", "pipe", "pipe"], | |
| 32 | − }); | |
| 33 | − | |
| 34 | − let stdout = ""; | |
| 35 | − let stderr = ""; | |
| 36 | − | |
| 37 | − child.stdout?.setEncoding("utf8"); | |
| 38 | − child.stdout?.on("data", (chunk) => { | |
| 39 | − stdout += chunk; | |
| 40 | − }); | |
| 41 | − | |
| 42 | − child.stderr?.setEncoding("utf8"); | |
| 43 | − child.stderr?.on("data", (chunk) => { | |
| 44 | − stderr += chunk; | |
| 45 | − }); | |
| 46 | − | |
| 47 | − const exitCode = await new Promise<number>((resolveExitCode, reject) => { | |
| 48 | − child.once("error", reject); | |
| 49 | − child.once("close", (code) => { | |
| 50 | − resolveExitCode(code ?? -1); | |
| 51 | − }); | |
| 52 | − }); | |
| 53 | − | |
| 54 | − if (exitCode !== 0) { | |
| 55 | − throw new Error( | |
| 56 | − [ | |
| 57 | − `${input.command} ${input.args.join(" ")} exited with code ${exitCode}`, | |
| 58 | − stdout.trim().length > 0 ? `stdout:\n${stdout.trim()}` : null, | |
| 59 | − stderr.trim().length > 0 ? `stderr:\n${stderr.trim()}` : null, | |
| 60 | − ] | |
| 61 | − .filter((part) => part !== null) | |
| 62 | − .join("\n\n"), | |
| 63 | − ); | |
| 64 | − } | |
| 65 | − | |
| 66 | − return { | |
| 67 | − stdout: stdout.trim(), | |
| 68 | − stderr: stderr.trim(), | |
| 69 | − }; | |
| 70 | −}; | |
| 71 | − | |
| 72 | −const computeSha256 = (contents: Uint8Array): string => | |
| 73 | − createHash("sha256").update(contents).digest("hex"); | |
| 74 | − | |
| 75 | −const createChecksumsFile = async ( | |
| 76 | − releaseDir: string, | |
| 77 | − assetPaths: ReadonlyArray<string>, | |
| 78 | −): Promise<string> => { | |
| 79 | − const entries = await Promise.all( | |
| 80 | − [...assetPaths] | |
| 81 | − .sort((left, right) => left.localeCompare(right)) | |
| 82 | − .map(async (assetPath) => { | |
| 83 | − const contents = await readFile(assetPath); | |
| 84 | − const relativePath = relative(releaseDir, assetPath).replaceAll("\\", "/"); | |
| 85 | − return `${computeSha256(contents)} ${relativePath}`; | |
| 86 | − }), | |
| 87 | − ); | |
| 88 | − | |
| 89 | − const checksumsPath = join(releaseDir, "checksums.txt"); | |
| 90 | − await writeFile(checksumsPath, `${entries.join("\n")}\n`); | |
| 91 | − return checksumsPath; | |
| 92 | −}; | |
| 93 | − | |
| 94 | −const packDistributionPackage = async (releaseDir: string): Promise<string> => { | |
| 95 | − const output = await runCommand({ | |
| 96 | − command: "npm", | |
| 97 | − args: ["pack", "./apps/executor/dist/npm", "--pack-destination", releaseDir, "--json"], | |
| 98 | − cwd: repoRoot, | |
| 99 | − }); | |
| 100 | − const [result] = JSON.parse(output.stdout) as ReadonlyArray<PackResult>; | |
| 101 | − const filename = result?.filename; | |
| 102 | − | |
| 103 | − if (!filename) { | |
| 104 | − throw new Error(`npm pack did not report an output filename. stdout:\n${output.stdout}`); | |
| 105 | − } | |
| 106 | − | |
| 107 | − return join(releaseDir, filename); | |
| 108 | −}; | |
| 109 | − | |
| 110 | −const main = async () => { | |
| 111 | − const releaseDir = defaultReleaseDir; | |
| 112 | − const portableDir = join(releaseDir, "portable"); | |
| 113 | − | |
| 114 | − await rm(releaseDir, { recursive: true, force: true }); | |
| 115 | − await mkdir(releaseDir, { recursive: true }); | |
| 116 | − | |
| 117 | − await buildDistributionPackage(); | |
| 118 | − const packageArchivePath = await packDistributionPackage(releaseDir); | |
| 119 | − const portableArtifact = await buildPortableDistribution({ | |
| 120 | − outputDir: portableDir, | |
| 121 | − buildWeb: false, | |
| 122 | − targets: portableTargetIds, | |
| 123 | − }); | |
| 124 | − const portableArchivePaths = portableArtifact.artifacts | |
| 125 | − .map((artifact) => artifact.archivePath) | |
| 126 | − .filter((archivePath): archivePath is string => archivePath !== null); | |
| 127 | − const checksumsPath = await createChecksumsFile(releaseDir, [ | |
| 128 | − packageArchivePath, | |
| 129 | − ...portableArchivePaths, | |
| 130 | − ]); | |
| 131 | − | |
| 132 | − for (const assetPath of [packageArchivePath, ...portableArchivePaths, checksumsPath]) { | |
| 133 | − process.stdout.write(`${assetPath}\n`); | |
| 134 | − } | |
| 135 | −}; | |
| 136 | − | |
| 137 | −await main(); | |
| @@ -1,232 +0,0 @@ | ||
| 1 | −import { spawn } from "node:child_process"; | |
| 2 | −import { existsSync } from "node:fs"; | |
| 3 | −import { mkdir, mkdtemp, rm } from "node:fs/promises"; | |
| 4 | −import { createServer } from "node:net"; | |
| 5 | −import { tmpdir } from "node:os"; | |
| 6 | −import { join } from "node:path"; | |
| 7 | − | |
| 8 | −import { describe, expect, it } from "vitest"; | |
| 9 | − | |
| 10 | −import { | |
| 11 | − buildPortableDistribution, | |
| 12 | − resolveDefaultPortableTarget, | |
| 13 | −} from "./portable"; | |
| 14 | − | |
| 15 | −type CommandResult = { | |
| 16 | − stdout: string; | |
| 17 | − stderr: string; | |
| 18 | − exitCode: number; | |
| 19 | −}; | |
| 20 | − | |
| 21 | −const runCommand = async (input: { | |
| 22 | − command: string; | |
| 23 | − args: ReadonlyArray<string>; | |
| 24 | − cwd: string; | |
| 25 | − env: NodeJS.ProcessEnv; | |
| 26 | − okExitCodes?: ReadonlyArray<number>; | |
| 27 | −}): Promise<CommandResult> => { | |
| 28 | − const child = spawn(input.command, [...input.args], { | |
| 29 | − cwd: input.cwd, | |
| 30 | − env: input.env, | |
| 31 | − stdio: ["ignore", "pipe", "pipe"], | |
| 32 | − }); | |
| 33 | − | |
| 34 | − let stdout = ""; | |
| 35 | − let stderr = ""; | |
| 36 | − | |
| 37 | − child.stdout?.setEncoding("utf8"); | |
| 38 | − child.stdout?.on("data", (chunk) => { | |
| 39 | − stdout += chunk; | |
| 40 | − }); | |
| 41 | − | |
| 42 | − child.stderr?.setEncoding("utf8"); | |
| 43 | − child.stderr?.on("data", (chunk) => { | |
| 44 | − stderr += chunk; | |
| 45 | − }); | |
| 46 | − | |
| 47 | − const exitCode = await new Promise<number>((resolveExitCode, reject) => { | |
| 48 | − child.once("error", reject); | |
| 49 | − child.once("close", (code) => { | |
| 50 | − resolveExitCode(code ?? -1); | |
| 51 | − }); | |
| 52 | − }); | |
| 53 | − | |
| 54 | − const result = { | |
| 55 | − stdout: stdout.trim(), | |
| 56 | − stderr: stderr.trim(), | |
| 57 | − exitCode, | |
| 58 | − } satisfies CommandResult; | |
| 59 | − | |
| 60 | − if ((input.okExitCodes ?? [0]).includes(exitCode)) { | |
| 61 | − return result; | |
| 62 | − } | |
| 63 | − | |
| 64 | − throw new Error( | |
| 65 | − [ | |
| 66 | − `${input.command} ${input.args.join(" ")} exited with code ${exitCode}`, | |
| 67 | − result.stdout.length > 0 ? `stdout:\n${result.stdout}` : null, | |
| 68 | − result.stderr.length > 0 ? `stderr:\n${result.stderr}` : null, | |
| 69 | − ].filter((part) => part !== null).join("\n\n"), | |
| 70 | − ); | |
| 71 | −}; | |
| 72 | − | |
| 73 | −const allocatePort = async (): Promise<number> => | |
| 74 | − await new Promise<number>((resolvePort, reject) => { | |
| 75 | − const server = createServer(); | |
| 76 | − server.once("error", reject); | |
| 77 | − server.listen(0, "127.0.0.1", () => { | |
| 78 | − const address = server.address(); | |
| 79 | − if (!address || typeof address === "string") { | |
| 80 | − server.close(() => reject(new Error("Failed to allocate test port"))); | |
| 81 | − return; | |
| 82 | − } | |
| 83 | − | |
| 84 | − server.close((error) => { | |
| 85 | − if (error) { | |
| 86 | − reject(error); | |
| 87 | − return; | |
| 88 | − } | |
| 89 | − | |
| 90 | − resolvePort(address.port); | |
| 91 | − }); | |
| 92 | − }); | |
| 93 | − }); | |
| 94 | − | |
| 95 | −describe("portable distribution flow", () => { | |
| 96 | − it("installs and boots a portable bundle in a fresh home", async () => { | |
| 97 | − const tempRoot = await mkdtemp(join(tmpdir(), "executor-portable-")); | |
| 98 | − | |
| 99 | − try { | |
| 100 | − const distDir = join(tempRoot, "dist"); | |
| 101 | − const homeDir = join(tempRoot, "home"); | |
| 102 | − const binDir = join(homeDir, ".local", "bin"); | |
| 103 | − const installHome = process.platform === "darwin" | |
| 104 | − ? join(homeDir, "Library", "Application Support", "Executor") | |
| 105 | − : join(homeDir, ".local", "share", "executor"); | |
| 106 | − const runHome = process.platform === "darwin" | |
| 107 | − ? join(installHome, "run") | |
| 108 | − : join(homeDir, ".local", "state", "executor", "run"); | |
| 109 | − const legacyExecutorBin = join(homeDir, ".executor", "bin", "executor"); | |
| 110 | − const baseUrl = `http://127.0.0.1:${await allocatePort()}`; | |
| 111 | − | |
| 112 | − await mkdir(homeDir, { recursive: true }); | |
| 113 | − await mkdir(binDir, { recursive: true }); | |
| 114 | − | |
| 115 | − const artifact = await buildPortableDistribution({ | |
| 116 | − outputDir: distDir, | |
| 117 | − buildWeb: false, | |
| 118 | − targets: [resolveDefaultPortableTarget()], | |
| 119 | − }); | |
| 120 | − const bundle = artifact.artifacts[0]!; | |
| 121 | − | |
| 122 | − const env = { | |
| 123 | − ...process.env, | |
| 124 | − HOME: homeDir, | |
| 125 | − PATH: `${binDir}:/usr/bin:/bin`, | |
| 126 | − } satisfies NodeJS.ProcessEnv; | |
| 127 | − | |
| 128 | − await runCommand({ | |
| 129 | − command: "sh", | |
| 130 | − args: [bundle.installScriptPath], | |
| 131 | − cwd: bundle.bundleDir, | |
| 132 | − env, | |
| 133 | − }); | |
| 134 | − | |
| 135 | − await runCommand({ | |
| 136 | − command: "sh", | |
| 137 | − args: [bundle.installScriptPath], | |
| 138 | − cwd: bundle.bundleDir, | |
| 139 | − env, | |
| 140 | − }); | |
| 141 | − | |
| 142 | − expect(existsSync(join(binDir, "executor"))).toBe(true); | |
| 143 | − expect(existsSync(join(installHome, "portable"))).toBe(true); | |
| 144 | − expect(existsSync(legacyExecutorBin)).toBe(false); | |
| 145 | − | |
| 146 | − const initialDoctor = await runCommand({ | |
| 147 | − command: "executor", | |
| 148 | − args: ["doctor", "--json", "--base-url", baseUrl], | |
| 149 | − cwd: tempRoot, | |
| 150 | − env, | |
| 151 | − }); | |
| 152 | − const initialDoctorJson = JSON.parse(initialDoctor.stdout) as { | |
| 153 | − ok: boolean; | |
| 154 | − checks: Record<string, { ok: boolean }>; | |
| 155 | − }; | |
| 156 | − expect(initialDoctorJson.ok).toBe(false); | |
| 157 | − expect(initialDoctorJson.checks.webAssets?.ok).toBe(true); | |
| 158 | − expect(initialDoctorJson.checks.migrations?.ok).toBe(true); | |
| 159 | − | |
| 160 | − await runCommand({ | |
| 161 | − command: "executor", | |
| 162 | − args: ["up", "--base-url", baseUrl], | |
| 163 | − cwd: tempRoot, | |
| 164 | − env, | |
| 165 | − }); | |
| 166 | − | |
| 167 | − const statusResult = await runCommand({ | |
| 168 | − command: "executor", | |
| 169 | − args: ["status", "--json", "--base-url", baseUrl], | |
| 170 | − cwd: tempRoot, | |
| 171 | − env, | |
| 172 | − }); | |
| 173 | − const status = JSON.parse(statusResult.stdout) as { | |
| 174 | − reachable: boolean; | |
| 175 | − pidRunning: boolean; | |
| 176 | − logFile: string; | |
| 177 | − localDataDir: string; | |
| 178 | − pidFile: string; | |
| 179 | − installation: { workspaceId: string; accountId: string } | null; | |
| 180 | − }; | |
| 181 | − expect(status.reachable).toBe(true); | |
| 182 | − expect(status.pidRunning).toBe(true); | |
| 183 | − expect(status.installation).not.toBeNull(); | |
| 184 | − expect(status.logFile.startsWith(runHome)).toBe(true); | |
| 185 | − expect(status.localDataDir.startsWith(join(installHome, "data"))).toBe(true); | |
| 186 | − expect(status.pidFile.startsWith(runHome)).toBe(true); | |
| 187 | − | |
| 188 | − const html = await fetch(new URL("/", baseUrl)); | |
| 189 | − expect(html.status).toBe(200); | |
| 190 | − expect(html.headers.get("content-type")).toContain("text/html"); | |
| 191 | − expect(await html.text()).toContain('<div id="root"></div>'); | |
| 192 | − | |
| 193 | − const installationResponse = await fetch(new URL("/v1/local/installation", baseUrl)); | |
| 194 | − expect(installationResponse.status).toBe(200); | |
| 195 | − const installation = await installationResponse.json() as { | |
| 196 | − workspaceId: string; | |
| 197 | − accountId: string; | |
| 198 | − }; | |
| 199 | − | |
| 200 | − await runCommand({ | |
| 201 | − command: "executor", | |
| 202 | − args: ["down", "--base-url", baseUrl], | |
| 203 | − cwd: tempRoot, | |
| 204 | − env, | |
| 205 | − }); | |
| 206 | − await runCommand({ | |
| 207 | − command: "executor", | |
| 208 | − args: ["up", "--base-url", baseUrl], | |
| 209 | − cwd: tempRoot, | |
| 210 | − env, | |
| 211 | − }); | |
| 212 | − | |
| 213 | − const installationAfterRestartResponse = await fetch(new URL("/v1/local/installation", baseUrl)); | |
| 214 | − expect(installationAfterRestartResponse.status).toBe(200); | |
| 215 | − const installationAfterRestart = await installationAfterRestartResponse.json() as { | |
| 216 | − workspaceId: string; | |
| 217 | − accountId: string; | |
| 218 | − }; | |
| 219 | − expect(installationAfterRestart.workspaceId).toBe(installation.workspaceId); | |
| 220 | − expect(installationAfterRestart.accountId).toBe(installation.accountId); | |
| 221 | − | |
| 222 | − await runCommand({ | |
| 223 | − command: "executor", | |
| 224 | − args: ["down", "--base-url", baseUrl], | |
| 225 | − cwd: tempRoot, | |
| 226 | − env, | |
| 227 | − }); | |
| 228 | − } finally { | |
| 229 | − await rm(tempRoot, { recursive: true, force: true }); | |
| 230 | − } | |
| 231 | − }, 240_000); | |
| 232 | −}); | |
| @@ -1,607 +0,0 @@ | ||
| 1 | −import { createHash } from "node:crypto"; | |
| 2 | −import { spawn } from "node:child_process"; | |
| 3 | −import { existsSync } from "node:fs"; | |
| 4 | −import { | |
| 5 | − chmod, | |
| 6 | − copyFile, | |
| 7 | − cp, | |
| 8 | − mkdir, | |
| 9 | − mkdtemp, | |
| 10 | − readFile, | |
| 11 | − rm, | |
| 12 | − writeFile, | |
| 13 | −} from "node:fs/promises"; | |
| 14 | −import { tmpdir } from "node:os"; | |
| 15 | −import { basename, join, resolve } from "node:path"; | |
| 16 | − | |
| 17 | −import { buildDistributionPackage } from "./artifact"; | |
| 18 | −import { readDistributionPackageMetadata, repoRoot } from "./metadata"; | |
| 19 | − | |
| 20 | −const defaultOutputDir = resolve(repoRoot, "apps/executor/dist/portable"); | |
| 21 | −const defaultNodeCacheDir = resolve(repoRoot, "apps/executor/.cache/portable-node"); | |
| 22 | −const DEFAULT_OFFICIAL_NODE_VERSION = "v24.14.0"; | |
| 23 | − | |
| 24 | −type PortableTarget = { | |
| 25 | − id: string; | |
| 26 | − archiveExtension: "tar.gz" | "zip"; | |
| 27 | − archiveFileName: (nodeVersion: string) => string; | |
| 28 | − extractedExecutablePath: (nodeVersion: string) => string; | |
| 29 | − bundledExecutableName: string; | |
| 30 | −}; | |
| 31 | − | |
| 32 | −const portableTargets: Record<string, PortableTarget> = { | |
| 33 | − "linux-x64": { | |
| 34 | − id: "linux-x64", | |
| 35 | − archiveExtension: "tar.gz", | |
| 36 | − archiveFileName: (nodeVersion: string) => `node-${nodeVersion}-linux-x64.tar.gz`, | |
| 37 | − extractedExecutablePath: (nodeVersion: string) => join(`node-${nodeVersion}-linux-x64`, "bin", "node"), | |
| 38 | − bundledExecutableName: "node", | |
| 39 | − }, | |
| 40 | − "linux-arm64": { | |
| 41 | − id: "linux-arm64", | |
| 42 | − archiveExtension: "tar.gz", | |
| 43 | − archiveFileName: (nodeVersion: string) => `node-${nodeVersion}-linux-arm64.tar.gz`, | |
| 44 | − extractedExecutablePath: (nodeVersion: string) => join(`node-${nodeVersion}-linux-arm64`, "bin", "node"), | |
| 45 | − bundledExecutableName: "node", | |
| 46 | − }, | |
| 47 | − "darwin-x64": { | |
| 48 | − id: "darwin-x64", | |
| 49 | − archiveExtension: "tar.gz", | |
| 50 | − archiveFileName: (nodeVersion: string) => `node-${nodeVersion}-darwin-x64.tar.gz`, | |
| 51 | − extractedExecutablePath: (nodeVersion: string) => join(`node-${nodeVersion}-darwin-x64`, "bin", "node"), | |
| 52 | − bundledExecutableName: "node", | |
| 53 | − }, | |
| 54 | − "darwin-arm64": { | |
| 55 | − id: "darwin-arm64", | |
| 56 | − archiveExtension: "tar.gz", | |
| 57 | − archiveFileName: (nodeVersion: string) => `node-${nodeVersion}-darwin-arm64.tar.gz`, | |
| 58 | − extractedExecutablePath: (nodeVersion: string) => join(`node-${nodeVersion}-darwin-arm64`, "bin", "node"), | |
| 59 | − bundledExecutableName: "node", | |
| 60 | − }, | |
| 61 | − "win-x64": { | |
| 62 | − id: "win-x64", | |
| 63 | − archiveExtension: "zip", | |
| 64 | − archiveFileName: (nodeVersion: string) => `node-${nodeVersion}-win-x64.zip`, | |
| 65 | − extractedExecutablePath: (nodeVersion: string) => join(`node-${nodeVersion}-win-x64`, "node.exe"), | |
| 66 | − bundledExecutableName: "node.exe", | |
| 67 | − }, | |
| 68 | − "win-arm64": { | |
| 69 | − id: "win-arm64", | |
| 70 | − archiveExtension: "zip", | |
| 71 | − archiveFileName: (nodeVersion: string) => `node-${nodeVersion}-win-arm64.zip`, | |
| 72 | − extractedExecutablePath: (nodeVersion: string) => join(`node-${nodeVersion}-win-arm64`, "node.exe"), | |
| 73 | − bundledExecutableName: "node.exe", | |
| 74 | − }, | |
| 75 | −}; | |
| 76 | − | |
| 77 | −export const portableTargetIds = Object.freeze(Object.keys(portableTargets)); | |
| 78 | − | |
| 79 | −export type BuildPortableDistributionOptions = { | |
| 80 | − outputDir?: string; | |
| 81 | − packageName?: string; | |
| 82 | − packageVersion?: string; | |
| 83 | − buildWeb?: boolean; | |
| 84 | − targets?: ReadonlyArray<string>; | |
| 85 | − nodeVersion?: string; | |
| 86 | − createArchives?: boolean; | |
| 87 | −}; | |
| 88 | − | |
| 89 | −export type PortableBundleArtifact = { | |
| 90 | − target: string; | |
| 91 | − bundleDir: string; | |
| 92 | − launcherPath: string; | |
| 93 | − installScriptPath: string; | |
| 94 | − archivePath: string | null; | |
| 95 | − nodeVersion: string; | |
| 96 | −}; | |
| 97 | − | |
| 98 | −export type PortableDistributionArtifact = { | |
| 99 | − outputDir: string; | |
| 100 | − artifacts: ReadonlyArray<PortableBundleArtifact>; | |
| 101 | −}; | |
| 102 | − | |
| 103 | − | |
| 104 | −type NodeShasums = Map<string, string>; | |
| 105 | − | |
| 106 | −type CommandInput = { | |
| 107 | − command: string; | |
| 108 | − args: ReadonlyArray<string>; | |
| 109 | − cwd: string; | |
| 110 | −}; | |
| 111 | − | |
| 112 | −type CommandOutput = { | |
| 113 | − stdout: string; | |
| 114 | − stderr: string; | |
| 115 | −}; | |
| 116 | − | |
| 117 | −const runCommand = async (input: CommandInput): Promise<CommandOutput> => { | |
| 118 | − const child = spawn(input.command, [...input.args], { | |
| 119 | − cwd: input.cwd, | |
| 120 | − env: process.env, | |
| 121 | − stdio: ["ignore", "pipe", "pipe"], | |
| 122 | − }); | |
| 123 | − | |
| 124 | − let stdout = ""; | |
| 125 | − let stderr = ""; | |
| 126 | − | |
| 127 | − child.stdout?.setEncoding("utf8"); | |
| 128 | − child.stdout?.on("data", (chunk) => { | |
| 129 | − stdout += chunk; | |
| 130 | − }); | |
| 131 | − | |
| 132 | − child.stderr?.setEncoding("utf8"); | |
| 133 | − child.stderr?.on("data", (chunk) => { | |
| 134 | − stderr += chunk; | |
| 135 | − }); | |
| 136 | − | |
| 137 | − const exitCode = await new Promise<number>((resolveExitCode, reject) => { | |
| 138 | − child.once("error", reject); | |
| 139 | − child.once("close", (code) => { | |
| 140 | − resolveExitCode(code ?? -1); | |
| 141 | − }); | |
| 142 | − }); | |
| 143 | − | |
| 144 | − if (exitCode !== 0) { | |
| 145 | − throw new Error( | |
| 146 | − [ | |
| 147 | − `${input.command} ${input.args.join(" ")} exited with code ${exitCode}`, | |
| 148 | − stdout.trim().length > 0 ? `stdout:\n${stdout.trim()}` : null, | |
| 149 | − stderr.trim().length > 0 ? `stderr:\n${stderr.trim()}` : null, | |
| 150 | − ] | |
| 151 | − .filter((part) => part !== null) | |
| 152 | − .join("\n\n"), | |
| 153 | − ); | |
| 154 | − } | |
| 155 | − | |
| 156 | − return { | |
| 157 | − stdout: stdout.trim(), | |
| 158 | − stderr: stderr.trim(), | |
| 159 | − }; | |
| 160 | −}; | |
| 161 | − | |
| 162 | −const normalizeNodeVersion = (value: string): string => | |
| 163 | − value.startsWith("v") ? value : `v${value}`; | |
| 164 | − | |
| 165 | −const computeSha256 = (contents: Uint8Array): string => | |
| 166 | − createHash("sha256").update(contents).digest("hex"); | |
| 167 | − | |
| 168 | −const parseNodeShasums = (contents: string): NodeShasums => | |
| 169 | − new Map( | |
| 170 | − contents | |
| 171 | − .split(/\r?\n/u) | |
| 172 | − .map((line) => line.trim()) | |
| 173 | − .filter((line) => line.length > 0) | |
| 174 | − .flatMap((line) => { | |
| 175 | − const match = /^(?<sha>[a-f0-9]{64})\s+(?<filename>.+)$/iu.exec(line); | |
| 176 | − if (!match?.groups) { | |
| 177 | − return []; | |
| 178 | − } | |
| 179 | − | |
| 180 | − return [[match.groups.filename, match.groups.sha.toLowerCase()] as const]; | |
| 181 | − }), | |
| 182 | − ); | |
| 183 | − | |
| 184 | −const getHostTargetId = (): string => { | |
| 185 | − if (process.platform === "linux" && process.arch === "x64") { | |
| 186 | − return "linux-x64"; | |
| 187 | − } | |
| 188 | − | |
| 189 | − if (process.platform === "linux" && process.arch === "arm64") { | |
| 190 | − return "linux-arm64"; | |
| 191 | − } | |
| 192 | − | |
| 193 | − if (process.platform === "darwin" && process.arch === "x64") { | |
| 194 | − return "darwin-x64"; | |
| 195 | − } | |
| 196 | − | |
| 197 | − if (process.platform === "darwin" && process.arch === "arm64") { | |
| 198 | − return "darwin-arm64"; | |
| 199 | − } | |
| 200 | − | |
| 201 | − if (process.platform === "win32" && process.arch === "x64") { | |
| 202 | − return "win-x64"; | |
| 203 | − } | |
| 204 | − | |
| 205 | − if (process.platform === "win32" && process.arch === "arm64") { | |
| 206 | − return "win-arm64"; | |
| 207 | − } | |
| 208 | − | |
| 209 | − throw new Error(`Unsupported host platform for portable bundle: ${process.platform}-${process.arch}`); | |
| 210 | −}; | |
| 211 | − | |
| 212 | −export const resolveDefaultPortableTarget = (): string => getHostTargetId(); | |
| 213 | − | |
| 214 | −const resolveTargets = (requested: ReadonlyArray<string> | undefined): ReadonlyArray<PortableTarget> => { | |
| 215 | − const targetIds = requested && requested.length > 0 ? requested : [getHostTargetId()]; | |
| 216 | − | |
| 217 | − return targetIds.map((targetId) => { | |
| 218 | − const target = portableTargets[targetId]; | |
| 219 | − if (!target) { | |
| 220 | − throw new Error(`Unsupported portable target: ${targetId}`); | |
| 221 | − } | |
| 222 | − return target; | |
| 223 | − }); | |
| 224 | −}; | |
| 225 | − | |
| 226 | − | |
| 227 | −const resolveNodeVersion = async (requestedVersion: string | undefined): Promise<string> => { | |
| 228 | − if (!requestedVersion) { | |
| 229 | − return DEFAULT_OFFICIAL_NODE_VERSION; | |
| 230 | − } | |
| 231 | − | |
| 232 | − return normalizeNodeVersion(requestedVersion); | |
| 233 | −}; | |
| 234 | − | |
| 235 | − | |
| 236 | −const ensureOfficialNodeRuntime = async (input: { | |
| 237 | − target: PortableTarget; | |
| 238 | − nodeVersion: string; | |
| 239 | − cacheDir: string; | |
| 240 | − destinationPath: string; | |
| 241 | −}): Promise<void> => { | |
| 242 | − const archiveName = input.target.archiveFileName(input.nodeVersion); | |
| 243 | − const archivePath = join(input.cacheDir, archiveName); | |
| 244 | − const shasumsPath = join(input.cacheDir, "SHASUMS256.txt"); | |
| 245 | − const url = `https://nodejs.org/dist/${input.nodeVersion}/${archiveName}`; | |
| 246 | − const shasumsUrl = `https://nodejs.org/dist/${input.nodeVersion}/SHASUMS256.txt`; | |
| 247 | − | |
| 248 | − await mkdir(input.cacheDir, { recursive: true }); | |
| 249 | − | |
| 250 | − let shasumsContents = existsSync(shasumsPath) | |
| 251 | − ? await readFile(shasumsPath, "utf8") | |
| 252 | − : null; | |
| 253 | − | |
| 254 | − if (shasumsContents === null) { | |
| 255 | − const response = await fetch(shasumsUrl); | |
| 256 | − if (!response.ok) { | |
| 257 | − throw new Error(`Unable to download ${shasumsUrl}: ${response.status} ${response.statusText}`); | |
| 258 | − } | |
| 259 | − | |
| 260 | − shasumsContents = await response.text(); | |
| 261 | − await writeFile(shasumsPath, shasumsContents); | |
| 262 | − } | |
| 263 | − | |
| 264 | − const shasums = parseNodeShasums(shasumsContents); | |
| 265 | − const expectedChecksum = shasums.get(archiveName); | |
| 266 | − if (!expectedChecksum) { | |
| 267 | − throw new Error(`Missing checksum for ${archiveName} in ${shasumsUrl}`); | |
| 268 | − } | |
| 269 | − | |
| 270 | − const ensureArchiveMatchesChecksum = async (): Promise<void> => { | |
| 271 | − if (!existsSync(archivePath)) { | |
| 272 | − return; | |
| 273 | − } | |
| 274 | − | |
| 275 | − const existingArchive = await readFile(archivePath); | |
| 276 | − if (computeSha256(existingArchive) === expectedChecksum) { | |
| 277 | − return; | |
| 278 | − } | |
| 279 | − | |
| 280 | − await rm(archivePath, { force: true }); | |
| 281 | − }; | |
| 282 | − | |
| 283 | − await ensureArchiveMatchesChecksum(); | |
| 284 | − | |
| 285 | − if (!existsSync(archivePath)) { | |
| 286 | − const response = await fetch(url); | |
| 287 | − if (!response.ok) { | |
| 288 | − throw new Error(`Unable to download ${url}: ${response.status} ${response.statusText}`); | |
| 289 | − } | |
| 290 | − | |
| 291 | − const bytes = Buffer.from(await response.arrayBuffer()); | |
| 292 | − const actualChecksum = computeSha256(bytes); | |
| 293 | − if (actualChecksum !== expectedChecksum) { | |
| 294 | − throw new Error( | |
| 295 | − `Checksum mismatch for ${archiveName}: expected ${expectedChecksum}, received ${actualChecksum}`, | |
| 296 | − ); | |
| 297 | − } | |
| 298 | − | |
| 299 | − await writeFile(archivePath, bytes); | |
| 300 | − } | |
| 301 | − | |
| 302 | − const extractDir = await mkdtemp(join(tmpdir(), "executor-portable-node-")); | |
| 303 | − try { | |
| 304 | − if (input.target.archiveExtension === "tar.gz") { | |
| 305 | − await runCommand({ | |
| 306 | − command: "tar", | |
| 307 | − args: ["-xzf", archivePath, "-C", extractDir], | |
| 308 | − cwd: repoRoot, | |
| 309 | − }); | |
| 310 | − } else { | |
| 311 | − await runCommand({ | |
| 312 | − command: "unzip", | |
| 313 | − args: ["-q", archivePath, "-d", extractDir], | |
| 314 | − cwd: repoRoot, | |
| 315 | − }); | |
| 316 | − } | |
| 317 | − | |
| 318 | − const extractedExecutable = join( | |
| 319 | − extractDir, | |
| 320 | − input.target.extractedExecutablePath(input.nodeVersion), | |
| 321 | − ); | |
| 322 | − | |
| 323 | − if (!existsSync(extractedExecutable)) { | |
| 324 | − throw new Error(`Missing extracted Node.js executable at ${extractedExecutable}`); | |
| 325 | − } | |
| 326 | − | |
| 327 | − await copyFile(extractedExecutable, input.destinationPath); | |
| 328 | − } finally { | |
| 329 | − await rm(extractDir, { recursive: true, force: true }); | |
| 330 | − } | |
| 331 | −}; | |
| 332 | − | |
| 333 | −const createUnixLauncher = (): string => [ | |
| 334 | − "#!/bin/sh", | |
| 335 | − "set -eu", | |
| 336 | − 'SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)', | |
| 337 | − 'exec "$SCRIPT_DIR/runtime/node" "$SCRIPT_DIR/bin/executor.mjs" "$@"', | |
| 338 | − "", | |
| 339 | −].join("\n"); | |
| 340 | − | |
| 341 | −const createWindowsLauncher = (): string => [ | |
| 342 | − "@echo off", | |
| 343 | − 'set "SCRIPT_DIR=%~dp0"', | |
| 344 | − '"%SCRIPT_DIR%runtime\\node.exe" "%SCRIPT_DIR%bin\\executor.mjs" %*', | |
| 345 | − "", | |
| 346 | −].join("\r\n"); | |
| 347 | − | |
| 348 | −const createInstallScript = (input: { | |
| 349 | − packageName: string; | |
| 350 | − packageVersion: string; | |
| 351 | − target: string; | |
| 352 | −}) => [ | |
| 353 | − "#!/bin/sh", | |
| 354 | − "set -eu", | |
| 355 | − 'SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)', | |
| 356 | − 'DEFAULT_INSTALL_HOME=${EXECUTOR_INSTALL_HOME:-}', | |
| 357 | − 'if [ -z "$DEFAULT_INSTALL_HOME" ]; then', | |
| 358 | − ' if [ "$(uname -s)" = "Darwin" ]; then', | |
| 359 | − ' DEFAULT_INSTALL_HOME="$HOME/Library/Application Support/Executor"', | |
| 360 | − ' else', | |
| 361 | − ' DEFAULT_INSTALL_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/executor"', | |
| 362 | − ' fi', | |
| 363 | − 'fi', | |
| 364 | − 'DEFAULT_BIN_DIR=${EXECUTOR_BIN_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}', | |
| 365 | − `PACKAGE_NAME=${JSON.stringify(input.packageName)}`, | |
| 366 | − `PACKAGE_VERSION=${JSON.stringify(input.packageVersion)}`, | |
| 367 | − `TARGET_ID=${JSON.stringify(input.target)}`, | |
| 368 | − 'INSTALL_DIR="$DEFAULT_INSTALL_HOME/portable/$PACKAGE_VERSION/$TARGET_ID"', | |
| 369 | − 'INSTALL_PARENT=$(dirname "$INSTALL_DIR")', | |
| 370 | − 'STAGING_DIR="$INSTALL_PARENT/.install-$PACKAGE_VERSION-$TARGET_ID-$$"', | |
| 371 | − 'PREVIOUS_DIR="$INSTALL_PARENT/.previous-$PACKAGE_VERSION-$TARGET_ID-$$"', | |
| 372 | − 'BIN_DIR="$DEFAULT_BIN_DIR"', | |
| 373 | − "", | |
| 374 | − 'while [ "$#" -gt 0 ]; do', | |
| 375 | − ' case "$1" in', | |
| 376 | − ' --install-dir)', | |
| 377 | − ' INSTALL_DIR="$2"', | |
| 378 | − ' INSTALL_PARENT=$(dirname "$INSTALL_DIR")', | |
| 379 | − ' STAGING_DIR="$INSTALL_PARENT/.install-$PACKAGE_VERSION-$TARGET_ID-$$"', | |
| 380 | − ' PREVIOUS_DIR="$INSTALL_PARENT/.previous-$PACKAGE_VERSION-$TARGET_ID-$$"', | |
| 381 | − ' shift 2', | |
| 382 | − ' ;;', | |
| 383 | − ' --bin-dir)', | |
| 384 | − ' BIN_DIR="$2"', | |
| 385 | − ' shift 2', | |
| 386 | − ' ;;', | |
| 387 | − ' *)', | |
| 388 | − ' printf "Unknown argument: %s\\n" "$1" >&2', | |
| 389 | − ' exit 1', | |
| 390 | − ' ;;', | |
| 391 | − ' esac', | |
| 392 | − 'done', | |
| 393 | − "", | |
| 394 | − 'rm -rf "$STAGING_DIR" "$PREVIOUS_DIR"', | |
| 395 | − 'mkdir -p "$INSTALL_PARENT" "$STAGING_DIR" "$BIN_DIR"', | |
| 396 | − 'cp -R "$SCRIPT_DIR/bin" "$STAGING_DIR/bin"', | |
| 397 | − 'cp -R "$SCRIPT_DIR/runtime" "$STAGING_DIR/runtime"', | |
| 398 | − 'cp -R "$SCRIPT_DIR/resources" "$STAGING_DIR/resources"', | |
| 399 | − 'cp "$SCRIPT_DIR/package.json" "$STAGING_DIR/package.json"', | |
| 400 | − 'cp "$SCRIPT_DIR/executor" "$STAGING_DIR/executor"', | |
| 401 | − 'cp "$SCRIPT_DIR/executor.cmd" "$STAGING_DIR/executor.cmd"', | |
| 402 | − 'cp "$SCRIPT_DIR/install.sh" "$STAGING_DIR/install.sh"', | |
| 403 | − 'cp "$SCRIPT_DIR/install.ps1" "$STAGING_DIR/install.ps1"', | |
| 404 | − 'chmod 755 "$STAGING_DIR/executor" "$STAGING_DIR/runtime/node"', | |
| 405 | − 'if [ -e "$INSTALL_DIR" ]; then mv "$INSTALL_DIR" "$PREVIOUS_DIR"; fi', | |
| 406 | − 'mv "$STAGING_DIR" "$INSTALL_DIR"', | |
| 407 | − 'rm -rf "$PREVIOUS_DIR"', | |
| 408 | − 'WRAPPER_PATH="$BIN_DIR/executor"', | |
| 409 | − 'WRAPPER_TEMP="$WRAPPER_PATH.tmp.$$"', | |
| 410 | − 'cat > "$WRAPPER_TEMP" <<EOF', | |
| 411 | − '#!/bin/sh', | |
| 412 | − 'exec "$INSTALL_DIR/executor" "\\$@"', | |
| 413 | − 'EOF', | |
| 414 | − 'chmod 755 "$WRAPPER_TEMP"', | |
| 415 | − 'mv "$WRAPPER_TEMP" "$WRAPPER_PATH"', | |
| 416 | − 'printf "Installed %s %s to %s\\n" "$PACKAGE_NAME" "$PACKAGE_VERSION" "$INSTALL_DIR"', | |
| 417 | − 'printf "Launcher: %s\\n" "$BIN_DIR/executor"', | |
| 418 | − 'case ":${PATH}:" in', | |
| 419 | − ' *":${BIN_DIR}:"*) ;;', | |
| 420 | − ' *) printf "Add %s to PATH to run `executor` directly.\\n" "$BIN_DIR" ;;', | |
| 421 | − 'esac', | |
| 422 | − "", | |
| 423 | −].join("\n"); | |
| 424 | − | |
| 425 | −const createInstallPowerShellScript = (input: { | |
| 426 | − packageName: string; | |
| 427 | − packageVersion: string; | |
| 428 | − target: string; | |
| 429 | −}) => [ | |
| 430 | − 'param(', | |
| 431 | − ' [string]$InstallHome = $(if ($env:EXECUTOR_INSTALL_HOME) { $env:EXECUTOR_INSTALL_HOME } elseif ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Executor" } else { Join-Path $env:USERPROFILE "AppData\\Local\\Executor" }),', | |
| 432 | − ' [string]$BinDir = $(if ($env:EXECUTOR_BIN_DIR) { $env:EXECUTOR_BIN_DIR } else { Join-Path $env:USERPROFILE ".local\\bin" })', | |
| 433 | − ')', | |
| 434 | − '$ErrorActionPreference = "Stop"', | |
| 435 | − '$InstallDir = Join-Path $InstallHome "portable\\' + input.packageVersion + '\\' + input.target + '"', | |
| 436 | − '$InstallParent = Split-Path -Parent $InstallDir', | |
| 437 | − '$StageDir = Join-Path $InstallParent (".install-' + input.packageVersion + '-' + input.target + '-" + [Guid]::NewGuid().ToString("N"))', | |
| 438 | − '$PreviousDir = Join-Path $InstallParent (".previous-' + input.packageVersion + '-' + input.target + '-" + [Guid]::NewGuid().ToString("N"))', | |
| 439 | − '$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path', | |
| 440 | − 'New-Item -ItemType Directory -Force -Path $InstallParent | Out-Null', | |
| 441 | − 'if (Test-Path $StageDir) { Remove-Item -Path $StageDir -Recurse -Force }', | |
| 442 | − 'if (Test-Path $PreviousDir) { Remove-Item -Path $PreviousDir -Recurse -Force }', | |
| 443 | − 'New-Item -ItemType Directory -Force -Path $StageDir | Out-Null', | |
| 444 | − 'New-Item -ItemType Directory -Force -Path $BinDir | Out-Null', | |
| 445 | − 'Copy-Item -Path (Join-Path $ScriptDir "bin") -Destination (Join-Path $StageDir "bin") -Recurse -Force', | |
| 446 | − 'Copy-Item -Path (Join-Path $ScriptDir "runtime") -Destination (Join-Path $StageDir "runtime") -Recurse -Force', | |
| 447 | − 'Copy-Item -Path (Join-Path $ScriptDir "resources") -Destination (Join-Path $StageDir "resources") -Recurse -Force', | |
| 448 | − 'Copy-Item -Path (Join-Path $ScriptDir "package.json") -Destination (Join-Path $StageDir "package.json") -Force', | |
| 449 | − 'Copy-Item -Path (Join-Path $ScriptDir "executor") -Destination (Join-Path $StageDir "executor") -Force', | |
| 450 | − 'Copy-Item -Path (Join-Path $ScriptDir "executor.cmd") -Destination (Join-Path $StageDir "executor.cmd") -Force', | |
| 451 | − 'Copy-Item -Path (Join-Path $ScriptDir "install.sh") -Destination (Join-Path $StageDir "install.sh") -Force', | |
| 452 | − 'Copy-Item -Path (Join-Path $ScriptDir "install.ps1") -Destination (Join-Path $StageDir "install.ps1") -Force', | |
| 453 | − 'if (Test-Path $InstallDir) { Move-Item -Path $InstallDir -Destination $PreviousDir -Force }', | |
| 454 | − 'Move-Item -Path $StageDir -Destination $InstallDir -Force', | |
| 455 | − 'if (Test-Path $PreviousDir) { Remove-Item -Path $PreviousDir -Recurse -Force }', | |
| 456 | − '$WrapperPath = Join-Path $BinDir "executor.cmd"', | |
| 457 | − '$WrapperTemp = "$WrapperPath.tmp"', | |
| 458 | − 'Set-Content -Path $WrapperTemp -Value ("@echo off`r`n\"" + (Join-Path $InstallDir "executor.cmd") + "\" %*`r`n") -NoNewline', | |
| 459 | − 'Move-Item -Path $WrapperTemp -Destination $WrapperPath -Force', | |
| 460 | − 'Write-Host "Installed ' + input.packageName + ' ' + input.packageVersion + ' to $InstallDir"', | |
| 461 | − 'Write-Host "Launcher: $WrapperPath"', | |
| 462 | −].join("\r\n"); | |
| 463 | − | |
| 464 | −const createArchive = async (input: { | |
| 465 | − bundleDir: string; | |
| 466 | − archivePath: string; | |
| 467 | − target: PortableTarget; | |
| 468 | − outputDir: string; | |
| 469 | −}): Promise<void> => { | |
| 470 | − const bundleName = basename(input.bundleDir); | |
| 471 | − | |
| 472 | − if (input.target.archiveExtension === "tar.gz") { | |
| 473 | − await runCommand({ | |
| 474 | − command: "tar", | |
| 475 | − args: ["-czf", input.archivePath, "-C", input.outputDir, bundleName], | |
| 476 | − cwd: repoRoot, | |
| 477 | − }); | |
| 478 | − return; | |
| 479 | − } | |
| 480 | − | |
| 481 | − await runCommand({ | |
| 482 | − command: "python3", | |
| 483 | − args: [ | |
| 484 | − "-c", | |
| 485 | − [ | |
| 486 | − "import os, sys, zipfile", | |
| 487 | − "bundle_dir = os.path.abspath(sys.argv[1])", | |
| 488 | − "archive_path = os.path.abspath(sys.argv[2])", | |
| 489 | − "parent = os.path.dirname(bundle_dir)", | |
| 490 | − "with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as archive:", | |
| 491 | − " for root, _, files in os.walk(bundle_dir):", | |
| 492 | − " for name in files:", | |
| 493 | − " path = os.path.join(root, name)", | |
| 494 | − " archive.write(path, os.path.relpath(path, parent))", | |
| 495 | − ].join("\n"), | |
| 496 | − input.bundleDir, | |
| 497 | − input.archivePath, | |
| 498 | − ], | |
| 499 | − cwd: repoRoot, | |
| 500 | − }); | |
| 501 | −}; | |
| 502 | − | |
| 503 | −const ensureRuntimeExecutable = async (input: { | |
| 504 | − target: PortableTarget; | |
| 505 | − nodeVersion: string; | |
| 506 | − cacheDir: string; | |
| 507 | − destinationPath: string; | |
| 508 | −}): Promise<void> => { | |
| 509 | − await ensureOfficialNodeRuntime(input); | |
| 510 | −}; | |
| 511 | − | |
| 512 | −export const buildPortableDistribution = async ( | |
| 513 | − options: BuildPortableDistributionOptions = {}, | |
| 514 | −): Promise<PortableDistributionArtifact> => { | |
| 515 | − const defaults = await readDistributionPackageMetadata(); | |
| 516 | − const packageName = options.packageName ?? defaults.name; | |
| 517 | − const packageVersion = options.packageVersion ?? defaults.version; | |
| 518 | − const outputDir = resolve(options.outputDir ?? defaultOutputDir); | |
| 519 | − const targets = resolveTargets(options.targets); | |
| 520 | − const nodeVersion = await resolveNodeVersion(options.nodeVersion); | |
| 521 | − const createArchives = options.createArchives ?? true; | |
| 522 | − const cacheDir = join(defaultNodeCacheDir, nodeVersion); | |
| 523 | − const stageRoot = await mkdtemp(join(tmpdir(), "executor-portable-stage-")); | |
| 524 | − | |
| 525 | − await rm(outputDir, { recursive: true, force: true }); | |
| 526 | − await mkdir(outputDir, { recursive: true }); | |
| 527 | − | |
| 528 | − try { | |
| 529 | − const stagedPackage = await buildDistributionPackage({ | |
| 530 | − outputDir: join(stageRoot, "package"), | |
| 531 | − packageName, | |
| 532 | − packageVersion, | |
| 533 | − buildWeb: options.buildWeb, | |
| 534 | − }); | |
| 535 | − | |
| 536 | − const artifacts: PortableBundleArtifact[] = []; | |
| 537 | − | |
| 538 | − for (const target of targets) { | |
| 539 | − const bundleName = `${packageName}-${packageVersion}-${target.id}`; | |
| 540 | − const bundleDir = join(outputDir, bundleName); | |
| 541 | − const runtimeDir = join(bundleDir, "runtime"); | |
| 542 | − const runtimeExecutablePath = join(runtimeDir, target.bundledExecutableName); | |
| 543 | − const launcherPath = join(bundleDir, "executor"); | |
| 544 | − const installScriptPath = join(bundleDir, "install.sh"); | |
| 545 | − const archivePath = createArchives | |
| 546 | − ? join(outputDir, `${bundleName}.${target.archiveExtension}`) | |
| 547 | − : null; | |
| 548 | − | |
| 549 | − await cp(stagedPackage.packageDir, bundleDir, { recursive: true }); | |
| 550 | − await mkdir(runtimeDir, { recursive: true }); | |
| 551 | − await ensureRuntimeExecutable({ | |
| 552 | − target, | |
| 553 | − nodeVersion, | |
| 554 | − cacheDir, | |
| 555 | − destinationPath: runtimeExecutablePath, | |
| 556 | − }); | |
| 557 | − await writeFile(launcherPath, createUnixLauncher()); | |
| 558 | − await writeFile(join(bundleDir, "executor.cmd"), createWindowsLauncher()); | |
| 559 | − await writeFile( | |
| 560 | − installScriptPath, | |
| 561 | − createInstallScript({ | |
| 562 | − packageName, | |
| 563 | − packageVersion, | |
| 564 | − target: target.id, | |
| 565 | − }), | |
| 566 | − ); | |
| 567 | − await writeFile( | |
| 568 | − join(bundleDir, "install.ps1"), | |
| 569 | − createInstallPowerShellScript({ | |
| 570 | − packageName, | |
| 571 | − packageVersion, | |
| 572 | − target: target.id, | |
| 573 | − }), | |
| 574 | − ); | |
| 575 | − await chmod(launcherPath, 0o755); | |
| 576 | − await chmod(installScriptPath, 0o755); | |
| 577 | − if (target.bundledExecutableName === "node") { | |
| 578 | − await chmod(runtimeExecutablePath, 0o755); | |
| 579 | − } | |
| 580 | − | |
| 581 | − if (archivePath !== null) { | |
| 582 | − await createArchive({ | |
| 583 | − bundleDir, | |
| 584 | − archivePath, | |
| 585 | − target, | |
| 586 | − outputDir, | |
| 587 | − }); | |
| 588 | − } | |
| 589 | − | |
| 590 | − artifacts.push({ | |
| 591 | − target: target.id, | |
| 592 | − bundleDir, | |
| 593 | − launcherPath, | |
| 594 | − installScriptPath, | |
| 595 | − archivePath, | |
| 596 | − nodeVersion, | |
| 597 | − }); | |
| 598 | − } | |
| 599 | − | |
| 600 | − return { | |
| 601 | − outputDir, | |
| 602 | − artifacts, | |
| 603 | − }; | |
| 604 | − } finally { | |
| 605 | − await rm(stageRoot, { recursive: true, force: true }); | |
| 606 | − } | |
| 607 | −}; | |
| @@ -0,0 +1,224 @@ | ||
| 1 | +import { spawn } from "node:child_process"; | |
| 2 | +import { mkdir, rm } from "node:fs/promises"; | |
| 3 | +import { join, resolve } from "node:path"; | |
| 4 | + | |
| 5 | +import { buildDistributionPackage } from "./artifact"; | |
| 6 | +import { readDistributionPackageMetadata, repoRoot } from "./metadata"; | |
| 7 | + | |
| 8 | +const defaultReleaseDir = resolve(repoRoot, "apps/executor/dist/release"); | |
| 9 | +const semverPattern = | |
| 10 | + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; | |
| 11 | + | |
| 12 | +type PublishCliOptions = { | |
| 13 | + dryRun: boolean; | |
| 14 | +}; | |
| 15 | + | |
| 16 | +type CommandInput = { | |
| 17 | + command: string; | |
| 18 | + args: ReadonlyArray<string>; | |
| 19 | + cwd: string; | |
| 20 | +}; | |
| 21 | + | |
| 22 | +type CommandOutput = { | |
| 23 | + stdout: string; | |
| 24 | + stderr: string; | |
| 25 | +}; | |
| 26 | + | |
| 27 | +type PackResult = { | |
| 28 | + filename?: string; | |
| 29 | +}; | |
| 30 | + | |
| 31 | +const parseArgs = (argv: ReadonlyArray<string>): PublishCliOptions => { | |
| 32 | + const options: PublishCliOptions = { | |
| 33 | + dryRun: false, | |
| 34 | + }; | |
| 35 | + | |
| 36 | + for (const arg of argv) { | |
| 37 | + if (arg === "--dry-run") { | |
| 38 | + options.dryRun = true; | |
| 39 | + continue; | |
| 40 | + } | |
| 41 | + | |
| 42 | + throw new Error(`Unknown argument: ${arg}`); | |
| 43 | + } | |
| 44 | + | |
| 45 | + return options; | |
| 46 | +}; | |
| 47 | + | |
| 48 | +const runCommand = async (input: CommandInput): Promise<CommandOutput> => { | |
| 49 | + const child = spawn(input.command, [...input.args], { | |
| 50 | + cwd: input.cwd, | |
| 51 | + env: process.env, | |
| 52 | + stdio: ["ignore", "pipe", "pipe"], | |
| 53 | + }); | |
| 54 | + | |
| 55 | + let stdout = ""; | |
| 56 | + let stderr = ""; | |
| 57 | + | |
| 58 | + child.stdout?.setEncoding("utf8"); | |
| 59 | + child.stdout?.on("data", (chunk) => { | |
| 60 | + stdout += chunk; | |
| 61 | + }); | |
| 62 | + | |
| 63 | + child.stderr?.setEncoding("utf8"); | |
| 64 | + child.stderr?.on("data", (chunk) => { | |
| 65 | + stderr += chunk; | |
| 66 | + }); | |
| 67 | + | |
| 68 | + const exitCode = await new Promise<number>((resolveExitCode, reject) => { | |
| 69 | + child.once("error", reject); | |
| 70 | + child.once("close", (code) => { | |
| 71 | + resolveExitCode(code ?? -1); | |
| 72 | + }); | |
| 73 | + }); | |
| 74 | + | |
| 75 | + if (exitCode !== 0) { | |
| 76 | + throw new Error( | |
| 77 | + [ | |
| 78 | + `${input.command} ${input.args.join(" ")} exited with code ${exitCode}`, | |
| 79 | + stdout.trim().length > 0 ? `stdout:\n${stdout.trim()}` : null, | |
| 80 | + stderr.trim().length > 0 ? `stderr:\n${stderr.trim()}` : null, | |
| 81 | + ] | |
| 82 | + .filter((part) => part !== null) | |
| 83 | + .join("\n\n"), | |
| 84 | + ); | |
| 85 | + } | |
| 86 | + | |
| 87 | + return { | |
| 88 | + stdout: stdout.trim(), | |
| 89 | + stderr: stderr.trim(), | |
| 90 | + }; | |
| 91 | +}; | |
| 92 | + | |
| 93 | +const resolveTagFromEnvironment = (): string | undefined => { | |
| 94 | + const refName = process.env.GITHUB_REF_NAME?.trim(); | |
| 95 | + if (process.env.GITHUB_REF_TYPE === "tag" && refName) { | |
| 96 | + return refName; | |
| 97 | + } | |
| 98 | + | |
| 99 | + const ref = process.env.GITHUB_REF?.trim(); | |
| 100 | + if (ref?.startsWith("refs/tags/")) { | |
| 101 | + return ref.slice("refs/tags/".length); | |
| 102 | + } | |
| 103 | + | |
| 104 | + return undefined; | |
| 105 | +}; | |
| 106 | + | |
| 107 | +const resolveGitHubRepository = (): string => { | |
| 108 | + const repository = process.env.GH_REPO?.trim() || process.env.GITHUB_REPOSITORY?.trim(); | |
| 109 | + if (!repository) { | |
| 110 | + throw new Error("Set GH_REPO or GITHUB_REPOSITORY before creating a GitHub release."); | |
| 111 | + } | |
| 112 | + | |
| 113 | + return repository; | |
| 114 | +}; | |
| 115 | + | |
| 116 | +const validateVersion = (version: string): void => { | |
| 117 | + if (!semverPattern.test(version)) { | |
| 118 | + throw new Error(`apps/executor/package.json version is not valid semver: ${version}`); | |
| 119 | + } | |
| 120 | +}; | |
| 121 | + | |
| 122 | +const resolveChannel = (version: string): "latest" | "beta" => | |
| 123 | + version.includes("-") ? "beta" : "latest"; | |
| 124 | + | |
| 125 | +const packDistributionPackage = async (releaseDir: string): Promise<string> => { | |
| 126 | + const output = await runCommand({ | |
| 127 | + command: "npm", | |
| 128 | + args: ["pack", "./apps/executor/dist/npm", "--pack-destination", releaseDir, "--json"], | |
| 129 | + cwd: repoRoot, | |
| 130 | + }); | |
| 131 | + const [result] = JSON.parse(output.stdout) as ReadonlyArray<PackResult>; | |
| 132 | + const filename = result?.filename; | |
| 133 | + | |
| 134 | + if (!filename) { | |
| 135 | + throw new Error(`npm pack did not report an output filename. stdout:\n${output.stdout}`); | |
| 136 | + } | |
| 137 | + | |
| 138 | + return join(releaseDir, filename); | |
| 139 | +}; | |
| 140 | + | |
| 141 | +const publishDistributionPackage = async (channel: "latest" | "beta"): Promise<void> => { | |
| 142 | + const args = ["publish", "./dist/npm", "--access", "public", "--tag", channel]; | |
| 143 | + if (process.env.GITHUB_ACTIONS === "true") { | |
| 144 | + args.push("--provenance"); | |
| 145 | + } | |
| 146 | + | |
| 147 | + await runCommand({ | |
| 148 | + command: "npm", | |
| 149 | + args, | |
| 150 | + cwd: resolve(repoRoot, "apps/executor"), | |
| 151 | + }); | |
| 152 | +}; | |
| 153 | + | |
| 154 | +const createGitHubRelease = async (input: { | |
| 155 | + tag: string; | |
| 156 | + channel: "latest" | "beta"; | |
| 157 | + assetPath: string; | |
| 158 | +}): Promise<void> => { | |
| 159 | + if (!process.env.GH_TOKEN?.trim()) { | |
| 160 | + throw new Error("GH_TOKEN is required to create a GitHub release."); | |
| 161 | + } | |
| 162 | + | |
| 163 | + const args = [ | |
| 164 | + "release", | |
| 165 | + "create", | |
| 166 | + input.tag, | |
| 167 | + input.assetPath, | |
| 168 | + "--repo", | |
| 169 | + resolveGitHubRepository(), | |
| 170 | + "--title", | |
| 171 | + input.tag, | |
| 172 | + "--generate-notes", | |
| 173 | + "--verify-tag", | |
| 174 | + ]; | |
| 175 | + | |
| 176 | + if (input.channel === "beta") { | |
| 177 | + args.push("--prerelease"); | |
| 178 | + } else { | |
| 179 | + args.push("--latest"); | |
| 180 | + } | |
| 181 | + | |
| 182 | + await runCommand({ | |
| 183 | + command: "gh", | |
| 184 | + args, | |
| 185 | + cwd: repoRoot, | |
| 186 | + }); | |
| 187 | +}; | |
| 188 | + | |
| 189 | +const main = async () => { | |
| 190 | + const options = parseArgs(process.argv.slice(2)); | |
| 191 | + const metadata = await readDistributionPackageMetadata(); | |
| 192 | + const version = metadata.version; | |
| 193 | + const tag = `v${version}`; | |
| 194 | + const refTag = resolveTagFromEnvironment(); | |
| 195 | + | |
| 196 | + validateVersion(version); | |
| 197 | + | |
| 198 | + if (refTag && refTag !== tag) { | |
| 199 | + throw new Error(`GitHub tag ${refTag} does not match apps/executor/package.json version ${version}`); | |
| 200 | + } | |
| 201 | + | |
| 202 | + const channel = resolveChannel(version); | |
| 203 | + await rm(defaultReleaseDir, { recursive: true, force: true }); | |
| 204 | + await mkdir(defaultReleaseDir, { recursive: true }); | |
| 205 | + | |
| 206 | + await buildDistributionPackage(); | |
| 207 | + const archivePath = await packDistributionPackage(defaultReleaseDir); | |
| 208 | + | |
| 209 | + process.stdout.write(`Prepared executor@${version} for ${channel}\n`); | |
| 210 | + process.stdout.write(`${archivePath}\n`); | |
| 211 | + | |
| 212 | + if (options.dryRun) { | |
| 213 | + return; | |
| 214 | + } | |
| 215 | + | |
| 216 | + await publishDistributionPackage(channel); | |
| 217 | + await createGitHubRelease({ | |
| 218 | + tag, | |
| 219 | + channel, | |
| 220 | + assetPath: archivePath, | |
| 221 | + }); | |
| 222 | +}; | |
| 223 | + | |
| 224 | +await main(); | |