thinkwork-cli 0.12.2 → 0.12.3
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/cli.js +981 -29
- package/dist/commands/enterprise/templates/deploy-repo/.github/workflows/deploy.yml +232 -0
- package/dist/commands/enterprise/templates/deploy-repo/README.md +31 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/branding/README.md +7 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/deployment.json +6 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/evals/README.md +10 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/seeds/README.md +7 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/skills/README.md +7 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/workspace-defaults/README.md +7 -0
- package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +606 -0
- package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +99 -0
- package/dist/commands/enterprise/templates/deploy-repo/terraform/backend-dev.hcl +6 -0
- package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +101 -0
- package/dist/commands/enterprise/templates/deploy-repo/terraform/stages/dev.tfvars +9 -0
- package/dist/commands/enterprise/templates/deploy-repo/terraform/stages/prod.tfvars +9 -0
- package/dist/commands/enterprise/templates/deploy-repo/thinkwork.lock +17 -0
- package/dist/terraform/examples/greenfield/main.tf +26 -0
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -0
- package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +7 -7
- package/dist/terraform/modules/app/lambda-api/handlers.tf +78 -68
- package/dist/terraform/modules/app/lambda-api/outputs.tf +9 -4
- package/dist/terraform/modules/app/lambda-api/remote-artifacts.tf +36 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +7 -0
- package/dist/terraform/modules/app/lambda-api/workspace-events.tf +1 -1
- package/dist/terraform/modules/thinkwork/main.tf +3 -2
- package/dist/terraform/modules/thinkwork/outputs.tf +5 -0
- package/dist/terraform/modules/thinkwork/variables.tf +6 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -201,7 +201,7 @@ async function ensureWorkspace(cwd, stage) {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
function runTerraformRaw(cwd, args) {
|
|
204
|
-
return new Promise((
|
|
204
|
+
return new Promise((resolve6, reject) => {
|
|
205
205
|
const proc = spawn("terraform", args, {
|
|
206
206
|
cwd,
|
|
207
207
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -211,13 +211,13 @@ function runTerraformRaw(cwd, args) {
|
|
|
211
211
|
proc.stdout.on("data", (d) => stdout += d);
|
|
212
212
|
proc.stderr.on("data", (d) => stderr += d);
|
|
213
213
|
proc.on("close", (code) => {
|
|
214
|
-
if (code === 0)
|
|
214
|
+
if (code === 0) resolve6(stdout);
|
|
215
215
|
else reject(new Error(`terraform ${args.join(" ")} failed (exit ${code}): ${stderr}`));
|
|
216
216
|
});
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
219
|
function runTerraform(cwd, args) {
|
|
220
|
-
return new Promise((
|
|
220
|
+
return new Promise((resolve6) => {
|
|
221
221
|
console.log(`
|
|
222
222
|
\u2192 terraform ${args.join(" ")}
|
|
223
223
|
`);
|
|
@@ -225,7 +225,7 @@ function runTerraform(cwd, args) {
|
|
|
225
225
|
cwd,
|
|
226
226
|
stdio: "inherit"
|
|
227
227
|
});
|
|
228
|
-
proc.on("close", (code) =>
|
|
228
|
+
proc.on("close", (code) => resolve6(code ?? 1));
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
231
|
async function ensureInit(cwd) {
|
|
@@ -469,10 +469,10 @@ async function confirm(message) {
|
|
|
469
469
|
input: process.stdin,
|
|
470
470
|
output: process.stdout
|
|
471
471
|
});
|
|
472
|
-
return new Promise((
|
|
472
|
+
return new Promise((resolve6) => {
|
|
473
473
|
rl.question(`${message} [y/N] `, (answer) => {
|
|
474
474
|
rl.close();
|
|
475
|
-
|
|
475
|
+
resolve6(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
476
476
|
});
|
|
477
477
|
});
|
|
478
478
|
}
|
|
@@ -553,7 +553,7 @@ async function runPostDeployProbe(stage) {
|
|
|
553
553
|
);
|
|
554
554
|
return;
|
|
555
555
|
}
|
|
556
|
-
await new Promise((
|
|
556
|
+
await new Promise((resolve6) => {
|
|
557
557
|
const proc = spawn2("bash", [scriptPath, "--stage", stage], {
|
|
558
558
|
stdio: "inherit",
|
|
559
559
|
env: process.env
|
|
@@ -564,11 +564,11 @@ async function runPostDeployProbe(stage) {
|
|
|
564
564
|
`post-deploy probe exited ${code} \u2014 deploy not rolled back`
|
|
565
565
|
);
|
|
566
566
|
}
|
|
567
|
-
|
|
567
|
+
resolve6();
|
|
568
568
|
});
|
|
569
569
|
proc.on("error", (err) => {
|
|
570
570
|
printWarning(`post-deploy probe spawn failed: ${err.message}`);
|
|
571
|
-
|
|
571
|
+
resolve6();
|
|
572
572
|
});
|
|
573
573
|
});
|
|
574
574
|
}
|
|
@@ -782,11 +782,21 @@ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsS
|
|
|
782
782
|
import chalk4 from "chalk";
|
|
783
783
|
|
|
784
784
|
// src/environments.ts
|
|
785
|
-
import {
|
|
785
|
+
import {
|
|
786
|
+
existsSync as existsSync4,
|
|
787
|
+
mkdirSync as mkdirSync2,
|
|
788
|
+
writeFileSync as writeFileSync2,
|
|
789
|
+
readFileSync as readFileSync2,
|
|
790
|
+
readdirSync
|
|
791
|
+
} from "fs";
|
|
786
792
|
import { join as join2 } from "path";
|
|
787
793
|
import { homedir as homedir2 } from "os";
|
|
788
794
|
var THINKWORK_HOME = join2(homedir2(), ".thinkwork");
|
|
789
795
|
var ENVIRONMENTS_DIR = join2(THINKWORK_HOME, "environments");
|
|
796
|
+
var ENTERPRISE_DEPLOYMENTS_DIR = join2(
|
|
797
|
+
THINKWORK_HOME,
|
|
798
|
+
"enterprise-deployments"
|
|
799
|
+
);
|
|
790
800
|
function ensureDir(dir) {
|
|
791
801
|
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
792
802
|
}
|
|
@@ -799,6 +809,13 @@ function saveEnvironment(config) {
|
|
|
799
809
|
JSON.stringify(config, null, 2) + "\n"
|
|
800
810
|
);
|
|
801
811
|
}
|
|
812
|
+
function saveEnterpriseDeployment(config) {
|
|
813
|
+
ensureDir(ENTERPRISE_DEPLOYMENTS_DIR);
|
|
814
|
+
writeFileSync2(
|
|
815
|
+
join2(ENTERPRISE_DEPLOYMENTS_DIR, `${config.customerSlug}.json`),
|
|
816
|
+
JSON.stringify(config, null, 2) + "\n"
|
|
817
|
+
);
|
|
818
|
+
}
|
|
802
819
|
function loadEnvironment(stage) {
|
|
803
820
|
const configPath = join2(ENVIRONMENTS_DIR, stage, "config.json");
|
|
804
821
|
if (!existsSync4(configPath)) return null;
|
|
@@ -1008,7 +1025,7 @@ function registerConfigCommand(program2) {
|
|
|
1008
1025
|
import { spawn as spawn3 } from "child_process";
|
|
1009
1026
|
import { resolve } from "path";
|
|
1010
1027
|
function getTerraformOutput(cwd, key) {
|
|
1011
|
-
return new Promise((
|
|
1028
|
+
return new Promise((resolve6, reject) => {
|
|
1012
1029
|
const proc = spawn3("terraform", ["output", "-raw", key], {
|
|
1013
1030
|
cwd,
|
|
1014
1031
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1016,17 +1033,17 @@ function getTerraformOutput(cwd, key) {
|
|
|
1016
1033
|
let stdout = "";
|
|
1017
1034
|
proc.stdout.on("data", (d) => stdout += d);
|
|
1018
1035
|
proc.on("close", (code) => {
|
|
1019
|
-
if (code === 0)
|
|
1036
|
+
if (code === 0) resolve6(stdout.trim());
|
|
1020
1037
|
else reject(new Error(`terraform output ${key} failed (exit ${code})`));
|
|
1021
1038
|
});
|
|
1022
1039
|
});
|
|
1023
1040
|
}
|
|
1024
1041
|
function runScript(scriptPath, args) {
|
|
1025
|
-
return new Promise((
|
|
1042
|
+
return new Promise((resolve6) => {
|
|
1026
1043
|
const proc = spawn3("bash", [scriptPath, ...args], {
|
|
1027
1044
|
stdio: "inherit"
|
|
1028
1045
|
});
|
|
1029
|
-
proc.on("close", (code) =>
|
|
1046
|
+
proc.on("close", (code) => resolve6(code ?? 1));
|
|
1030
1047
|
});
|
|
1031
1048
|
}
|
|
1032
1049
|
function registerBootstrapCommand(program2) {
|
|
@@ -1266,9 +1283,9 @@ async function ensureTerraform() {
|
|
|
1266
1283
|
}
|
|
1267
1284
|
async function ensurePrerequisites() {
|
|
1268
1285
|
console.log(chalk5.dim(" Checking prerequisites...\n"));
|
|
1269
|
-
const
|
|
1286
|
+
const awsOk2 = await ensureAwsCli();
|
|
1270
1287
|
const tfOk = await ensureTerraform();
|
|
1271
|
-
if (
|
|
1288
|
+
if (awsOk2 && tfOk) {
|
|
1272
1289
|
console.log("");
|
|
1273
1290
|
return true;
|
|
1274
1291
|
}
|
|
@@ -1401,7 +1418,7 @@ function buildAuthorizeUrl(cognito, redirectUri, state) {
|
|
|
1401
1418
|
return `${cognito.domainUrl}/oauth2/authorize?${params.toString()}`;
|
|
1402
1419
|
}
|
|
1403
1420
|
function waitForCallbackCode(opts) {
|
|
1404
|
-
return new Promise((
|
|
1421
|
+
return new Promise((resolve6, reject) => {
|
|
1405
1422
|
const server = createServer((req, res) => handleRequest(req, res));
|
|
1406
1423
|
let finished = false;
|
|
1407
1424
|
const finish = (err, code) => {
|
|
@@ -1412,7 +1429,7 @@ function waitForCallbackCode(opts) {
|
|
|
1412
1429
|
closer.closeAllConnections?.();
|
|
1413
1430
|
server.close(() => {
|
|
1414
1431
|
if (err) reject(err);
|
|
1415
|
-
else
|
|
1432
|
+
else resolve6(code);
|
|
1416
1433
|
});
|
|
1417
1434
|
};
|
|
1418
1435
|
const timer = setTimeout(() => {
|
|
@@ -1603,10 +1620,10 @@ function escapeHtml(s) {
|
|
|
1603
1620
|
// src/commands/login.ts
|
|
1604
1621
|
function ask(prompt) {
|
|
1605
1622
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1606
|
-
return new Promise((
|
|
1623
|
+
return new Promise((resolve6) => {
|
|
1607
1624
|
rl.question(prompt, (answer) => {
|
|
1608
1625
|
rl.close();
|
|
1609
|
-
|
|
1626
|
+
resolve6(answer.trim());
|
|
1610
1627
|
});
|
|
1611
1628
|
});
|
|
1612
1629
|
}
|
|
@@ -2018,8 +2035,8 @@ Registered callback URL:
|
|
|
2018
2035
|
}
|
|
2019
2036
|
const targetProfile = opts.profile ?? "thinkwork";
|
|
2020
2037
|
printHeader("login", targetProfile);
|
|
2021
|
-
const
|
|
2022
|
-
if (!
|
|
2038
|
+
const awsOk2 = await ensureAwsCli();
|
|
2039
|
+
if (!awsOk2) process.exit(1);
|
|
2023
2040
|
const port = Number.parseInt(opts.port, 10);
|
|
2024
2041
|
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
2025
2042
|
printError(`Invalid --port value: "${opts.port}".`);
|
|
@@ -2167,10 +2184,10 @@ var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
|
2167
2184
|
function ask2(prompt, defaultVal = "") {
|
|
2168
2185
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2169
2186
|
const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
|
|
2170
|
-
return new Promise((
|
|
2187
|
+
return new Promise((resolve6) => {
|
|
2171
2188
|
rl.question(` ${prompt}${suffix}: `, (answer) => {
|
|
2172
2189
|
rl.close();
|
|
2173
|
-
|
|
2190
|
+
resolve6(answer.trim() || defaultVal);
|
|
2174
2191
|
});
|
|
2175
2192
|
});
|
|
2176
2193
|
}
|
|
@@ -3906,7 +3923,7 @@ function registerUpdateCommand(program2) {
|
|
|
3906
3923
|
import { spawn as spawn5 } from "child_process";
|
|
3907
3924
|
import { input as input2, select as select7 } from "@inquirer/prompts";
|
|
3908
3925
|
function getTerraformOutput2(cwd, key) {
|
|
3909
|
-
return new Promise((
|
|
3926
|
+
return new Promise((resolve6, reject) => {
|
|
3910
3927
|
const proc = spawn5("terraform", ["output", "-raw", key], {
|
|
3911
3928
|
cwd,
|
|
3912
3929
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3916,7 +3933,7 @@ function getTerraformOutput2(cwd, key) {
|
|
|
3916
3933
|
proc.stdout.on("data", (d) => stdout += d);
|
|
3917
3934
|
proc.stderr.on("data", (d) => stderr += d);
|
|
3918
3935
|
proc.on("close", (code) => {
|
|
3919
|
-
if (code === 0)
|
|
3936
|
+
if (code === 0) resolve6(stdout.trim());
|
|
3920
3937
|
else
|
|
3921
3938
|
reject(
|
|
3922
3939
|
new Error(
|
|
@@ -3927,7 +3944,7 @@ function getTerraformOutput2(cwd, key) {
|
|
|
3927
3944
|
});
|
|
3928
3945
|
}
|
|
3929
3946
|
function runAwsCognitoReset(userPoolId, username, region) {
|
|
3930
|
-
return new Promise((
|
|
3947
|
+
return new Promise((resolve6) => {
|
|
3931
3948
|
const args = [
|
|
3932
3949
|
"cognito-idp",
|
|
3933
3950
|
"admin-reset-user-password",
|
|
@@ -3944,7 +3961,7 @@ function runAwsCognitoReset(userPoolId, username, region) {
|
|
|
3944
3961
|
let stderr = "";
|
|
3945
3962
|
proc.stdout.on("data", (d) => stdout += d);
|
|
3946
3963
|
proc.stderr.on("data", (d) => stderr += d);
|
|
3947
|
-
proc.on("close", (code) =>
|
|
3964
|
+
proc.on("close", (code) => resolve6({ code: code ?? 1, stdout, stderr }));
|
|
3948
3965
|
});
|
|
3949
3966
|
}
|
|
3950
3967
|
function requireTty2(label) {
|
|
@@ -4625,6 +4642,7 @@ var CliRoutineTenantBySlugDocument = { "kind": "Document", "definitions": [{ "ki
|
|
|
4625
4642
|
var CliScheduledJobsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliScheduledJobs" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "agentId" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "routineId" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "triggerType" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "enabled" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Boolean" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "limit" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "scheduledJobs" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "agentId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "agentId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "routineId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "routineId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "triggerType" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "triggerType" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "enabled" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "enabled" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "limit" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "limit" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "description" } }, { "kind": "Field", "name": { "kind": "Name", "value": "triggerType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "routineId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "scheduleType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "scheduleExpression" } }, { "kind": "Field", "name": { "kind": "Name", "value": "timezone" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "lastRunAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "nextRunAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
|
|
4626
4643
|
var CliScheduledJobDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliScheduledJob" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "scheduledJob" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "description" } }, { "kind": "Field", "name": { "kind": "Name", "value": "triggerType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "routineId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "prompt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "scheduleType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "scheduleExpression" } }, { "kind": "Field", "name": { "kind": "Name", "value": "timezone" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "ebScheduleName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "lastRunAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "nextRunAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updatedAt" } }] } }] } }] };
|
|
4627
4644
|
var CliCreateScheduledJobDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliCreateScheduledJob" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "input" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "CreateScheduledJobInput" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "createScheduledJob" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "input" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "input" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "scheduleExpression" } }, { "kind": "Field", "name": { "kind": "Name", "value": "timezone" } }] } }] } }] };
|
|
4645
|
+
var CliDeleteScheduledJobDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliDeleteScheduledJob" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "deleteScheduledJob" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "ok" } }] } }] } }] };
|
|
4628
4646
|
var CliSchedJobTenantBySlugDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliSchedJobTenantBySlug" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "tenantBySlug" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "slug" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }] } }] } }] };
|
|
4629
4647
|
var CliSkillCatalogDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliSkillCatalog" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "skillCatalog" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "skillId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "displayName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "description" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "icon" } }, { "kind": "Field", "name": { "kind": "Name", "value": "source" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }] } }] } }] };
|
|
4630
4648
|
var CliSkillTenantBySlugDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliSkillTenantBySlug" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "tenantBySlug" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "slug" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }] } }] } }] };
|
|
@@ -4800,6 +4818,7 @@ var documents = {
|
|
|
4800
4818
|
"\n query CliScheduledJobs(\n $tenantId: ID!\n $agentId: ID\n $routineId: ID\n $triggerType: String\n $enabled: Boolean\n $limit: Int\n ) {\n scheduledJobs(\n tenantId: $tenantId\n agentId: $agentId\n routineId: $routineId\n triggerType: $triggerType\n enabled: $enabled\n limit: $limit\n ) {\n id\n name\n description\n triggerType\n agentId\n routineId\n scheduleType\n scheduleExpression\n timezone\n enabled\n lastRunAt\n nextRunAt\n createdAt\n }\n }\n": CliScheduledJobsDocument,
|
|
4801
4819
|
"\n query CliScheduledJob($id: ID!) {\n scheduledJob(id: $id) {\n id\n name\n description\n triggerType\n agentId\n routineId\n prompt\n scheduleType\n scheduleExpression\n timezone\n enabled\n ebScheduleName\n lastRunAt\n nextRunAt\n createdAt\n updatedAt\n }\n }\n": CliScheduledJobDocument,
|
|
4802
4820
|
"\n mutation CliCreateScheduledJob($input: CreateScheduledJobInput!) {\n createScheduledJob(input: $input) {\n id\n name\n enabled\n scheduleExpression\n timezone\n }\n }\n": CliCreateScheduledJobDocument,
|
|
4821
|
+
"\n mutation CliDeleteScheduledJob($id: ID!) {\n deleteScheduledJob(id: $id) {\n id\n ok\n }\n }\n": CliDeleteScheduledJobDocument,
|
|
4803
4822
|
"\n query CliSchedJobTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n }\n }\n": CliSchedJobTenantBySlugDocument,
|
|
4804
4823
|
"\n query CliSkillCatalog {\n skillCatalog {\n id\n skillId\n displayName\n description\n category\n icon\n source\n enabled\n }\n }\n": CliSkillCatalogDocument,
|
|
4805
4824
|
"\n query CliSkillTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n }\n }\n": CliSkillTenantBySlugDocument,
|
|
@@ -9912,6 +9931,14 @@ var CreateScheduledJobDoc = graphql(`
|
|
|
9912
9931
|
}
|
|
9913
9932
|
}
|
|
9914
9933
|
`);
|
|
9934
|
+
var DeleteScheduledJobDoc = graphql(`
|
|
9935
|
+
mutation CliDeleteScheduledJob($id: ID!) {
|
|
9936
|
+
deleteScheduledJob(id: $id) {
|
|
9937
|
+
id
|
|
9938
|
+
ok
|
|
9939
|
+
}
|
|
9940
|
+
}
|
|
9941
|
+
`);
|
|
9915
9942
|
var SchedJobTenantBySlugDoc = graphql(`
|
|
9916
9943
|
query CliSchedJobTenantBySlug($slug: String!) {
|
|
9917
9944
|
tenantBySlug(slug: $slug) {
|
|
@@ -10072,6 +10099,39 @@ async function runSchedCreate(name, opts) {
|
|
|
10072
10099
|
`Created scheduled job ${data.createScheduledJob.id} \u2014 ${data.createScheduledJob.name} (${data.createScheduledJob.scheduleExpression}, ${data.createScheduledJob.timezone}).`
|
|
10073
10100
|
);
|
|
10074
10101
|
}
|
|
10102
|
+
async function runSchedDelete(id, opts) {
|
|
10103
|
+
const ctx = await resolveSchedContext(opts);
|
|
10104
|
+
if (!opts.yes) {
|
|
10105
|
+
if (!isInteractive()) {
|
|
10106
|
+
printError(
|
|
10107
|
+
"Refusing to delete without --yes in non-interactive mode (CI / piped stdin)."
|
|
10108
|
+
);
|
|
10109
|
+
process.exit(1);
|
|
10110
|
+
}
|
|
10111
|
+
requireTty("confirmation");
|
|
10112
|
+
const answer = await promptOrExit(
|
|
10113
|
+
() => input16({
|
|
10114
|
+
message: `Delete scheduled job ${id}? Type "delete" to confirm:`
|
|
10115
|
+
})
|
|
10116
|
+
);
|
|
10117
|
+
if (answer.trim() !== "delete") {
|
|
10118
|
+
console.log(" Cancelled.");
|
|
10119
|
+
return;
|
|
10120
|
+
}
|
|
10121
|
+
}
|
|
10122
|
+
const data = await gqlMutate(ctx.client, DeleteScheduledJobDoc, { id });
|
|
10123
|
+
if (isJsonMode()) {
|
|
10124
|
+
printJson(data.deleteScheduledJob);
|
|
10125
|
+
return;
|
|
10126
|
+
}
|
|
10127
|
+
if (data.deleteScheduledJob.ok) {
|
|
10128
|
+
printSuccess(`Deleted scheduled job ${data.deleteScheduledJob.id}.`);
|
|
10129
|
+
} else {
|
|
10130
|
+
console.log(
|
|
10131
|
+
` Scheduled job ${id} was already deleted (no row matched).`
|
|
10132
|
+
);
|
|
10133
|
+
}
|
|
10134
|
+
}
|
|
10075
10135
|
function notYetImplementedAtApi(verb) {
|
|
10076
10136
|
printError(
|
|
10077
10137
|
`\`scheduled-job ${verb}\` is not yet implemented at the GraphQL API.
|
|
@@ -10096,7 +10156,9 @@ Examples:
|
|
|
10096
10156
|
`
|
|
10097
10157
|
).action(runSchedCreate);
|
|
10098
10158
|
job.command("update <id>").description("Update a scheduled job. (API surface pending \u2014 currently a no-op.)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--schedule <expr>").option("--timezone <tz>").option("--payload <json>").option("--enable").option("--disable").action(() => notYetImplementedAtApi("update"));
|
|
10099
|
-
job.command("delete <id>").description(
|
|
10159
|
+
job.command("delete <id>").description(
|
|
10160
|
+
"Delete a scheduled job. Deprovisions the EventBridge schedule first, then removes the row."
|
|
10161
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(runSchedDelete);
|
|
10100
10162
|
job.command("run <id>").description("Trigger a scheduled job immediately. (API surface pending.)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--wait", "Block until the run completes").action(() => notYetImplementedAtApi("run"));
|
|
10101
10163
|
}
|
|
10102
10164
|
|
|
@@ -14393,6 +14455,895 @@ Examples:
|
|
|
14393
14455
|
});
|
|
14394
14456
|
}
|
|
14395
14457
|
|
|
14458
|
+
// src/commands/enterprise/bootstrap.ts
|
|
14459
|
+
import { resolve as resolve5 } from "path";
|
|
14460
|
+
|
|
14461
|
+
// src/commands/enterprise/template.ts
|
|
14462
|
+
import {
|
|
14463
|
+
existsSync as existsSync10,
|
|
14464
|
+
mkdirSync as mkdirSync5,
|
|
14465
|
+
readFileSync as readFileSync8,
|
|
14466
|
+
readdirSync as readdirSync2,
|
|
14467
|
+
statSync as statSync2,
|
|
14468
|
+
writeFileSync as writeFileSync5
|
|
14469
|
+
} from "fs";
|
|
14470
|
+
import { dirname as dirname4, join as join7, relative as relative2, resolve as resolve4 } from "path";
|
|
14471
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
14472
|
+
var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
|
|
14473
|
+
var MANAGED_MARKER = "thinkwork-managed: enterprise-deploy-template";
|
|
14474
|
+
var CUSTOMER_SLUG_PATTERN = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
|
|
14475
|
+
function renderEnterpriseDeployRepoTemplate(options) {
|
|
14476
|
+
const customerSlug = validateCustomerSlug(options.customerSlug);
|
|
14477
|
+
const stages = validateStages(options.stages ?? ["dev", "prod"]);
|
|
14478
|
+
const targetDir = resolve4(options.targetDir);
|
|
14479
|
+
const templateRoot = findEnterpriseTemplateRoot();
|
|
14480
|
+
const replacements = buildTemplateReplacements({
|
|
14481
|
+
...options,
|
|
14482
|
+
customerSlug,
|
|
14483
|
+
stages
|
|
14484
|
+
});
|
|
14485
|
+
const templateFiles = listTemplateFiles(templateRoot).filter(
|
|
14486
|
+
(path2) => !relative2(templateRoot, path2).startsWith("terraform/stages/") && !/^terraform\/backend-[^.]+\.hcl$/.test(
|
|
14487
|
+
relative2(templateRoot, path2).split("\\").join("/")
|
|
14488
|
+
)
|
|
14489
|
+
);
|
|
14490
|
+
const written = [];
|
|
14491
|
+
const preserved = [];
|
|
14492
|
+
for (const templatePath of templateFiles) {
|
|
14493
|
+
const relativePath = relative2(templateRoot, templatePath);
|
|
14494
|
+
const outputPath = join7(targetDir, relativePath);
|
|
14495
|
+
let content = applyTemplate(
|
|
14496
|
+
readFileSync8(templatePath, "utf8"),
|
|
14497
|
+
replacements
|
|
14498
|
+
);
|
|
14499
|
+
if (relativePath.split("\\").join("/") === "customer/deployment.json") {
|
|
14500
|
+
content = renderCustomerDeploymentJson(content, customerSlug, stages);
|
|
14501
|
+
}
|
|
14502
|
+
writeManagedFile(outputPath, content, written, preserved);
|
|
14503
|
+
}
|
|
14504
|
+
const stageTemplateRoot = join7(templateRoot, "terraform", "stages");
|
|
14505
|
+
const backendTemplatePath = join7(
|
|
14506
|
+
templateRoot,
|
|
14507
|
+
"terraform",
|
|
14508
|
+
"backend-dev.hcl"
|
|
14509
|
+
);
|
|
14510
|
+
for (const stage of stages) {
|
|
14511
|
+
const explicitTemplate = join7(stageTemplateRoot, `${stage}.tfvars`);
|
|
14512
|
+
const fallbackTemplate = join7(stageTemplateRoot, "dev.tfvars");
|
|
14513
|
+
const templatePath = existsSync10(explicitTemplate) ? explicitTemplate : fallbackTemplate;
|
|
14514
|
+
const outputPath = join7(
|
|
14515
|
+
targetDir,
|
|
14516
|
+
"terraform",
|
|
14517
|
+
"stages",
|
|
14518
|
+
`${stage}.tfvars`
|
|
14519
|
+
);
|
|
14520
|
+
const content = applyTemplate(readFileSync8(templatePath, "utf8"), {
|
|
14521
|
+
...replacements,
|
|
14522
|
+
STAGE: stage
|
|
14523
|
+
});
|
|
14524
|
+
writeManagedFile(outputPath, content, written, preserved);
|
|
14525
|
+
const backendPath = join7(targetDir, "terraform", `backend-${stage}.hcl`);
|
|
14526
|
+
const backendContent = applyTemplate(
|
|
14527
|
+
readFileSync8(backendTemplatePath, "utf8"),
|
|
14528
|
+
{
|
|
14529
|
+
...replacements,
|
|
14530
|
+
STAGE: stage
|
|
14531
|
+
}
|
|
14532
|
+
);
|
|
14533
|
+
writeManagedFile(backendPath, backendContent, written, preserved);
|
|
14534
|
+
}
|
|
14535
|
+
return { targetDir, written, preserved };
|
|
14536
|
+
}
|
|
14537
|
+
function validateCustomerSlug(slug) {
|
|
14538
|
+
const normalized = slug.trim();
|
|
14539
|
+
if (!CUSTOMER_SLUG_PATTERN.test(normalized)) {
|
|
14540
|
+
throw new Error(
|
|
14541
|
+
`Invalid customer slug "${slug}". Must be lowercase alphanumeric + hyphens, 3-40 characters, starting with a letter.`
|
|
14542
|
+
);
|
|
14543
|
+
}
|
|
14544
|
+
return normalized;
|
|
14545
|
+
}
|
|
14546
|
+
function validateStages(stages) {
|
|
14547
|
+
const normalized = [
|
|
14548
|
+
...new Set(stages.map((stage) => stage.trim()).filter(Boolean))
|
|
14549
|
+
];
|
|
14550
|
+
if (normalized.length === 0) {
|
|
14551
|
+
throw new Error("At least one deployment stage is required.");
|
|
14552
|
+
}
|
|
14553
|
+
for (const stage of normalized) {
|
|
14554
|
+
const check = validateStage(stage);
|
|
14555
|
+
if (!check.valid) {
|
|
14556
|
+
throw new Error(check.error ?? `Invalid stage name "${stage}".`);
|
|
14557
|
+
}
|
|
14558
|
+
}
|
|
14559
|
+
return normalized;
|
|
14560
|
+
}
|
|
14561
|
+
function findEnterpriseTemplateRoot() {
|
|
14562
|
+
const candidates = [
|
|
14563
|
+
resolve4(__dirname2, "templates/deploy-repo"),
|
|
14564
|
+
resolve4(__dirname2, "commands/enterprise/templates/deploy-repo")
|
|
14565
|
+
];
|
|
14566
|
+
for (const candidate of candidates) {
|
|
14567
|
+
if (existsSync10(join7(candidate, "thinkwork.lock"))) return candidate;
|
|
14568
|
+
}
|
|
14569
|
+
throw new Error(
|
|
14570
|
+
"Enterprise deployment repo template not found. The CLI package may be incomplete."
|
|
14571
|
+
);
|
|
14572
|
+
}
|
|
14573
|
+
function buildTemplateReplacements(options) {
|
|
14574
|
+
const releaseVersion = options.releaseVersion ?? "v0.0.0";
|
|
14575
|
+
const artifactBucket = options.artifactBucket ?? `${options.customerSlug}-thinkwork-release-artifacts`;
|
|
14576
|
+
return {
|
|
14577
|
+
ACCOUNT_ID: options.accountId ?? "123456789012",
|
|
14578
|
+
ARTIFACT_BUCKET: artifactBucket,
|
|
14579
|
+
CUSTOMER_SLUG: options.customerSlug,
|
|
14580
|
+
LAMBDA_ARTIFACT_PREFIX: `releases/${releaseVersion}/lambdas`,
|
|
14581
|
+
REGION: options.region ?? "us-east-1",
|
|
14582
|
+
RELEASE_MANIFEST_SHA256: options.releaseManifestSha256 ?? "CHANGE_ME",
|
|
14583
|
+
RELEASE_MANIFEST_URL: options.releaseManifestUrl ?? `https://github.com/thinkwork-ai/thinkwork/releases/download/${releaseVersion}/thinkwork-release.json`,
|
|
14584
|
+
RELEASE_VERSION: releaseVersion,
|
|
14585
|
+
TERRAFORM_MODULE_VERSION: options.terraformModuleVersion ?? releaseVersion.replace(/^v/, "")
|
|
14586
|
+
};
|
|
14587
|
+
}
|
|
14588
|
+
function renderCustomerDeploymentJson(content, customerSlug, stages) {
|
|
14589
|
+
const deployment = JSON.parse(content);
|
|
14590
|
+
deployment.stages = Object.fromEntries(
|
|
14591
|
+
stages.map((stage) => [
|
|
14592
|
+
stage,
|
|
14593
|
+
{
|
|
14594
|
+
tenantSlug: stage === "prod" ? customerSlug : `${customerSlug}-${stage}`,
|
|
14595
|
+
evalPacks: [],
|
|
14596
|
+
seedPacks: [],
|
|
14597
|
+
skillPacks: [],
|
|
14598
|
+
workspaceDefaultPacks: [],
|
|
14599
|
+
branding: null
|
|
14600
|
+
}
|
|
14601
|
+
])
|
|
14602
|
+
);
|
|
14603
|
+
return `${JSON.stringify(deployment, null, 2)}
|
|
14604
|
+
`;
|
|
14605
|
+
}
|
|
14606
|
+
function listTemplateFiles(root) {
|
|
14607
|
+
const out = [];
|
|
14608
|
+
for (const entry of readdirSync2(root)) {
|
|
14609
|
+
const path2 = join7(root, entry);
|
|
14610
|
+
const stat = statSync2(path2);
|
|
14611
|
+
if (stat.isDirectory()) {
|
|
14612
|
+
out.push(...listTemplateFiles(path2));
|
|
14613
|
+
} else if (stat.isFile()) {
|
|
14614
|
+
out.push(path2);
|
|
14615
|
+
}
|
|
14616
|
+
}
|
|
14617
|
+
return out.sort();
|
|
14618
|
+
}
|
|
14619
|
+
function applyTemplate(source, replacements) {
|
|
14620
|
+
return source.replaceAll(/\{\{([A-Z0-9_]+)\}\}/g, (_match, key) => {
|
|
14621
|
+
if (!(key in replacements)) {
|
|
14622
|
+
throw new Error(`Unknown enterprise deployment template token: ${key}`);
|
|
14623
|
+
}
|
|
14624
|
+
return replacements[key];
|
|
14625
|
+
});
|
|
14626
|
+
}
|
|
14627
|
+
function writeManagedFile(path2, content, written, preserved) {
|
|
14628
|
+
mkdirSync5(dirname4(path2), { recursive: true });
|
|
14629
|
+
if (existsSync10(path2)) {
|
|
14630
|
+
const current = readFileSync8(path2, "utf8");
|
|
14631
|
+
if (!current.includes(MANAGED_MARKER)) {
|
|
14632
|
+
preserved.push(path2);
|
|
14633
|
+
return;
|
|
14634
|
+
}
|
|
14635
|
+
}
|
|
14636
|
+
writeFileSync5(path2, content);
|
|
14637
|
+
written.push(path2);
|
|
14638
|
+
}
|
|
14639
|
+
|
|
14640
|
+
// src/commands/enterprise/aws-bootstrap.ts
|
|
14641
|
+
import { execFileSync } from "child_process";
|
|
14642
|
+
import { mkdtempSync, writeFileSync as writeFileSync6 } from "fs";
|
|
14643
|
+
import { tmpdir } from "os";
|
|
14644
|
+
import { join as join8 } from "path";
|
|
14645
|
+
function buildEnterpriseAwsBootstrapPlan(config) {
|
|
14646
|
+
const oidcProviderArn = `arn:aws:iam::${config.accountId}:oidc-provider/token.actions.githubusercontent.com`;
|
|
14647
|
+
return {
|
|
14648
|
+
stateBucket: config.stateBucket,
|
|
14649
|
+
lockTable: config.lockTable,
|
|
14650
|
+
artifactBucket: config.artifactBucket,
|
|
14651
|
+
oidcProviderArn,
|
|
14652
|
+
stageRoles: config.stages.map((stage) => {
|
|
14653
|
+
const roleName = `thinkwork-${config.customerSlug}-${stage}-deploy`;
|
|
14654
|
+
return {
|
|
14655
|
+
stage,
|
|
14656
|
+
roleName,
|
|
14657
|
+
roleArn: `arn:aws:iam::${config.accountId}:role/${roleName}`,
|
|
14658
|
+
trustPolicy: buildGitHubOidcTrustPolicy({
|
|
14659
|
+
oidcProviderArn,
|
|
14660
|
+
repository: config.repository,
|
|
14661
|
+
stage
|
|
14662
|
+
}),
|
|
14663
|
+
deployPolicyName: `thinkwork-${config.customerSlug}-${stage}-deploy`,
|
|
14664
|
+
deployPolicy: buildEnterpriseDeployRolePolicy({
|
|
14665
|
+
accountId: config.accountId,
|
|
14666
|
+
region: config.region,
|
|
14667
|
+
stage,
|
|
14668
|
+
stateBucket: config.stateBucket,
|
|
14669
|
+
lockTable: config.lockTable,
|
|
14670
|
+
artifactBucket: config.artifactBucket
|
|
14671
|
+
})
|
|
14672
|
+
};
|
|
14673
|
+
})
|
|
14674
|
+
};
|
|
14675
|
+
}
|
|
14676
|
+
function buildGitHubOidcTrustPolicy(options) {
|
|
14677
|
+
return {
|
|
14678
|
+
Version: "2012-10-17",
|
|
14679
|
+
Statement: [
|
|
14680
|
+
{
|
|
14681
|
+
Effect: "Allow",
|
|
14682
|
+
Principal: {
|
|
14683
|
+
Federated: options.oidcProviderArn
|
|
14684
|
+
},
|
|
14685
|
+
Action: "sts:AssumeRoleWithWebIdentity",
|
|
14686
|
+
Condition: {
|
|
14687
|
+
StringEquals: {
|
|
14688
|
+
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
|
|
14689
|
+
"token.actions.githubusercontent.com:sub": `repo:${options.repository}:environment:${options.stage}`
|
|
14690
|
+
}
|
|
14691
|
+
}
|
|
14692
|
+
}
|
|
14693
|
+
]
|
|
14694
|
+
};
|
|
14695
|
+
}
|
|
14696
|
+
function buildEnterpriseDeployRolePolicy(options) {
|
|
14697
|
+
const thinkworkPrefix = `thinkwork-${options.stage}`;
|
|
14698
|
+
return {
|
|
14699
|
+
Version: "2012-10-17",
|
|
14700
|
+
Statement: [
|
|
14701
|
+
{
|
|
14702
|
+
Sid: "TerraformStateAndReleaseBuckets",
|
|
14703
|
+
Effect: "Allow",
|
|
14704
|
+
Action: [
|
|
14705
|
+
"s3:GetBucketLocation",
|
|
14706
|
+
"s3:GetBucketVersioning",
|
|
14707
|
+
"s3:GetEncryptionConfiguration",
|
|
14708
|
+
"s3:ListBucket",
|
|
14709
|
+
"s3:PutBucketVersioning",
|
|
14710
|
+
"s3:PutEncryptionConfiguration",
|
|
14711
|
+
"s3:PutLifecycleConfiguration",
|
|
14712
|
+
"s3:PutPublicAccessBlock"
|
|
14713
|
+
],
|
|
14714
|
+
Resource: [
|
|
14715
|
+
`arn:aws:s3:::${options.stateBucket}`,
|
|
14716
|
+
`arn:aws:s3:::${options.artifactBucket}`,
|
|
14717
|
+
`arn:aws:s3:::${thinkworkPrefix}-*`
|
|
14718
|
+
]
|
|
14719
|
+
},
|
|
14720
|
+
{
|
|
14721
|
+
Sid: "TerraformStateAndReleaseObjects",
|
|
14722
|
+
Effect: "Allow",
|
|
14723
|
+
Action: [
|
|
14724
|
+
"s3:AbortMultipartUpload",
|
|
14725
|
+
"s3:DeleteObject",
|
|
14726
|
+
"s3:GetObject",
|
|
14727
|
+
"s3:PutObject"
|
|
14728
|
+
],
|
|
14729
|
+
Resource: [
|
|
14730
|
+
`arn:aws:s3:::${options.stateBucket}/*`,
|
|
14731
|
+
`arn:aws:s3:::${options.artifactBucket}/*`,
|
|
14732
|
+
`arn:aws:s3:::${thinkworkPrefix}-*/*`
|
|
14733
|
+
]
|
|
14734
|
+
},
|
|
14735
|
+
{
|
|
14736
|
+
Sid: "TerraformStateLocks",
|
|
14737
|
+
Effect: "Allow",
|
|
14738
|
+
Action: [
|
|
14739
|
+
"dynamodb:CreateTable",
|
|
14740
|
+
"dynamodb:DescribeTable",
|
|
14741
|
+
"dynamodb:GetItem",
|
|
14742
|
+
"dynamodb:PutItem",
|
|
14743
|
+
"dynamodb:DeleteItem",
|
|
14744
|
+
"dynamodb:UpdateItem"
|
|
14745
|
+
],
|
|
14746
|
+
Resource: `arn:aws:dynamodb:${options.region}:${options.accountId}:table/${options.lockTable}`
|
|
14747
|
+
},
|
|
14748
|
+
{
|
|
14749
|
+
Sid: "ThinkWorkNamedResources",
|
|
14750
|
+
Effect: "Allow",
|
|
14751
|
+
Action: [
|
|
14752
|
+
"apigateway:*",
|
|
14753
|
+
"appsync:*",
|
|
14754
|
+
"bedrock:*",
|
|
14755
|
+
"bedrock-agentcore:*",
|
|
14756
|
+
"cloudfront:*",
|
|
14757
|
+
"cognito-idp:*",
|
|
14758
|
+
"dynamodb:*",
|
|
14759
|
+
"ec2:*",
|
|
14760
|
+
"ecr:*",
|
|
14761
|
+
"ecs:*",
|
|
14762
|
+
"elasticfilesystem:*",
|
|
14763
|
+
"elasticloadbalancing:*",
|
|
14764
|
+
"events:*",
|
|
14765
|
+
"iam:*",
|
|
14766
|
+
"lambda:*",
|
|
14767
|
+
"logs:*",
|
|
14768
|
+
"rds:*",
|
|
14769
|
+
"scheduler:*",
|
|
14770
|
+
"secretsmanager:*",
|
|
14771
|
+
"ses:*",
|
|
14772
|
+
"sqs:*",
|
|
14773
|
+
"ssm:*",
|
|
14774
|
+
"states:*",
|
|
14775
|
+
"xray:*"
|
|
14776
|
+
],
|
|
14777
|
+
Resource: "*"
|
|
14778
|
+
}
|
|
14779
|
+
]
|
|
14780
|
+
};
|
|
14781
|
+
}
|
|
14782
|
+
var AwsCliEnterpriseBootstrapClient = class {
|
|
14783
|
+
async ensureStateBucket(bucket, region) {
|
|
14784
|
+
return ensureBucket(bucket, region, "terraform state bucket");
|
|
14785
|
+
}
|
|
14786
|
+
async ensureLockTable(table, region) {
|
|
14787
|
+
if (awsOk([
|
|
14788
|
+
"dynamodb",
|
|
14789
|
+
"describe-table",
|
|
14790
|
+
"--table-name",
|
|
14791
|
+
table,
|
|
14792
|
+
"--region",
|
|
14793
|
+
region
|
|
14794
|
+
])) {
|
|
14795
|
+
return {
|
|
14796
|
+
target: table,
|
|
14797
|
+
status: "reused",
|
|
14798
|
+
message: `DynamoDB lock table ${table} already exists.`
|
|
14799
|
+
};
|
|
14800
|
+
}
|
|
14801
|
+
execFileSync("aws", [
|
|
14802
|
+
"dynamodb",
|
|
14803
|
+
"create-table",
|
|
14804
|
+
"--table-name",
|
|
14805
|
+
table,
|
|
14806
|
+
"--attribute-definitions",
|
|
14807
|
+
"AttributeName=LockID,AttributeType=S",
|
|
14808
|
+
"--key-schema",
|
|
14809
|
+
"AttributeName=LockID,KeyType=HASH",
|
|
14810
|
+
"--billing-mode",
|
|
14811
|
+
"PAY_PER_REQUEST",
|
|
14812
|
+
"--region",
|
|
14813
|
+
region
|
|
14814
|
+
]);
|
|
14815
|
+
return {
|
|
14816
|
+
target: table,
|
|
14817
|
+
status: "created",
|
|
14818
|
+
message: `Created DynamoDB lock table ${table}.`
|
|
14819
|
+
};
|
|
14820
|
+
}
|
|
14821
|
+
async ensureArtifactBucket(bucket, region) {
|
|
14822
|
+
return ensureBucket(bucket, region, "release artifact bucket");
|
|
14823
|
+
}
|
|
14824
|
+
async ensureOidcProvider(accountId) {
|
|
14825
|
+
const arn = `arn:aws:iam::${accountId}:oidc-provider/token.actions.githubusercontent.com`;
|
|
14826
|
+
if (awsOk([
|
|
14827
|
+
"iam",
|
|
14828
|
+
"get-open-id-connect-provider",
|
|
14829
|
+
"--open-id-connect-provider-arn",
|
|
14830
|
+
arn
|
|
14831
|
+
])) {
|
|
14832
|
+
return {
|
|
14833
|
+
target: arn,
|
|
14834
|
+
status: "reused",
|
|
14835
|
+
message: "GitHub Actions OIDC provider already exists."
|
|
14836
|
+
};
|
|
14837
|
+
}
|
|
14838
|
+
execFileSync("aws", [
|
|
14839
|
+
"iam",
|
|
14840
|
+
"create-open-id-connect-provider",
|
|
14841
|
+
"--url",
|
|
14842
|
+
"https://token.actions.githubusercontent.com",
|
|
14843
|
+
"--client-id-list",
|
|
14844
|
+
"sts.amazonaws.com",
|
|
14845
|
+
"--thumbprint-list",
|
|
14846
|
+
"6938fd4d98bab03faadb97b34396831e3780aea1"
|
|
14847
|
+
]);
|
|
14848
|
+
return {
|
|
14849
|
+
target: arn,
|
|
14850
|
+
status: "created",
|
|
14851
|
+
message: "Created GitHub Actions OIDC provider."
|
|
14852
|
+
};
|
|
14853
|
+
}
|
|
14854
|
+
async ensureDeployRole(role) {
|
|
14855
|
+
if (awsOk(["iam", "get-role", "--role-name", role.roleName])) {
|
|
14856
|
+
putRolePolicy(role);
|
|
14857
|
+
return {
|
|
14858
|
+
target: role.roleArn,
|
|
14859
|
+
status: "updated",
|
|
14860
|
+
message: `Deploy role ${role.roleName} already exists; updated inline deploy policy ${role.deployPolicyName}.`
|
|
14861
|
+
};
|
|
14862
|
+
}
|
|
14863
|
+
const dir = mkdtempSync(join8(tmpdir(), "thinkwork-enterprise-role-"));
|
|
14864
|
+
const trustPath = join8(dir, "trust.json");
|
|
14865
|
+
writeFileSync6(trustPath, JSON.stringify(role.trustPolicy));
|
|
14866
|
+
execFileSync("aws", [
|
|
14867
|
+
"iam",
|
|
14868
|
+
"create-role",
|
|
14869
|
+
"--role-name",
|
|
14870
|
+
role.roleName,
|
|
14871
|
+
"--assume-role-policy-document",
|
|
14872
|
+
`file://${trustPath}`
|
|
14873
|
+
]);
|
|
14874
|
+
putRolePolicy(role);
|
|
14875
|
+
return {
|
|
14876
|
+
target: role.roleArn,
|
|
14877
|
+
status: "created",
|
|
14878
|
+
message: `Created deploy role ${role.roleName} and attached inline deploy policy ${role.deployPolicyName}.`
|
|
14879
|
+
};
|
|
14880
|
+
}
|
|
14881
|
+
};
|
|
14882
|
+
function putRolePolicy(role) {
|
|
14883
|
+
const dir = mkdtempSync(join8(tmpdir(), "thinkwork-enterprise-policy-"));
|
|
14884
|
+
const policyPath = join8(dir, "policy.json");
|
|
14885
|
+
writeFileSync6(policyPath, JSON.stringify(role.deployPolicy));
|
|
14886
|
+
execFileSync("aws", [
|
|
14887
|
+
"iam",
|
|
14888
|
+
"put-role-policy",
|
|
14889
|
+
"--role-name",
|
|
14890
|
+
role.roleName,
|
|
14891
|
+
"--policy-name",
|
|
14892
|
+
role.deployPolicyName,
|
|
14893
|
+
"--policy-document",
|
|
14894
|
+
`file://${policyPath}`
|
|
14895
|
+
]);
|
|
14896
|
+
}
|
|
14897
|
+
function ensureBucket(bucket, region, label) {
|
|
14898
|
+
if (awsOk(["s3api", "head-bucket", "--bucket", bucket])) {
|
|
14899
|
+
return {
|
|
14900
|
+
target: bucket,
|
|
14901
|
+
status: "reused",
|
|
14902
|
+
message: `${label} ${bucket} already exists.`
|
|
14903
|
+
};
|
|
14904
|
+
}
|
|
14905
|
+
const args = region === "us-east-1" ? ["s3api", "create-bucket", "--bucket", bucket, "--region", region] : [
|
|
14906
|
+
"s3api",
|
|
14907
|
+
"create-bucket",
|
|
14908
|
+
"--bucket",
|
|
14909
|
+
bucket,
|
|
14910
|
+
"--region",
|
|
14911
|
+
region,
|
|
14912
|
+
"--create-bucket-configuration",
|
|
14913
|
+
`LocationConstraint=${region}`
|
|
14914
|
+
];
|
|
14915
|
+
execFileSync("aws", args);
|
|
14916
|
+
execFileSync("aws", [
|
|
14917
|
+
"s3api",
|
|
14918
|
+
"put-public-access-block",
|
|
14919
|
+
"--bucket",
|
|
14920
|
+
bucket,
|
|
14921
|
+
"--public-access-block-configuration",
|
|
14922
|
+
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
|
|
14923
|
+
]);
|
|
14924
|
+
return {
|
|
14925
|
+
target: bucket,
|
|
14926
|
+
status: "created",
|
|
14927
|
+
message: `Created ${label} ${bucket}.`
|
|
14928
|
+
};
|
|
14929
|
+
}
|
|
14930
|
+
function awsOk(args) {
|
|
14931
|
+
try {
|
|
14932
|
+
execFileSync("aws", args, { stdio: "ignore" });
|
|
14933
|
+
return true;
|
|
14934
|
+
} catch {
|
|
14935
|
+
return false;
|
|
14936
|
+
}
|
|
14937
|
+
}
|
|
14938
|
+
|
|
14939
|
+
// src/commands/enterprise/github.ts
|
|
14940
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
14941
|
+
function parseGitHubRepository(input20) {
|
|
14942
|
+
const trimmed = input20.trim();
|
|
14943
|
+
const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(trimmed);
|
|
14944
|
+
if (!match) {
|
|
14945
|
+
throw new Error(
|
|
14946
|
+
`Invalid GitHub repository "${input20}". Use owner/name, for example acme/thinkwork-deploy.`
|
|
14947
|
+
);
|
|
14948
|
+
}
|
|
14949
|
+
return {
|
|
14950
|
+
owner: match[1],
|
|
14951
|
+
name: match[2],
|
|
14952
|
+
fullName: `${match[1]}/${match[2]}`
|
|
14953
|
+
};
|
|
14954
|
+
}
|
|
14955
|
+
function buildEnterpriseGitHubBootstrapPlan(options) {
|
|
14956
|
+
const repository = parseGitHubRepository(options.repository);
|
|
14957
|
+
return {
|
|
14958
|
+
repository,
|
|
14959
|
+
dispatchWorkflow: options.dispatchWorkflow ?? false,
|
|
14960
|
+
environments: options.stages.map((stage) => {
|
|
14961
|
+
const role = options.stageRoles.find((item) => item.stage === stage);
|
|
14962
|
+
if (!role) {
|
|
14963
|
+
throw new Error(`Missing deploy role for stage "${stage}".`);
|
|
14964
|
+
}
|
|
14965
|
+
return {
|
|
14966
|
+
stage,
|
|
14967
|
+
roleArn: role.roleArn,
|
|
14968
|
+
vars: {
|
|
14969
|
+
AWS_REGION: options.region,
|
|
14970
|
+
AWS_ROLE_ARN: role.roleArn,
|
|
14971
|
+
THINKWORK_ARTIFACT_BUCKET: options.artifactBucket
|
|
14972
|
+
},
|
|
14973
|
+
secretPlaceholders: ["TF_VAR_DB_PASSWORD", "TF_VAR_API_AUTH_SECRET"]
|
|
14974
|
+
};
|
|
14975
|
+
})
|
|
14976
|
+
};
|
|
14977
|
+
}
|
|
14978
|
+
var GhCliEnterpriseBootstrapClient = class {
|
|
14979
|
+
constructor(repository) {
|
|
14980
|
+
this.repository = repository;
|
|
14981
|
+
}
|
|
14982
|
+
repository;
|
|
14983
|
+
async ensureEnvironment(environment) {
|
|
14984
|
+
gh([
|
|
14985
|
+
"api",
|
|
14986
|
+
"--method",
|
|
14987
|
+
"PUT",
|
|
14988
|
+
`repos/${this.repository.fullName}/environments/${environment.stage}`,
|
|
14989
|
+
"--field",
|
|
14990
|
+
"wait_timer=0"
|
|
14991
|
+
]);
|
|
14992
|
+
return {
|
|
14993
|
+
target: `${this.repository.fullName}:${environment.stage}`,
|
|
14994
|
+
status: "updated",
|
|
14995
|
+
message: `Ensured GitHub Environment ${environment.stage}.`
|
|
14996
|
+
};
|
|
14997
|
+
}
|
|
14998
|
+
async upsertEnvironmentVariables(environment) {
|
|
14999
|
+
for (const [name, value] of Object.entries(environment.vars)) {
|
|
15000
|
+
gh([
|
|
15001
|
+
"variable",
|
|
15002
|
+
"set",
|
|
15003
|
+
name,
|
|
15004
|
+
"--repo",
|
|
15005
|
+
this.repository.fullName,
|
|
15006
|
+
"--env",
|
|
15007
|
+
environment.stage,
|
|
15008
|
+
"--body",
|
|
15009
|
+
value
|
|
15010
|
+
]);
|
|
15011
|
+
}
|
|
15012
|
+
return {
|
|
15013
|
+
target: `${this.repository.fullName}:${environment.stage}:vars`,
|
|
15014
|
+
status: "updated",
|
|
15015
|
+
message: `Updated non-secret GitHub Environment variables for ${environment.stage}.`
|
|
15016
|
+
};
|
|
15017
|
+
}
|
|
15018
|
+
async writeRepositoryFiles(targetDir) {
|
|
15019
|
+
return {
|
|
15020
|
+
target: targetDir,
|
|
15021
|
+
status: "updated",
|
|
15022
|
+
message: "Repository files were written locally. Commit/push this directory or run from a checked-out deployment repo."
|
|
15023
|
+
};
|
|
15024
|
+
}
|
|
15025
|
+
async dispatchWorkflow(stage) {
|
|
15026
|
+
gh([
|
|
15027
|
+
"workflow",
|
|
15028
|
+
"run",
|
|
15029
|
+
"deploy.yml",
|
|
15030
|
+
"--repo",
|
|
15031
|
+
this.repository.fullName,
|
|
15032
|
+
"--field",
|
|
15033
|
+
`stage=${stage}`
|
|
15034
|
+
]);
|
|
15035
|
+
return {
|
|
15036
|
+
target: `${this.repository.fullName}:deploy.yml:${stage}`,
|
|
15037
|
+
status: "created",
|
|
15038
|
+
message: `Dispatched deploy workflow for ${stage}.`
|
|
15039
|
+
};
|
|
15040
|
+
}
|
|
15041
|
+
};
|
|
15042
|
+
function gh(args) {
|
|
15043
|
+
return execFileSync2("gh", args, { encoding: "utf8" });
|
|
15044
|
+
}
|
|
15045
|
+
|
|
15046
|
+
// src/commands/enterprise/release.ts
|
|
15047
|
+
function resolveEnterpriseReleasePin(options) {
|
|
15048
|
+
const version = options.releaseVersion ?? `v${VERSION}`;
|
|
15049
|
+
return {
|
|
15050
|
+
version,
|
|
15051
|
+
manifestUrl: options.manifestUrl ?? `https://github.com/thinkwork-ai/thinkwork/releases/download/${version}/thinkwork-release.json`,
|
|
15052
|
+
manifestSha256: options.manifestSha256 ?? "CHANGE_ME",
|
|
15053
|
+
terraformModuleVersion: options.terraformModuleVersion ?? version.replace(/^v/, "")
|
|
15054
|
+
};
|
|
15055
|
+
}
|
|
15056
|
+
|
|
15057
|
+
// src/commands/enterprise/bootstrap.ts
|
|
15058
|
+
function buildEnterpriseBootstrapPlan(options, identity) {
|
|
15059
|
+
const customerSlug = validateCustomerSlug(options.customerSlug);
|
|
15060
|
+
const repository = parseGitHubRepository(options.repository).fullName;
|
|
15061
|
+
const stages = validateStages(options.stages ?? ["dev", "prod"]);
|
|
15062
|
+
const accountId = options.accountId ?? identity?.account;
|
|
15063
|
+
const region = options.region ?? identity?.region;
|
|
15064
|
+
if (!accountId) {
|
|
15065
|
+
throw new Error(
|
|
15066
|
+
"AWS account ID is required. Configure AWS credentials or pass --account-id."
|
|
15067
|
+
);
|
|
15068
|
+
}
|
|
15069
|
+
if (!region || region === "unknown") {
|
|
15070
|
+
throw new Error(
|
|
15071
|
+
"AWS region is required. Configure AWS_REGION or pass --region."
|
|
15072
|
+
);
|
|
15073
|
+
}
|
|
15074
|
+
const release = resolveEnterpriseReleasePin({
|
|
15075
|
+
releaseVersion: options.releaseVersion,
|
|
15076
|
+
manifestUrl: options.manifestUrl,
|
|
15077
|
+
manifestSha256: options.manifestSha256,
|
|
15078
|
+
terraformModuleVersion: options.terraformModuleVersion
|
|
15079
|
+
});
|
|
15080
|
+
const artifactBucket = options.artifactBucket ?? `${customerSlug}-thinkwork-release-artifacts`;
|
|
15081
|
+
const stateBucket = options.stateBucket ?? `${customerSlug}-thinkwork-terraform-state`;
|
|
15082
|
+
const lockTable = options.lockTable ?? `${customerSlug}-thinkwork-terraform-locks`;
|
|
15083
|
+
const aws = buildEnterpriseAwsBootstrapPlan({
|
|
15084
|
+
accountId,
|
|
15085
|
+
region,
|
|
15086
|
+
repository,
|
|
15087
|
+
stages,
|
|
15088
|
+
customerSlug,
|
|
15089
|
+
stateBucket,
|
|
15090
|
+
lockTable,
|
|
15091
|
+
artifactBucket
|
|
15092
|
+
});
|
|
15093
|
+
const github = buildEnterpriseGitHubBootstrapPlan({
|
|
15094
|
+
repository,
|
|
15095
|
+
stages,
|
|
15096
|
+
region,
|
|
15097
|
+
artifactBucket,
|
|
15098
|
+
stageRoles: aws.stageRoles,
|
|
15099
|
+
dispatchWorkflow: options.dispatchWorkflow
|
|
15100
|
+
});
|
|
15101
|
+
return {
|
|
15102
|
+
customerSlug,
|
|
15103
|
+
targetDir: resolve5(options.targetDir),
|
|
15104
|
+
repository,
|
|
15105
|
+
stages,
|
|
15106
|
+
accountId,
|
|
15107
|
+
region,
|
|
15108
|
+
release,
|
|
15109
|
+
aws,
|
|
15110
|
+
github
|
|
15111
|
+
};
|
|
15112
|
+
}
|
|
15113
|
+
async function runEnterpriseBootstrap(options, deps = {}) {
|
|
15114
|
+
const identity = deps.identity === void 0 ? getAwsIdentity() : deps.identity;
|
|
15115
|
+
if (!options.dryRun && !identity && (!options.accountId || !options.region)) {
|
|
15116
|
+
throw new Error(
|
|
15117
|
+
"AWS identity is required before mutating AWS or GitHub resources."
|
|
15118
|
+
);
|
|
15119
|
+
}
|
|
15120
|
+
const plan = buildEnterpriseBootstrapPlan(options, identity);
|
|
15121
|
+
const template = renderEnterpriseDeployRepoTemplate({
|
|
15122
|
+
targetDir: plan.targetDir,
|
|
15123
|
+
customerSlug: plan.customerSlug,
|
|
15124
|
+
stages: plan.stages,
|
|
15125
|
+
region: plan.region,
|
|
15126
|
+
accountId: plan.accountId,
|
|
15127
|
+
releaseVersion: plan.release.version,
|
|
15128
|
+
releaseManifestUrl: plan.release.manifestUrl,
|
|
15129
|
+
releaseManifestSha256: plan.release.manifestSha256,
|
|
15130
|
+
terraformModuleVersion: plan.release.terraformModuleVersion,
|
|
15131
|
+
artifactBucket: plan.aws.artifactBucket
|
|
15132
|
+
});
|
|
15133
|
+
const awsClient = deps.awsClient ?? new AwsCliEnterpriseBootstrapClient();
|
|
15134
|
+
const githubClient = deps.githubClient ?? new GhCliEnterpriseBootstrapClient(parseGitHubRepository(plan.repository));
|
|
15135
|
+
const awsResults = [];
|
|
15136
|
+
const githubResults = [];
|
|
15137
|
+
if (options.dryRun) {
|
|
15138
|
+
awsResults.push(
|
|
15139
|
+
planned(plan.aws.stateBucket, "Would ensure Terraform state bucket."),
|
|
15140
|
+
planned(plan.aws.lockTable, "Would ensure Terraform lock table."),
|
|
15141
|
+
planned(plan.aws.artifactBucket, "Would ensure release artifact bucket."),
|
|
15142
|
+
planned(
|
|
15143
|
+
plan.aws.oidcProviderArn,
|
|
15144
|
+
"Would ensure GitHub Actions OIDC provider."
|
|
15145
|
+
),
|
|
15146
|
+
...plan.aws.stageRoles.map(
|
|
15147
|
+
(role) => planned(role.roleArn, `Would ensure deploy role for ${role.stage}.`)
|
|
15148
|
+
)
|
|
15149
|
+
);
|
|
15150
|
+
githubResults.push(
|
|
15151
|
+
planned(plan.targetDir, "Would write deployment repository files."),
|
|
15152
|
+
...plan.github.environments.flatMap((environment) => [
|
|
15153
|
+
planned(
|
|
15154
|
+
`${plan.repository}:${environment.stage}`,
|
|
15155
|
+
`Would ensure GitHub Environment ${environment.stage}.`
|
|
15156
|
+
),
|
|
15157
|
+
planned(
|
|
15158
|
+
`${plan.repository}:${environment.stage}:vars`,
|
|
15159
|
+
`Would upsert non-secret GitHub variables for ${environment.stage}.`
|
|
15160
|
+
),
|
|
15161
|
+
planned(
|
|
15162
|
+
`${plan.repository}:${environment.stage}:secrets`,
|
|
15163
|
+
`Would require GitHub Environment secrets for ${environment.stage}: ${environment.secretPlaceholders.join(", ")}.`
|
|
15164
|
+
),
|
|
15165
|
+
...plan.github.dispatchWorkflow ? [
|
|
15166
|
+
planned(
|
|
15167
|
+
`${plan.repository}:deploy.yml:${environment.stage}`,
|
|
15168
|
+
`Would dispatch deploy workflow for ${environment.stage}.`
|
|
15169
|
+
)
|
|
15170
|
+
] : []
|
|
15171
|
+
])
|
|
15172
|
+
);
|
|
15173
|
+
} else {
|
|
15174
|
+
awsResults.push(
|
|
15175
|
+
await awsClient.ensureStateBucket(plan.aws.stateBucket, plan.region),
|
|
15176
|
+
await awsClient.ensureLockTable(plan.aws.lockTable, plan.region),
|
|
15177
|
+
await awsClient.ensureArtifactBucket(
|
|
15178
|
+
plan.aws.artifactBucket,
|
|
15179
|
+
plan.region
|
|
15180
|
+
),
|
|
15181
|
+
await awsClient.ensureOidcProvider(plan.accountId)
|
|
15182
|
+
);
|
|
15183
|
+
for (const role of plan.aws.stageRoles) {
|
|
15184
|
+
awsResults.push(await awsClient.ensureDeployRole(role));
|
|
15185
|
+
}
|
|
15186
|
+
githubResults.push(await githubClient.writeRepositoryFiles(plan.targetDir));
|
|
15187
|
+
for (const environment of plan.github.environments) {
|
|
15188
|
+
githubResults.push(
|
|
15189
|
+
await githubClient.ensureEnvironment(environment),
|
|
15190
|
+
await githubClient.upsertEnvironmentVariables(environment),
|
|
15191
|
+
secretPlaceholderResult(plan.repository, environment)
|
|
15192
|
+
);
|
|
15193
|
+
if (plan.github.dispatchWorkflow) {
|
|
15194
|
+
githubResults.push(
|
|
15195
|
+
await githubClient.dispatchWorkflow(environment.stage)
|
|
15196
|
+
);
|
|
15197
|
+
}
|
|
15198
|
+
}
|
|
15199
|
+
}
|
|
15200
|
+
const metadata = options.dryRun ? planned(
|
|
15201
|
+
plan.customerSlug,
|
|
15202
|
+
"Would record local enterprise deployment metadata without secrets."
|
|
15203
|
+
) : recordDeploymentMetadata(plan, deps.saveDeployment);
|
|
15204
|
+
return {
|
|
15205
|
+
plan,
|
|
15206
|
+
template,
|
|
15207
|
+
aws: awsResults,
|
|
15208
|
+
github: githubResults,
|
|
15209
|
+
metadata
|
|
15210
|
+
};
|
|
15211
|
+
}
|
|
15212
|
+
function registerEnterpriseBootstrapCommand(program2) {
|
|
15213
|
+
program2.command("bootstrap [targetDir]").description(
|
|
15214
|
+
"Bootstrap a customer-owned ThinkWork deployment repository and CI trust bridge."
|
|
15215
|
+
).option("--customer <slug>", "Customer slug, e.g. acme").option("--repo <owner/name>", "Customer GitHub deployment repository").option("--stage <stage...>", "Deployment stage(s)", ["dev", "prod"]).option("--region <region>", "AWS region").option("--account-id <id>", "AWS account ID").option("--release-version <version>", "Pinned ThinkWork release version").option("--manifest-url <url>", "Pinned ThinkWork release manifest URL").option(
|
|
15216
|
+
"--manifest-sha256 <sha256>",
|
|
15217
|
+
"Pinned ThinkWork release manifest SHA-256"
|
|
15218
|
+
).option(
|
|
15219
|
+
"--terraform-module-version <version>",
|
|
15220
|
+
"Pinned Terraform Registry module version"
|
|
15221
|
+
).option(
|
|
15222
|
+
"--artifact-bucket <bucket>",
|
|
15223
|
+
"Customer-owned release artifact bucket"
|
|
15224
|
+
).option("--state-bucket <bucket>", "Terraform state bucket").option("--lock-table <table>", "Terraform state lock table").option("--dispatch", "Dispatch deploy workflow after bootstrap").option(
|
|
15225
|
+
"--dry-run",
|
|
15226
|
+
"Render files and print the plan without mutating AWS/GitHub"
|
|
15227
|
+
).option("-y, --yes", "Skip confirmation for mutating bootstrap").action(
|
|
15228
|
+
async (targetDir, opts) => {
|
|
15229
|
+
try {
|
|
15230
|
+
const customerSlug = await resolveCustomerSlug(opts.customer);
|
|
15231
|
+
const repository = await resolveRepository(opts.repo);
|
|
15232
|
+
const identity = getAwsIdentity();
|
|
15233
|
+
printHeader("enterprise bootstrap", customerSlug, identity);
|
|
15234
|
+
if (!opts.dryRun && !opts.yes) {
|
|
15235
|
+
const prodStages = (opts.stage ?? ["dev", "prod"]).filter(
|
|
15236
|
+
isProdLike
|
|
15237
|
+
);
|
|
15238
|
+
const message = prodStages.length > 0 ? ` Bootstrap deployment authority for ${repository} including production-like stage(s): ${prodStages.join(", ")}?` : ` Bootstrap deployment authority for ${repository}?`;
|
|
15239
|
+
if (!await confirm(message)) {
|
|
15240
|
+
console.log(" Aborted.");
|
|
15241
|
+
return;
|
|
15242
|
+
}
|
|
15243
|
+
}
|
|
15244
|
+
const result = await runEnterpriseBootstrap(
|
|
15245
|
+
{
|
|
15246
|
+
targetDir: targetDir ?? ".",
|
|
15247
|
+
customerSlug,
|
|
15248
|
+
repository,
|
|
15249
|
+
stages: opts.stage,
|
|
15250
|
+
region: opts.region,
|
|
15251
|
+
accountId: opts.accountId,
|
|
15252
|
+
releaseVersion: opts.releaseVersion,
|
|
15253
|
+
manifestUrl: opts.manifestUrl,
|
|
15254
|
+
manifestSha256: opts.manifestSha256,
|
|
15255
|
+
terraformModuleVersion: opts.terraformModuleVersion,
|
|
15256
|
+
artifactBucket: opts.artifactBucket,
|
|
15257
|
+
stateBucket: opts.stateBucket,
|
|
15258
|
+
lockTable: opts.lockTable,
|
|
15259
|
+
dispatchWorkflow: opts.dispatch,
|
|
15260
|
+
dryRun: opts.dryRun
|
|
15261
|
+
},
|
|
15262
|
+
{ identity }
|
|
15263
|
+
);
|
|
15264
|
+
printBootstrapSummary(result);
|
|
15265
|
+
printSuccess(
|
|
15266
|
+
opts.dryRun ? "Enterprise bootstrap dry-run complete" : "Enterprise bootstrap complete"
|
|
15267
|
+
);
|
|
15268
|
+
} catch (err) {
|
|
15269
|
+
printError(err.message);
|
|
15270
|
+
process.exit(1);
|
|
15271
|
+
}
|
|
15272
|
+
}
|
|
15273
|
+
);
|
|
15274
|
+
}
|
|
15275
|
+
function planned(target, message) {
|
|
15276
|
+
return { target, status: "planned", message };
|
|
15277
|
+
}
|
|
15278
|
+
function secretPlaceholderResult(repository, environment) {
|
|
15279
|
+
return {
|
|
15280
|
+
target: `${repository}:${environment.stage}:secrets`,
|
|
15281
|
+
status: "planned",
|
|
15282
|
+
message: `Set GitHub Environment secrets for ${environment.stage}: ${environment.secretPlaceholders.join(", ")}.`
|
|
15283
|
+
};
|
|
15284
|
+
}
|
|
15285
|
+
function recordDeploymentMetadata(plan, saveDeployment = saveEnterpriseDeployment) {
|
|
15286
|
+
saveDeployment({
|
|
15287
|
+
customerSlug: plan.customerSlug,
|
|
15288
|
+
repository: plan.repository,
|
|
15289
|
+
targetDir: plan.targetDir,
|
|
15290
|
+
accountId: plan.accountId,
|
|
15291
|
+
region: plan.region,
|
|
15292
|
+
stages: plan.stages,
|
|
15293
|
+
artifactBucket: plan.aws.artifactBucket,
|
|
15294
|
+
stateBucket: plan.aws.stateBucket,
|
|
15295
|
+
lockTable: plan.aws.lockTable,
|
|
15296
|
+
releaseVersion: plan.release.version,
|
|
15297
|
+
releaseManifestUrl: plan.release.manifestUrl,
|
|
15298
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15299
|
+
});
|
|
15300
|
+
return {
|
|
15301
|
+
target: plan.customerSlug,
|
|
15302
|
+
status: "updated",
|
|
15303
|
+
message: "Recorded local enterprise deployment metadata without secrets."
|
|
15304
|
+
};
|
|
15305
|
+
}
|
|
15306
|
+
async function resolveCustomerSlug(flag) {
|
|
15307
|
+
if (flag) return flag;
|
|
15308
|
+
if (!process.stdin.isTTY) {
|
|
15309
|
+
throw new Error("Customer slug is required. Pass --customer <slug>.");
|
|
15310
|
+
}
|
|
15311
|
+
const { input: input20 } = await import("@inquirer/prompts");
|
|
15312
|
+
return input20({ message: "Customer slug:" });
|
|
15313
|
+
}
|
|
15314
|
+
async function resolveRepository(flag) {
|
|
15315
|
+
if (flag) return flag;
|
|
15316
|
+
if (!process.stdin.isTTY) {
|
|
15317
|
+
throw new Error("GitHub repository is required. Pass --repo <owner/name>.");
|
|
15318
|
+
}
|
|
15319
|
+
const { input: input20 } = await import("@inquirer/prompts");
|
|
15320
|
+
return input20({ message: "GitHub deployment repo (owner/name):" });
|
|
15321
|
+
}
|
|
15322
|
+
function printBootstrapSummary(result) {
|
|
15323
|
+
console.log("");
|
|
15324
|
+
console.log(" AWS");
|
|
15325
|
+
for (const step of result.aws) {
|
|
15326
|
+
console.log(` - ${step.status}: ${step.message}`);
|
|
15327
|
+
}
|
|
15328
|
+
console.log(" GitHub");
|
|
15329
|
+
for (const step of result.github) {
|
|
15330
|
+
console.log(` - ${step.status}: ${step.message}`);
|
|
15331
|
+
}
|
|
15332
|
+
if (result.template.preserved.length > 0) {
|
|
15333
|
+
printWarning(
|
|
15334
|
+
`Preserved ${result.template.preserved.length} customer-owned file(s) without the ThinkWork managed marker.`
|
|
15335
|
+
);
|
|
15336
|
+
}
|
|
15337
|
+
}
|
|
15338
|
+
|
|
15339
|
+
// src/commands/enterprise.ts
|
|
15340
|
+
function registerEnterpriseCommand(program2) {
|
|
15341
|
+
const enterprise = program2.command("enterprise").description(
|
|
15342
|
+
"Bootstrap and operate customer-owned ThinkWork enterprise deployment repositories."
|
|
15343
|
+
);
|
|
15344
|
+
registerEnterpriseBootstrapCommand(enterprise);
|
|
15345
|
+
}
|
|
15346
|
+
|
|
14396
15347
|
// src/cli.ts
|
|
14397
15348
|
var program = new Command();
|
|
14398
15349
|
program.name("thinkwork").description(
|
|
@@ -14458,6 +15409,7 @@ registerPerformanceCommand(program);
|
|
|
14458
15409
|
registerTraceCommand(program);
|
|
14459
15410
|
registerDashboardCommand(program);
|
|
14460
15411
|
registerEvalCommand(program);
|
|
15412
|
+
registerEnterpriseCommand(program);
|
|
14461
15413
|
registerWikiCommand(program);
|
|
14462
15414
|
for (const cmd of program.commands) {
|
|
14463
15415
|
if (!cmd.options.some((o) => o.long === "--json")) {
|