diff --git a/README.md b/README.md index 4f40ff4..0a069a1 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,55 @@ -# test-typescript-deploys +# example-typescript-packaging -A minimal TypeScript example that deploys a Python app to -[Tower](https://tower.dev) end-to-end from Node. It uses -[`tower-package-wasm`](https://www.npmjs.com/package/tower-package-wasm) to -build a deterministic tar.gz bundle in memory, and a generated OpenAPI client -(from the Tower API spec) to create the app and upload the bundle. -Authentication is via a Tower API key loaded from `.env`. +A minimal TypeScript example that packages a Python app, deploys it to +[Tower](https://tower.dev), runs it, and streams the output back to your +terminal. Uses +[`tower-package-wasm`](https://www.npmjs.com/package/tower-package-wasm) +for packaging and a generated OpenAPI client +([`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/) + +[`openapi-typescript`](https://github.com/openapi-ts/openapi-typescript)) +for the API calls. ## Generating the client -The Tower OpenAPI spec is checked in at `openapi.yaml`. Types for the client -are generated with -[`openapi-typescript`](https://github.com/openapi-ts/openapi-typescript) and -consumed at runtime by -[`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/). After `npm install`, -run: +The Tower OpenAPI spec is checked in at `openapi.yaml`. After `npm install`: ```sh npm run generate ``` -This writes `src/generated/api.ts`. Re-run whenever `openapi.yaml` changes. To -refresh the spec itself, pull it from the live service: +This writes `src/generated/api.ts`. Re-run whenever `openapi.yaml` changes. +To refresh the spec itself, pull it from the live service: ```sh curl -sL https://api.tower.dev/v1/openapi.yaml -o openapi.yaml ``` -## Running the app +## Running the example Copy `.env.example` to `.env` and set `TOWER_API_KEY` (optionally override `TOWER_APP_NAME`). Then: ```sh npm install -npm run deploy +npm run deploy-and-run ``` -The script reads `src/sample-app/` (a trivial `main.py` plus a `Towerfile`), -hands the file bytes to `buildPackage` from `tower-package-wasm`, ensures the -named app exists on your account, and POSTs the resulting tarball to -`/apps/{name}/deploy` with an `X-Tower-Checksum-SHA256` header. Deploy runs -through `node --experimental-wasm-modules` because the published -`tower-package-wasm` is the bundler build and imports its `.wasm` file as an ES -module. +Expected output: + +``` +Created app "test-typescript-deploys". +Built package: 360 bytes, 1 app file(s). +Deployed app "test-typescript-deploys" version v1. +Started run #1. + hello from test-typescript-deploys +Run #1 complete. +``` + +`src/index.ts` reads as six numbered steps: load config, ensure the app +exists, [read the files, build the bundle with `tower-package-wasm`, and +upload them to the deploy endpoint](src/index.ts#L49-L97), then run the +app and stream its output. + +`deploy-and-run` invokes `node --experimental-wasm-modules` because the +published `tower-package-wasm` is the bundler build and imports `.wasm` +as an ES module. diff --git a/package.json b/package.json index 28eb8bb..af8979c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "generate": "openapi-typescript ./openapi.yaml -o ./src/generated/api.ts", - "deploy": "node --experimental-wasm-modules --no-warnings --import tsx ./src/index.ts", + "deploy-and-run": "node --experimental-wasm-modules --no-warnings --import tsx ./src/index.ts", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/src/index.ts b/src/index.ts index 1c85e8e..de8f224 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,7 +29,24 @@ function apiError(label: string, res: { response: Response; error?: unknown }): return new Error(`${label}: ${res.response.status} ${JSON.stringify(res.error)}`); } -// 2. Read the sample app off disk. +// 2. Make sure the app exists on the server. Describe it; create on 404. + +const describe = await client.GET("/apps/{name}", { + params: { path: { name }, query: { runs: 0, timezone: "UTC" } }, +}); +if (describe.response.ok) { + console.log(`App "${name}" already exists.`); +} else if (describe.response.status === 404) { + const created = await client.POST("/apps", { + body: { name, is_externally_accessible: false }, + }); + if (!created.response.ok) throw apiError("Create app failed", created); + console.log(`Created app "${name}".`); +} else { + throw apiError("Describe app failed", describe); +} + +// 3. Read the sample app off disk. const appDir = join(fileURLToPath(new URL(".", import.meta.url)), "sample-app"); const appFiles: PackageEntry[] = []; @@ -54,7 +71,7 @@ for (const entry of await readdir(appDir, { recursive: true, withFileTypes: true } if (!towerfileBytes) throw new Error(`No Towerfile found in ${appDir}`); -// 3. Build the deterministic tar.gz bundle. +// 4. Build the deterministic tar.gz bundle. // === tower-package-wasm call site === // Produces the tar.gz byte stream that gets POSTed to /apps/{name}/deploy. @@ -63,23 +80,6 @@ if (!towerfileBytes) throw new Error(`No Towerfile found in ${appDir}`); const pkg = buildPackage({ appFiles, moduleFiles: [], towerfileBytes }); console.log(`Built package: ${pkg.byteLength} bytes, ${appFiles.length} app file(s).`); -// 4. Make sure the app exists on the server. Describe it; create on 404. - -const describe = await client.GET("/apps/{name}", { - params: { path: { name }, query: { runs: 0, timezone: "UTC" } }, -}); -if (describe.response.ok) { - console.log(`App "${name}" already exists.`); -} else if (describe.response.status === 404) { - const created = await client.POST("/apps", { - body: { name, is_externally_accessible: false }, - }); - if (!created.response.ok) throw apiError("Create app failed", created); - console.log(`Created app "${name}".`); -} else { - throw apiError("Describe app failed", describe); -} - // 5. Upload the package. const checksum = createHash("sha256").update(pkg).digest("hex");