@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 +2 -2
- package/dist/bin/foundation-deploy.js +67 -54
- package/dist/bin/release-account.js +2 -2
- package/dist/{chunk-4aye5cee.js → chunk-m4pn6wc2.js} +1 -1
- package/dist/{chunk-v7tz8g50.js → chunk-qc5d46ky.js} +34 -23
- package/dist/src/index.js +2 -2
- package/package.json +1 -1
- package/src/deploy/github-app-create.ts +70 -61
- package/src/deploy/setup.ts +18 -5
- package/src/deploy/sh.ts +41 -19
- /package/dist/{chunk-9ddxyvq2.js → chunk-t86rw0f2.js} +0 -0
package/dist/bin/app.js
CHANGED
|
@@ -7,12 +7,12 @@ import {
|
|
|
7
7
|
FoundationNetwork,
|
|
8
8
|
FoundationPipeline,
|
|
9
9
|
NewsletterStack
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-m4pn6wc2.js";
|
|
11
11
|
import {
|
|
12
12
|
adminsFor,
|
|
13
13
|
instanceNames,
|
|
14
14
|
loadInstanceFile
|
|
15
|
-
} from "../chunk-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
1543
|
-
|
|
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 =
|
|
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}`);
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
manifestKey,
|
|
7
7
|
releaseCacheDir,
|
|
8
8
|
verifyManifest
|
|
9
|
-
} from "./chunk-
|
|
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
|
|
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
|
-
|
|
99
|
+
const child = nodeSpawn(bin, rest, {
|
|
99
100
|
cwd: opts.cwd,
|
|
100
101
|
env: { ...process.env, ...opts.env },
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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
|
|
128
|
-
await feed(proc, opts.stdin);
|
|
139
|
+
const child = start(cmd, opts, "pipe");
|
|
129
140
|
const [out, err, code] = await Promise.all([
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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-
|
|
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-
|
|
45
|
+
} from "../chunk-t86rw0f2.js";
|
|
46
46
|
export {
|
|
47
47
|
BUILD_TIMEOUT,
|
|
48
48
|
BUN_VERSION,
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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");
|
package/src/deploy/setup.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
const child = nodeSpawn(bin, rest, {
|
|
35
45
|
cwd: opts.cwd,
|
|
36
46
|
env: { ...process.env, ...opts.env },
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
66
|
-
await feed(proc, opts.stdin);
|
|
88
|
+
const child = start(cmd, opts, "pipe");
|
|
67
89
|
const [out, err, code] = await Promise.all([
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|