@deployfoundation/foundation-deploy 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/app.js CHANGED
@@ -7,12 +7,12 @@ import {
7
7
  FoundationNetwork,
8
8
  FoundationPipeline,
9
9
  NewsletterStack
10
- } from "../chunk-4aye5cee.js";
10
+ } from "../chunk-m4pn6wc2.js";
11
11
  import {
12
12
  adminsFor,
13
13
  instanceNames,
14
14
  loadInstanceFile
15
- } from "../chunk-9ddxyvq2.js";
15
+ } from "../chunk-t86rw0f2.js";
16
16
 
17
17
  // bin/app.ts
18
18
  import { execFileSync } from "node:child_process";
@@ -12,6 +12,7 @@ import {
12
12
  callerAccountId,
13
13
  cdkEnv,
14
14
  createSecretString,
15
+ hasFoundationWorkspace,
15
16
  instanceBanner,
16
17
  loadInstanceContext,
17
18
  putSecretJson,
@@ -28,7 +29,7 @@ import {
28
29
  stackExists,
29
30
  stackOutput,
30
31
  toolVersion
31
- } from "../chunk-v7tz8g50.js";
32
+ } from "../chunk-qc5d46ky.js";
32
33
  import {
33
34
  BASE_SLACK_EVENTS,
34
35
  CAPABILITY_IDS,
@@ -41,7 +42,7 @@ import {
41
42
  requiredSlackScopes,
42
43
  skillsKey,
43
44
  slackCommandPrefix
44
- } from "../chunk-9ddxyvq2.js";
45
+ } from "../chunk-t86rw0f2.js";
45
46
 
46
47
  // src/deploy/config-sync.ts
47
48
  import { existsSync, mkdirSync, mkdtempSync, readdirSync } from "node:fs";
@@ -1142,6 +1143,9 @@ async function deploy(ctx, opts) {
1142
1143
  }
1143
1144
  }
1144
1145
 
1146
+ // src/deploy/github-app-create.ts
1147
+ import { createServer } from "node:http";
1148
+
1145
1149
  // src/deploy/github-app-manifest.ts
1146
1150
  import { readFileSync as readFileSync3 } from "node:fs";
1147
1151
  import { join as join4 } from "node:path";
@@ -1193,15 +1197,14 @@ async function githubAppCreate(options) {
1193
1197
  const body = JSON.stringify({ ...manifest, redirect_url: redirectUrl });
1194
1198
  async function writeSecret(value) {
1195
1199
  const run2 = async (cmd) => {
1196
- const p = Bun.spawn(["aws", ...cmd, "--profile", profile, "--region", region], {
1197
- stdin: "pipe",
1198
- stdout: "pipe",
1199
- stderr: "pipe"
1200
- });
1201
- p.stdin.write(value);
1202
- p.stdin.end();
1203
- const code = await p.exited;
1204
- return { code, err: await new Response(p.stderr).text() };
1200
+ try {
1201
+ await runCapture(["aws", ...cmd, "--profile", profile, "--region", region], {
1202
+ stdin: value
1203
+ });
1204
+ return { code: 0, err: "" };
1205
+ } catch (error) {
1206
+ return { code: 1, err: error instanceof Error ? error.message : String(error) };
1207
+ }
1205
1208
  };
1206
1209
  let r = await run2([
1207
1210
  "secretsmanager",
@@ -1225,50 +1228,56 @@ async function githubAppCreate(options) {
1225
1228
  if (r.code !== 0)
1226
1229
  throw new Error(`aws secretsmanager failed: ${r.err.trim()}`);
1227
1230
  }
1228
- const server = Bun.serve({
1229
- port,
1230
- async fetch(req) {
1231
- const url = new URL(req.url);
1232
- if (url.pathname === "/") {
1233
- const html = `<!doctype html><title>Create ${instance.displayName} GitHub App</title><body style="font-family:system-ui;padding:2rem">
1231
+ const handler = async (req) => {
1232
+ const url = new URL(req.url);
1233
+ if (url.pathname === "/") {
1234
+ const html = `<!doctype html><title>Create ${instance.displayName} GitHub App</title><body style="font-family:system-ui;padding:2rem">
1234
1235
  <h2>Creating the <b>${instance.displayName}</b> GitHub App on <b>${org}</b>…</h2><p>If nothing happens, click the button.</p>
1235
1236
  <form id="f" method="post" action="https://github.com/organizations/${org}/settings/apps/new?state=${state}">
1236
1237
  <input type="hidden" name="manifest" id="m"><button>Create GitHub App</button></form>
1237
1238
  <script>document.getElementById('m').value=${JSON.stringify(body)};document.getElementById('f').submit();</script></body>`;
1238
- return new Response(html, { headers: { "content-type": "text/html" } });
1239
- }
1240
- if (url.pathname === "/callback") {
1241
- const code = url.searchParams.get("code");
1242
- if (url.searchParams.get("state") !== state || code === null)
1243
- return new Response("bad state/code", { status: 400 });
1244
- const res = await fetch(`https://api.github.com/app-manifests/${encodeURIComponent(code)}/conversions`, {
1245
- method: "POST",
1246
- headers: {
1247
- Accept: "application/vnd.github+json",
1248
- "X-GitHub-Api-Version": "2022-11-28"
1249
- }
1250
- });
1251
- if (!res.ok)
1252
- return new Response(`conversion failed: HTTP ${res.status}`, { status: 502 });
1253
- const app = await res.json();
1254
- await writeSecret(JSON.stringify({ app_id: String(app.id), installation_id: "", private_key: app.pem }));
1255
- const installUrl = `https://github.com/apps/${app.slug}/installations/new`;
1256
- console.log(JSON.stringify({
1257
- app_id: app.id,
1258
- slug: app.slug,
1259
- html_url: app.html_url,
1260
- install_url: installUrl,
1261
- secret: secretName
1262
- }));
1263
- setTimeout(() => {
1264
- server.stop(true);
1265
- process.exit(0);
1266
- }, 500);
1267
- return new Response(`<!doctype html><body style="font-family:system-ui;padding:2rem"><h2>✅ App "${app.slug}" created (id ${app.id}); private key stored in Secrets Manager ${secretName}.</h2><p>Next: <a href="${installUrl}">install it on your repos</a>.</p></body>`, { headers: { "content-type": "text/html" } });
1268
- }
1269
- return new Response("not found", { status: 404 });
1239
+ return new Response(html, { headers: { "content-type": "text/html" } });
1240
+ }
1241
+ if (url.pathname === "/callback") {
1242
+ const code = url.searchParams.get("code");
1243
+ if (url.searchParams.get("state") !== state || code === null)
1244
+ return new Response("bad state/code", { status: 400 });
1245
+ const res = await fetch(`https://api.github.com/app-manifests/${encodeURIComponent(code)}/conversions`, {
1246
+ method: "POST",
1247
+ headers: {
1248
+ Accept: "application/vnd.github+json",
1249
+ "X-GitHub-Api-Version": "2022-11-28"
1250
+ }
1251
+ });
1252
+ if (!res.ok)
1253
+ return new Response(`conversion failed: HTTP ${res.status}`, { status: 502 });
1254
+ const app = await res.json();
1255
+ await writeSecret(JSON.stringify({ app_id: String(app.id), installation_id: "", private_key: app.pem }));
1256
+ const installUrl = `https://github.com/apps/${app.slug}/installations/new`;
1257
+ console.log(JSON.stringify({
1258
+ app_id: app.id,
1259
+ slug: app.slug,
1260
+ html_url: app.html_url,
1261
+ install_url: installUrl,
1262
+ secret: secretName
1263
+ }));
1264
+ setTimeout(() => {
1265
+ server.close();
1266
+ process.exit(0);
1267
+ }, 500);
1268
+ return new Response(`<!doctype html><body style="font-family:system-ui;padding:2rem"><h2>✅ App "${app.slug}" created (id ${app.id}); private key stored in Secrets Manager ${secretName}.</h2><p>Next: <a href="${installUrl}">install it on your repos</a>.</p></body>`, { headers: { "content-type": "text/html" } });
1270
1269
  }
1270
+ return new Response("not found", { status: 404 });
1271
+ };
1272
+ const server = createServer(async (req, res) => {
1273
+ const request = new Request(`http://localhost:${port}${req.url ?? "/"}`, {
1274
+ method: req.method ?? "GET"
1275
+ });
1276
+ const response = await handler(request);
1277
+ res.writeHead(response.status, Object.fromEntries(response.headers));
1278
+ res.end(await response.text());
1271
1279
  });
1280
+ server.listen(port);
1272
1281
  console.log(`Open http://localhost:${port}/ to create the app (waiting up to 10 minutes)…`);
1273
1282
  setTimeout(() => {
1274
1283
  console.error("timed out");
@@ -1320,7 +1329,7 @@ async function postDeploy(ctx, opts = {}) {
1320
1329
  }
1321
1330
 
1322
1331
  // src/deploy/setup.ts
1323
- import { existsSync as existsSync3 } from "node:fs";
1332
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
1324
1333
  import { resolve as resolve3 } from "node:path";
1325
1334
 
1326
1335
  // src/deploy/slack-manifest.ts
@@ -1524,6 +1533,8 @@ async function output(ctx, stack, key) {
1524
1533
  }
1525
1534
  }
1526
1535
  async function installDependencies(ctx) {
1536
+ if (!hasFoundationWorkspace())
1537
+ return;
1527
1538
  await run(["bun", "install", "--frozen-lockfile"], {
1528
1539
  cwd: FOUNDATION_ROOT,
1529
1540
  dryRun: ctx.dryRun
@@ -1536,12 +1547,14 @@ async function preflight(ctx) {
1536
1547
  if (account !== expected)
1537
1548
  throw new Error(`profile ${ctx.profile} is account ${account}, expected ${expected} for instance ${ctx.instance.name}`);
1538
1549
  console.log(` account ${account} via profile ${ctx.profile} (${ctx.region})`);
1539
- if (ctx.dryRun === true) {
1550
+ if (!hasFoundationWorkspace()) {} else if (ctx.dryRun === true) {
1540
1551
  await run(["docker", "info"], { dryRun: true });
1541
1552
  } else {
1542
- const docker = Bun.spawn(["docker", "info"], { stdout: "ignore", stderr: "ignore" });
1543
- if (await docker.exited !== 0)
1553
+ try {
1554
+ await runCapture(["docker", "info"]);
1555
+ } catch {
1544
1556
  throw new Error("docker is not running");
1557
+ }
1545
1558
  console.log(" docker ✓");
1546
1559
  }
1547
1560
  for (const id of [
@@ -1571,7 +1584,7 @@ async function writeRuntimeSecret(ctx) {
1571
1584
  async function seedCodex(ctx, codexFile) {
1572
1585
  if (!existsSync3(codexFile))
1573
1586
  throw new Error(`${codexFile} not found — sign in locally first, or pass --codex-file`);
1574
- const document = await Bun.file(codexFile).text();
1587
+ const document = readFileSync5(codexFile, "utf8");
1575
1588
  JSON.parse(document);
1576
1589
  await putSecretString(ctx, ctx.names.secretCodex, document);
1577
1590
  console.log(` ${ctx.names.secretCodex} seeded from ${codexFile}`);
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import {
3
3
  DEFAULT_RELEASE_BUCKET
4
- } from "../chunk-v7tz8g50.js";
5
- import"../chunk-9ddxyvq2.js";
4
+ } from "../chunk-qc5d46ky.js";
5
+ import"../chunk-t86rw0f2.js";
6
6
 
7
7
  // bin/release-account.ts
8
8
  import * as cdk2 from "aws-cdk-lib";
@@ -14,7 +14,7 @@ import {
14
14
  releaseImageRepositoryArn,
15
15
  requiresAuthenticatedQueue,
16
16
  slackCommandPrefixes
17
- } from "./chunk-9ddxyvq2.js";
17
+ } from "./chunk-t86rw0f2.js";
18
18
 
19
19
  // src/stacks/agent-stack.ts
20
20
  import * as cdk from "aws-cdk-lib";
@@ -6,7 +6,7 @@ import {
6
6
  manifestKey,
7
7
  releaseCacheDir,
8
8
  verifyManifest
9
- } from "./chunk-9ddxyvq2.js";
9
+ } from "./chunk-t86rw0f2.js";
10
10
 
11
11
  // src/deploy/release.ts
12
12
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
@@ -83,6 +83,7 @@ function instanceBanner(context) {
83
83
  }
84
84
 
85
85
  // src/deploy/sh.ts
86
+ import { spawn as nodeSpawn } from "node:child_process";
86
87
  function formatCommand(cmd, env) {
87
88
  const prefix = Object.entries(env ?? {}).map(([k, v]) => `${k}=${quote(v)}`).join(" ");
88
89
  const body = cmd.map(quote).join(" ");
@@ -91,45 +92,55 @@ function formatCommand(cmd, env) {
91
92
  function quote(value) {
92
93
  return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
93
94
  }
94
- function spawn(cmd, opts, stdout) {
95
+ function start(cmd, opts, stdout) {
95
96
  const [bin, ...rest] = cmd;
96
97
  if (bin === undefined)
97
98
  throw new Error("run: empty command");
98
- return Bun.spawn([bin, ...rest], {
99
+ const child = nodeSpawn(bin, rest, {
99
100
  cwd: opts.cwd,
100
101
  env: { ...process.env, ...opts.env },
101
- stdin: opts.stdin === undefined ? "ignore" : "pipe",
102
- stdout,
103
- stderr: stdout === "inherit" ? "inherit" : "pipe"
102
+ stdio: [
103
+ opts.stdin === undefined ? "ignore" : "pipe",
104
+ stdout,
105
+ stdout === "inherit" ? "inherit" : "pipe"
106
+ ]
104
107
  });
108
+ if (opts.stdin !== undefined && child.stdin !== null) {
109
+ child.stdin.end(opts.stdin);
110
+ }
111
+ return child;
105
112
  }
106
- async function feed(proc, stdin) {
107
- if (stdin === undefined)
108
- return;
109
- const sink = proc.stdin;
110
- if (sink === null)
111
- return;
112
- sink.write(stdin);
113
- sink.end();
113
+ function exited(child) {
114
+ return new Promise((resolve, reject) => {
115
+ child.once("error", reject);
116
+ child.once("close", (code, signal) => resolve(code ?? (signal === null ? 1 : 128)));
117
+ });
118
+ }
119
+ function collect(stream) {
120
+ if (stream === null)
121
+ return Promise.resolve("");
122
+ return new Promise((resolve, reject) => {
123
+ const chunks = [];
124
+ stream.on("data", (c) => chunks.push(c));
125
+ stream.once("error", reject);
126
+ stream.once("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
127
+ });
114
128
  }
115
129
  async function run(cmd, opts = {}) {
116
130
  if (opts.dryRun === true) {
117
131
  console.log(` $ ${formatCommand(cmd, opts.env)}`);
118
132
  return;
119
133
  }
120
- const proc = spawn(cmd, opts, "inherit");
121
- await feed(proc, opts.stdin);
122
- const code = await proc.exited;
134
+ const code = await exited(start(cmd, opts, "inherit"));
123
135
  if (code !== 0)
124
136
  throw new Error(`${formatCommand(cmd)} exited ${code}`);
125
137
  }
126
138
  async function runCapture(cmd, opts = {}) {
127
- const proc = spawn(cmd, opts, "pipe");
128
- await feed(proc, opts.stdin);
139
+ const child = start(cmd, opts, "pipe");
129
140
  const [out, err, code] = await Promise.all([
130
- new Response(proc.stdout).text(),
131
- new Response(proc.stderr).text(),
132
- proc.exited
141
+ collect(child.stdout),
142
+ collect(child.stderr),
143
+ exited(child)
133
144
  ]);
134
145
  if (code !== 0)
135
146
  throw new Error(`${formatCommand(cmd)} exited ${code}: ${err.trim()}`);
@@ -425,4 +436,4 @@ function releaseContext(release) {
425
436
  ];
426
437
  }
427
438
 
428
- export { resolveInstanceFilePath, loadInstanceContext, instanceBanner, run, runCapture, awsContext, argv, cdkEnv, aws, awsMutate, stackOutput, stackExists, secretExists, readSecretString, readSecretJson, putSecretString, createSecretString, putSecretJson, callerAccountId, INFRA_ROOT, FOUNDATION_ROOT, AGENT_DOCKERFILE, PACKAGE_ASSETS, DEFAULT_RELEASE_BUCKET, toolVersion, releaseRequest, resolveRelease, releaseContext };
439
+ export { resolveInstanceFilePath, loadInstanceContext, instanceBanner, run, runCapture, awsContext, argv, cdkEnv, aws, awsMutate, stackOutput, stackExists, secretExists, readSecretString, readSecretJson, putSecretString, createSecretString, putSecretJson, callerAccountId, INFRA_ROOT, FOUNDATION_ROOT, AGENT_DOCKERFILE, PACKAGE_ASSETS, DEFAULT_RELEASE_BUCKET, toolVersion, hasFoundationWorkspace, releaseRequest, resolveRelease, releaseContext };
package/dist/src/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  FoundationPipeline,
10
10
  NewsletterStack,
11
11
  deployStatements
12
- } from "../chunk-4aye5cee.js";
12
+ } from "../chunk-m4pn6wc2.js";
13
13
  import {
14
14
  BUN_VERSION,
15
15
  LAMBDA_ENTRY_POINTS,
@@ -42,7 +42,7 @@ import {
42
42
  skillsKey,
43
43
  skipBundle,
44
44
  verifyManifest
45
- } from "../chunk-9ddxyvq2.js";
45
+ } from "../chunk-t86rw0f2.js";
46
46
  export {
47
47
  BUILD_TIMEOUT,
48
48
  BUN_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deployfoundation/foundation-deploy",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Deploys one Foundation instance from a published, signed release: the CDK app and the deploy tool.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -20,9 +20,11 @@
20
20
  *
21
21
  * Record the installation_id in the same secret after installing the App.
22
22
  */
23
+ import { createServer } from "node:http";
23
24
  import { instanceNames } from "@deployfoundation/foundation-core/instance";
24
25
  import { loadAppManifest } from "./github-app-manifest.ts";
25
26
  import type { InstanceContext } from "./instance.ts";
27
+ import { runCapture } from "./sh.ts";
26
28
 
27
29
  export interface GithubAppCreateOptions {
28
30
  paths: InstanceContext;
@@ -56,15 +58,14 @@ export async function githubAppCreate(options: GithubAppCreateOptions): Promise<
56
58
 
57
59
  async function writeSecret(value: string): Promise<void> {
58
60
  const run = async (cmd: string[]) => {
59
- const p = Bun.spawn(["aws", ...cmd, "--profile", profile, "--region", region], {
60
- stdin: "pipe",
61
- stdout: "pipe",
62
- stderr: "pipe",
63
- });
64
- p.stdin.write(value);
65
- p.stdin.end();
66
- const code = await p.exited;
67
- return { code, err: await new Response(p.stderr).text() };
61
+ try {
62
+ await runCapture(["aws", ...cmd, "--profile", profile, "--region", region], {
63
+ stdin: value,
64
+ });
65
+ return { code: 0, err: "" };
66
+ } catch (error) {
67
+ return { code: 1, err: error instanceof Error ? error.message : String(error) };
68
+ }
68
69
  };
69
70
  let r = await run([
70
71
  "secretsmanager",
@@ -88,64 +89,72 @@ export async function githubAppCreate(options: GithubAppCreateOptions): Promise<
88
89
  if (r.code !== 0) throw new Error(`aws secretsmanager failed: ${r.err.trim()}`);
89
90
  }
90
91
 
91
- const server = Bun.serve({
92
- port,
93
- async fetch(req) {
94
- const url = new URL(req.url);
95
- if (url.pathname === "/") {
96
- const html = `<!doctype html><title>Create ${instance.displayName} GitHub App</title><body style="font-family:system-ui;padding:2rem">
92
+ const handler = async (req: Request): Promise<Response> => {
93
+ const url = new URL(req.url);
94
+ if (url.pathname === "/") {
95
+ const html = `<!doctype html><title>Create ${instance.displayName} GitHub App</title><body style="font-family:system-ui;padding:2rem">
97
96
  <h2>Creating the <b>${instance.displayName}</b> GitHub App on <b>${org}</b>…</h2><p>If nothing happens, click the button.</p>
98
97
  <form id="f" method="post" action="https://github.com/organizations/${org}/settings/apps/new?state=${state}">
99
98
  <input type="hidden" name="manifest" id="m"><button>Create GitHub App</button></form>
100
99
  <script>document.getElementById('m').value=${JSON.stringify(body)};document.getElementById('f').submit();</script></body>`;
101
- return new Response(html, { headers: { "content-type": "text/html" } });
102
- }
103
- if (url.pathname === "/callback") {
104
- const code = url.searchParams.get("code");
105
- if (url.searchParams.get("state") !== state || code === null)
106
- return new Response("bad state/code", { status: 400 });
107
- const res = await fetch(
108
- `https://api.github.com/app-manifests/${encodeURIComponent(code)}/conversions`,
109
- {
110
- method: "POST",
111
- headers: {
112
- Accept: "application/vnd.github+json",
113
- "X-GitHub-Api-Version": "2022-11-28",
114
- },
100
+ return new Response(html, { headers: { "content-type": "text/html" } });
101
+ }
102
+ if (url.pathname === "/callback") {
103
+ const code = url.searchParams.get("code");
104
+ if (url.searchParams.get("state") !== state || code === null)
105
+ return new Response("bad state/code", { status: 400 });
106
+ const res = await fetch(
107
+ `https://api.github.com/app-manifests/${encodeURIComponent(code)}/conversions`,
108
+ {
109
+ method: "POST",
110
+ headers: {
111
+ Accept: "application/vnd.github+json",
112
+ "X-GitHub-Api-Version": "2022-11-28",
115
113
  },
116
- );
117
- if (!res.ok) return new Response(`conversion failed: HTTP ${res.status}`, { status: 502 });
118
- const app = (await res.json()) as {
119
- id: number;
120
- slug: string;
121
- pem: string;
122
- html_url: string;
123
- };
124
- await writeSecret(
125
- JSON.stringify({ app_id: String(app.id), installation_id: "", private_key: app.pem }),
126
- );
127
- const installUrl = `https://github.com/apps/${app.slug}/installations/new`;
128
- console.log(
129
- JSON.stringify({
130
- app_id: app.id,
131
- slug: app.slug,
132
- html_url: app.html_url,
133
- install_url: installUrl,
134
- secret: secretName,
135
- }),
136
- );
137
- setTimeout(() => {
138
- server.stop(true);
139
- process.exit(0);
140
- }, 500);
141
- return new Response(
142
- `<!doctype html><body style="font-family:system-ui;padding:2rem"><h2>✅ App "${app.slug}" created (id ${app.id}); private key stored in Secrets Manager ${secretName}.</h2><p>Next: <a href="${installUrl}">install it on your repos</a>.</p></body>`,
143
- { headers: { "content-type": "text/html" } },
144
- );
145
- }
146
- return new Response("not found", { status: 404 });
147
- },
114
+ },
115
+ );
116
+ if (!res.ok) return new Response(`conversion failed: HTTP ${res.status}`, { status: 502 });
117
+ const app = (await res.json()) as {
118
+ id: number;
119
+ slug: string;
120
+ pem: string;
121
+ html_url: string;
122
+ };
123
+ await writeSecret(
124
+ JSON.stringify({ app_id: String(app.id), installation_id: "", private_key: app.pem }),
125
+ );
126
+ const installUrl = `https://github.com/apps/${app.slug}/installations/new`;
127
+ console.log(
128
+ JSON.stringify({
129
+ app_id: app.id,
130
+ slug: app.slug,
131
+ html_url: app.html_url,
132
+ install_url: installUrl,
133
+ secret: secretName,
134
+ }),
135
+ );
136
+ setTimeout(() => {
137
+ server.close();
138
+ process.exit(0);
139
+ }, 500);
140
+ return new Response(
141
+ `<!doctype html><body style="font-family:system-ui;padding:2rem"><h2>✅ App "${app.slug}" created (id ${app.id}); private key stored in Secrets Manager ${secretName}.</h2><p>Next: <a href="${installUrl}">install it on your repos</a>.</p></body>`,
142
+ { headers: { "content-type": "text/html" } },
143
+ );
144
+ }
145
+ return new Response("not found", { status: 404 });
146
+ };
147
+ // Node's `http`, not `Bun.serve`: this command ships in the npm package and
148
+ // runs under Node. The handler keeps the fetch-style Request/Response shape.
149
+ const server = createServer(async (req, res) => {
150
+ const request = new Request(`http://localhost:${port}${req.url ?? "/"}`, {
151
+ method: req.method ?? "GET",
152
+ });
153
+ const response = await handler(request);
154
+ res.writeHead(response.status, Object.fromEntries(response.headers));
155
+ res.end(await response.text());
148
156
  });
157
+ server.listen(port);
149
158
  console.log(`Open http://localhost:${port}/ to create the app (waiting up to 10 minutes)…`);
150
159
  setTimeout(() => {
151
160
  console.error("timed out");
@@ -12,7 +12,7 @@
12
12
  * `--seed-codex` copies a local Codex credential into the instance's Codex
13
13
  * secret so the teammate is authenticated on day one without a Slack `login`.
14
14
  */
15
- import { existsSync } from "node:fs";
15
+ import { existsSync, readFileSync } from "node:fs";
16
16
  import { resolve } from "node:path";
17
17
  import {
18
18
  type AwsContext,
@@ -44,6 +44,7 @@ import {
44
44
  } from "./endpoint.ts";
45
45
  import { currentImageTag } from "./image.ts";
46
46
  import { FOUNDATION_ROOT, INFRA_ROOT } from "./paths.ts";
47
+ import { hasFoundationWorkspace } from "./release.ts";
47
48
  import { run, runCapture } from "./sh.ts";
48
49
  import { slackManifestFor } from "./slack-manifest.ts";
49
50
  import { stageCustomization } from "./stage-customization.ts";
@@ -96,6 +97,11 @@ async function output(ctx: AwsContext, stack: string, key: string): Promise<stri
96
97
  * workspace of this repo rather than a separate npm project.
97
98
  */
98
99
  async function installDependencies(ctx: AwsContext): Promise<void> {
100
+ // Only a Foundation checkout has a workspace to install. A copy of this tool
101
+ // installed from npm carries its dependencies already, and has neither Bun
102
+ // nor a lockfile beside it; running `bun install` there failed setup before
103
+ // it did anything, for every instance that is not Foundation itself.
104
+ if (!hasFoundationWorkspace()) return;
99
105
  await run(["bun", "install", "--frozen-lockfile"], {
100
106
  cwd: FOUNDATION_ROOT,
101
107
  dryRun: ctx.dryRun,
@@ -112,11 +118,18 @@ async function preflight(ctx: AwsContext): Promise<void> {
112
118
  );
113
119
  console.log(` account ${account} via profile ${ctx.profile} (${ctx.region})`);
114
120
 
115
- if (ctx.dryRun === true) {
121
+ // Docker builds the agent image locally, which only a checkout does. A
122
+ // release deploy pulls the signed image the manifest names instead.
123
+ if (!hasFoundationWorkspace()) {
124
+ // nothing to build
125
+ } else if (ctx.dryRun === true) {
116
126
  await run(["docker", "info"], { dryRun: true });
117
127
  } else {
118
- const docker = Bun.spawn(["docker", "info"], { stdout: "ignore", stderr: "ignore" });
119
- if ((await docker.exited) !== 0) throw new Error("docker is not running");
128
+ try {
129
+ await runCapture(["docker", "info"]);
130
+ } catch {
131
+ throw new Error("docker is not running");
132
+ }
120
133
  console.log(" docker ✓");
121
134
  }
122
135
 
@@ -151,7 +164,7 @@ async function writeRuntimeSecret(ctx: AwsContext): Promise<void> {
151
164
  async function seedCodex(ctx: AwsContext, codexFile: string): Promise<void> {
152
165
  if (!existsSync(codexFile))
153
166
  throw new Error(`${codexFile} not found — sign in locally first, or pass --codex-file`);
154
- const document = await Bun.file(codexFile).text();
167
+ const document = readFileSync(codexFile, "utf8");
155
168
  JSON.parse(document); // fail here rather than storing a broken store document
156
169
  await putSecretString(ctx, ctx.names.secretCodex, document);
157
170
  console.log(` ${ctx.names.secretCodex} seeded from ${codexFile}`);
package/src/deploy/sh.ts CHANGED
@@ -6,6 +6,9 @@
6
6
  * stdout for the caller and only surfaces stderr when the command fails.
7
7
  */
8
8
 
9
+ import { type ChildProcess, spawn as nodeSpawn } from "node:child_process";
10
+ import type { Readable } from "node:stream";
11
+
9
12
  export interface RunOptions {
10
13
  cwd?: string;
11
14
  env?: Record<string, string>;
@@ -28,24 +31,46 @@ function quote(value: string): string {
28
31
  return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
29
32
  }
30
33
 
31
- function spawn(cmd: string[], opts: RunOptions, stdout: "inherit" | "pipe") {
34
+ /**
35
+ * Node's `child_process`, not `Bun.spawn`. This file ships in
36
+ * `@deployfoundation/foundation-deploy`, which an instance's buildspec runs
37
+ * with `npx` under Node; a Bun-only API here made every command die with
38
+ * "Bun is not defined" before doing anything. `node:child_process` works under
39
+ * both runtimes, so the Bun-run tests and the Node-run tool share one path.
40
+ */
41
+ function start(cmd: string[], opts: RunOptions, stdout: "inherit" | "pipe"): ChildProcess {
32
42
  const [bin, ...rest] = cmd;
33
43
  if (bin === undefined) throw new Error("run: empty command");
34
- return Bun.spawn([bin, ...rest], {
44
+ const child = nodeSpawn(bin, rest, {
35
45
  cwd: opts.cwd,
36
46
  env: { ...process.env, ...opts.env },
37
- stdin: opts.stdin === undefined ? "ignore" : "pipe",
38
- stdout,
39
- stderr: stdout === "inherit" ? "inherit" : "pipe",
47
+ stdio: [
48
+ opts.stdin === undefined ? "ignore" : "pipe",
49
+ stdout,
50
+ stdout === "inherit" ? "inherit" : "pipe",
51
+ ],
52
+ });
53
+ if (opts.stdin !== undefined && child.stdin !== null) {
54
+ child.stdin.end(opts.stdin);
55
+ }
56
+ return child;
57
+ }
58
+
59
+ function exited(child: ChildProcess): Promise<number> {
60
+ return new Promise((resolve, reject) => {
61
+ child.once("error", reject);
62
+ child.once("close", (code, signal) => resolve(code ?? (signal === null ? 1 : 128)));
40
63
  });
41
64
  }
42
65
 
43
- async function feed(proc: { stdin: unknown }, stdin: string | undefined): Promise<void> {
44
- if (stdin === undefined) return;
45
- const sink = proc.stdin as { write: (s: string) => void; end: () => void } | null;
46
- if (sink === null) return;
47
- sink.write(stdin);
48
- sink.end();
66
+ function collect(stream: Readable | null): Promise<string> {
67
+ if (stream === null) return Promise.resolve("");
68
+ return new Promise((resolve, reject) => {
69
+ const chunks: Buffer[] = [];
70
+ stream.on("data", (c: Buffer) => chunks.push(c));
71
+ stream.once("error", reject);
72
+ stream.once("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
73
+ });
49
74
  }
50
75
 
51
76
  /** Run a command with inherited stdio. Throws if it exits non-zero. */
@@ -54,20 +79,17 @@ export async function run(cmd: string[], opts: RunOptions = {}): Promise<void> {
54
79
  console.log(` $ ${formatCommand(cmd, opts.env)}`);
55
80
  return;
56
81
  }
57
- const proc = spawn(cmd, opts, "inherit");
58
- await feed(proc, opts.stdin);
59
- const code = await proc.exited;
82
+ const code = await exited(start(cmd, opts, "inherit"));
60
83
  if (code !== 0) throw new Error(`${formatCommand(cmd)} exited ${code}`);
61
84
  }
62
85
 
63
86
  /** Run a command and return its trimmed stdout. Throws with stderr on failure. */
64
87
  export async function runCapture(cmd: string[], opts: RunOptions = {}): Promise<string> {
65
- const proc = spawn(cmd, opts, "pipe");
66
- await feed(proc, opts.stdin);
88
+ const child = start(cmd, opts, "pipe");
67
89
  const [out, err, code] = await Promise.all([
68
- new Response(proc.stdout).text(),
69
- new Response(proc.stderr).text(),
70
- proc.exited,
90
+ collect(child.stdout),
91
+ collect(child.stderr),
92
+ exited(child),
71
93
  ]);
72
94
  if (code !== 0) throw new Error(`${formatCommand(cmd)} exited ${code}: ${err.trim()}`);
73
95
  return out.trim();
File without changes