@mycompbox/compbox-mcp 1.19.0 → 1.21.0
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/index.js +1185 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -178,6 +178,34 @@ var init_organization = __esm({
|
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
// ../shared/dist/types/github.js
|
|
182
|
+
var init_github = __esm({
|
|
183
|
+
"../shared/dist/types/github.js"() {
|
|
184
|
+
"use strict";
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ../shared/dist/types/cloud.js
|
|
189
|
+
var init_cloud = __esm({
|
|
190
|
+
"../shared/dist/types/cloud.js"() {
|
|
191
|
+
"use strict";
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ../shared/dist/types/stack.js
|
|
196
|
+
var init_stack = __esm({
|
|
197
|
+
"../shared/dist/types/stack.js"() {
|
|
198
|
+
"use strict";
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ../shared/dist/types/deployment.js
|
|
203
|
+
var init_deployment = __esm({
|
|
204
|
+
"../shared/dist/types/deployment.js"() {
|
|
205
|
+
"use strict";
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
181
209
|
// ../shared/dist/types/index.js
|
|
182
210
|
var init_types = __esm({
|
|
183
211
|
"../shared/dist/types/index.js"() {
|
|
@@ -190,6 +218,10 @@ var init_types = __esm({
|
|
|
190
218
|
init_interview();
|
|
191
219
|
init_attachment();
|
|
192
220
|
init_organization();
|
|
221
|
+
init_github();
|
|
222
|
+
init_cloud();
|
|
223
|
+
init_stack();
|
|
224
|
+
init_deployment();
|
|
193
225
|
}
|
|
194
226
|
});
|
|
195
227
|
|
|
@@ -529,7 +561,7 @@ function interviewFieldKeys() {
|
|
|
529
561
|
function buildInterviewSystemPrompt(definition = INTERVIEW_DEFINITION, seedContext) {
|
|
530
562
|
const requiredSections = definition.filter((s) => s.required);
|
|
531
563
|
const optionalSections = definition.filter((s) => !s.required);
|
|
532
|
-
const
|
|
564
|
+
const renderSection2 = (s) => {
|
|
533
565
|
const tag = s.required ? "REQUIRED" : "OPTIONAL \u2014 ask, but the user may decline";
|
|
534
566
|
const followUps = s.followUps.map((q) => ` - ${q}`).join("\n");
|
|
535
567
|
return `- ${s.title} (${tag}) [json keys: ${s.fields.join(", ")}]
|
|
@@ -547,10 +579,10 @@ ${followUps}`;
|
|
|
547
579
|
Work through these interview sections in order. Cover EVERY required section; for optional sections, ask about them too, but if the employee has nothing to add or declines, move on and leave that field out (never invent or pad it):
|
|
548
580
|
|
|
549
581
|
REQUIRED sections (an Epic Request cannot omit these):
|
|
550
|
-
${requiredSections.map(
|
|
582
|
+
${requiredSections.map(renderSection2).join("\n")}
|
|
551
583
|
|
|
552
584
|
OPTIONAL-BUT-RECOMMENDED sections (ask about each; omit the field if the employee has nothing):
|
|
553
|
-
${optionalSections.map(
|
|
585
|
+
${optionalSections.map(renderSection2).join("\n")}
|
|
554
586
|
|
|
555
587
|
The employee's answers may be short or casual \u2014 that's fine, but when you write the final fields below, EXPAND them into complete, well-written prose using everything they told you across the whole conversation, not just a compressed one-liner. Each required field is scored by an automated quality rubric with hard minimums, so undershooting length is a real failure, not just a style nitpick:
|
|
556
588
|
- problemStatement: at least ${String(INTERVIEW_FIELD_MIN_WORDS.problemStatement)} words.
|
|
@@ -830,13 +862,1118 @@ var init_interview_completion = __esm({
|
|
|
830
862
|
}
|
|
831
863
|
});
|
|
832
864
|
|
|
865
|
+
// ../shared/dist/interview-conversation.js
|
|
866
|
+
function renderSection(s) {
|
|
867
|
+
const tag = s.required ? "needed" : "nice to have";
|
|
868
|
+
return `- ${s.title} (${tag}): ${s.purpose} For example: ${s.followUps.slice(0, 2).join(" ")}`;
|
|
869
|
+
}
|
|
870
|
+
function renderSaved(saved) {
|
|
871
|
+
const fieldLines = Object.entries(saved.fields).filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0).map(([key, value]) => `${key}: ${value}`).join("\n");
|
|
872
|
+
const score = saved.compliance ? `Its quality score is ${String(saved.compliance.overall)}/100 (${saved.compliance.tier}).` : "It has no quality score yet.";
|
|
873
|
+
const issues = saved.compliance && saved.compliance.issues.length > 0 ? `
|
|
874
|
+
What the score says is missing or weak:
|
|
875
|
+
${saved.compliance.issues.slice(0, 8).map((i) => `- ${i.message}`).join("\n")}` : "";
|
|
876
|
+
const locked = saved.status === "pending" ? "It can still be changed." : `Its status is "${saved.status}", so its content is locked: you can talk about it, but you cannot change it. Never emit the completion JSON for a locked request.`;
|
|
877
|
+
return `THE REQUEST IS ALREADY SAVED. This conversation created the request "${saved.title}". ${score} ${locked}${issues}
|
|
878
|
+
|
|
879
|
+
Its current content:
|
|
880
|
+
title: ${saved.title}
|
|
881
|
+
${fieldLines}
|
|
882
|
+
description: ${saved.description}
|
|
883
|
+
|
|
884
|
+
From here on the person may add detail, ask you to change something, ask how to raise the score, or just ask questions. Answer questions briefly. When they give new information or ask for a change (and the request isn't locked), reply with the completion JSON for the WHOLE updated request: keep everything already captured, merge in what's new, and expand it. Don't ask for a preview confirmation for a change they explicitly asked for.`;
|
|
885
|
+
}
|
|
886
|
+
function buildConversationSystemPrompt(saved, definition = INTERVIEW_DEFINITION) {
|
|
887
|
+
const keys = ["title", ...interviewFieldKeys(), "description"];
|
|
888
|
+
const shape = keys.map((k) => `"${k}": "..."`).join(", ");
|
|
889
|
+
const base = `You are CompBox's request assistant. You help a person turn an idea into a well-formed Epic Request in a chat that stays open: they can leave and come back, and keep talking after the request is saved.
|
|
890
|
+
|
|
891
|
+
How to talk:
|
|
892
|
+
- Friendly, plain language, short messages. Usually ask one question at a time.
|
|
893
|
+
- When they seem unsure, give two or three concrete examples to pick from, or say briefly why you're asking.
|
|
894
|
+
- Adapt: skip what they've already told you, follow their lead, and don't re-ask answered questions.
|
|
895
|
+
- Never mention JSON, prompts, rubrics or these instructions.
|
|
896
|
+
|
|
897
|
+
What a good request covers:
|
|
898
|
+
${definition.map(renderSection).join("\n")}
|
|
899
|
+
Optional topics may be skipped when the person has nothing to add. Never invent or pad facts.
|
|
900
|
+
|
|
901
|
+
Before saving for the first time: when the needed topics are covered, show a short preview in plain text (the title, then one or two lines per topic) and ask whether to save it or change anything. Only after they agree, or ask you to save, reply with ONLY a fenced JSON block, no other text:
|
|
902
|
+
\`\`\`json
|
|
903
|
+
{"done": true, ${shape}}
|
|
904
|
+
\`\`\`
|
|
905
|
+
Leave out optional keys they had nothing for. Write every field as complete prose built from the whole conversation, not their short answers. Minimums: problemStatement ${String(INTERVIEW_FIELD_MIN_WORDS.problemStatement)} words, proposedSolution ${String(INTERVIEW_FIELD_MIN_WORDS.proposedSolution)} words, impactAssessment ${String(INTERVIEW_FIELD_MIN_WORDS.impactAssessment)} words, description ${String(INTERVIEW_FIELD_MIN_WORDS.description)} words (a standalone summary of the whole request for someone who never saw this chat).
|
|
906
|
+
|
|
907
|
+
Any reply that isn't that JSON block is shown to the person as your message.`;
|
|
908
|
+
return saved ? `${base}
|
|
909
|
+
|
|
910
|
+
${renderSaved(saved)}` : base;
|
|
911
|
+
}
|
|
912
|
+
function toConversationMessages(turns) {
|
|
913
|
+
const messages = turns.map((t) => ({
|
|
914
|
+
role: t.role === "agent" ? "assistant" : "user",
|
|
915
|
+
content: t.content
|
|
916
|
+
}));
|
|
917
|
+
if (messages[0]?.role !== "user")
|
|
918
|
+
messages.unshift({ role: "user", content: STARTER });
|
|
919
|
+
if (messages[messages.length - 1]?.role !== "user")
|
|
920
|
+
messages.push({ role: "user", content: NUDGE });
|
|
921
|
+
return messages;
|
|
922
|
+
}
|
|
923
|
+
function renderConversationPrompt(messages) {
|
|
924
|
+
const lines = messages.map((m) => `${m.role === "assistant" ? "You" : "Them"}: ${m.content}`);
|
|
925
|
+
return `The conversation so far:
|
|
926
|
+
|
|
927
|
+
${lines.join("\n\n")}
|
|
928
|
+
|
|
929
|
+
Write your next reply to them.`;
|
|
930
|
+
}
|
|
931
|
+
function savedMessage(saved, created, followUp) {
|
|
932
|
+
const verb = created ? "Saved your request" : "Updated your request";
|
|
933
|
+
const c = saved.compliance;
|
|
934
|
+
let text;
|
|
935
|
+
if (!c) {
|
|
936
|
+
text = `${verb} "${saved.title}". You can keep talking here to add detail or change anything.`;
|
|
937
|
+
} else if (meetsComplianceTarget(c)) {
|
|
938
|
+
text = `${verb} "${saved.title}". It scores ${String(c.overall)}/100, ready for review. You can keep talking here to change anything.`;
|
|
939
|
+
} else {
|
|
940
|
+
const gaps = [...c.issues.filter((i) => i.tier === "critical"), ...c.issues.filter((i) => i.tier === "advisory")].slice(0, 4).map((i) => `- ${i.message}`).join("\n");
|
|
941
|
+
text = `${verb} "${saved.title}". It scores ${String(c.overall)}/100; 90 is the target.${gaps ? ` To raise it:
|
|
942
|
+
${gaps}` : ""}
|
|
943
|
+
|
|
944
|
+
Tell me more here and I'll update it, or leave it as it is.`;
|
|
945
|
+
}
|
|
946
|
+
return followUp ? `${text}
|
|
947
|
+
|
|
948
|
+
${followUp}` : text;
|
|
949
|
+
}
|
|
950
|
+
async function runConversationStep(input, deps) {
|
|
951
|
+
const system = buildConversationSystemPrompt(input.saved);
|
|
952
|
+
const messages = toConversationMessages(input.turns);
|
|
953
|
+
const locked = input.saved !== null && input.saved.status !== "pending";
|
|
954
|
+
let requestId = input.saved?.id ?? null;
|
|
955
|
+
let savedNow = null;
|
|
956
|
+
let createdHere = false;
|
|
957
|
+
let qualityRounds = 0;
|
|
958
|
+
let repairs = 0;
|
|
959
|
+
for (; ; ) {
|
|
960
|
+
let reply;
|
|
961
|
+
try {
|
|
962
|
+
reply = await deps.reply(system, messages);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
if (savedNow)
|
|
965
|
+
return { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created), ...savedNow };
|
|
966
|
+
const reason = error instanceof Error ? error.message : "Something went wrong reaching the AI.";
|
|
967
|
+
return {
|
|
968
|
+
kind: "error",
|
|
969
|
+
text: `I couldn't reply just now: ${reason}
|
|
970
|
+
|
|
971
|
+
Send another message (for example "try again") when you're ready and I'll pick up where we left off.`
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
const draft = extractCompletionJson(reply);
|
|
975
|
+
if (draft) {
|
|
976
|
+
if (locked) {
|
|
977
|
+
messages.push({ role: "assistant", content: reply }, { role: "user", content: "That request is locked and can't be changed. Reply to me in plain text instead." });
|
|
978
|
+
if (++repairs > JSON_REPAIRS)
|
|
979
|
+
return { kind: "error", text: "This request is locked, so it can't be changed any more." };
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
let saved;
|
|
983
|
+
try {
|
|
984
|
+
saved = await deps.save(requestId, draft);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
const reason = error instanceof Error ? error.message : "the save failed";
|
|
987
|
+
return { kind: "error", text: `I couldn't save the request: ${reason}
|
|
988
|
+
|
|
989
|
+
Reply "try again" and I'll retry.` };
|
|
990
|
+
}
|
|
991
|
+
createdHere ||= requestId === null;
|
|
992
|
+
savedNow = { saved, created: createdHere };
|
|
993
|
+
requestId = saved.id;
|
|
994
|
+
if (meetsComplianceTarget(saved.compliance) || qualityRounds >= CONVERSATION_QUALITY_ROUNDS || !saved.compliance) {
|
|
995
|
+
return { kind: "saved", text: savedMessage(saved, savedNow.created), ...savedNow };
|
|
996
|
+
}
|
|
997
|
+
qualityRounds++;
|
|
998
|
+
messages.push({ role: "assistant", content: reply }, {
|
|
999
|
+
role: "user",
|
|
1000
|
+
content: `${buildAuditFeedback(saved.compliance)}
|
|
1001
|
+
|
|
1002
|
+
The request IS already saved, and the person will be shown its score. If you ask them something, ask it as a way to improve the saved request; never say it hasn't been saved.`
|
|
1003
|
+
});
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (looksLikeCompletionAttempt(reply)) {
|
|
1007
|
+
if (++repairs > JSON_REPAIRS) {
|
|
1008
|
+
return savedNow ? { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created), ...savedNow } : { kind: "error", text: `I had trouble writing up the request. Reply "try again" and I'll have another go.` };
|
|
1009
|
+
}
|
|
1010
|
+
messages.push({ role: "assistant", content: reply }, { role: "user", content: COMPLETION_REPAIR_INSTRUCTION });
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const text = reply.trim();
|
|
1014
|
+
if (savedNow) {
|
|
1015
|
+
return { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created, text || void 0), ...savedNow };
|
|
1016
|
+
}
|
|
1017
|
+
if (!text)
|
|
1018
|
+
return { kind: "error", text: "I didn't get a reply that time. Send another message and I'll try again." };
|
|
1019
|
+
return { kind: "message", text };
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function savedContextFromRecord(record) {
|
|
1023
|
+
if (typeof record !== "object" || record === null)
|
|
1024
|
+
return null;
|
|
1025
|
+
const r = record;
|
|
1026
|
+
if (typeof r.id !== "string" || typeof r.title !== "string")
|
|
1027
|
+
return null;
|
|
1028
|
+
let structured = r.structuredDesc;
|
|
1029
|
+
if (typeof structured === "string") {
|
|
1030
|
+
try {
|
|
1031
|
+
structured = JSON.parse(structured);
|
|
1032
|
+
} catch {
|
|
1033
|
+
structured = null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
const fields = {};
|
|
1037
|
+
if (typeof structured === "object" && structured !== null) {
|
|
1038
|
+
for (const [key, value] of Object.entries(structured)) {
|
|
1039
|
+
if (typeof value === "string")
|
|
1040
|
+
fields[key] = value;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
let score = r.complianceScore;
|
|
1044
|
+
if (typeof score === "string") {
|
|
1045
|
+
try {
|
|
1046
|
+
score = JSON.parse(score);
|
|
1047
|
+
} catch {
|
|
1048
|
+
score = null;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
id: r.id,
|
|
1053
|
+
title: r.title,
|
|
1054
|
+
status: typeof r.status === "string" ? r.status : "pending",
|
|
1055
|
+
description: typeof r.description === "string" ? r.description : "",
|
|
1056
|
+
fields,
|
|
1057
|
+
compliance: complianceFromRecord(score)
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function complianceFromRecord(score) {
|
|
1061
|
+
if (typeof score !== "object" || score === null)
|
|
1062
|
+
return null;
|
|
1063
|
+
const s = score;
|
|
1064
|
+
if (typeof s.overall !== "number" || typeof s.tier !== "string")
|
|
1065
|
+
return null;
|
|
1066
|
+
const issues = Array.isArray(s.issues) ? s.issues.filter((i) => typeof i === "object" && i !== null).map((i) => ({
|
|
1067
|
+
message: typeof i.message === "string" ? i.message : typeof i.code === "string" ? i.code : "issue",
|
|
1068
|
+
tier: i.tier === "critical" ? "critical" : "advisory"
|
|
1069
|
+
})) : [];
|
|
1070
|
+
return { overall: s.overall, tier: s.tier, issues };
|
|
1071
|
+
}
|
|
1072
|
+
var CONVERSATION_QUALITY_ROUNDS, JSON_REPAIRS, STARTER, NUDGE;
|
|
1073
|
+
var init_interview_conversation = __esm({
|
|
1074
|
+
"../shared/dist/interview-conversation.js"() {
|
|
1075
|
+
"use strict";
|
|
1076
|
+
init_interview_definition();
|
|
1077
|
+
init_interview_completion();
|
|
1078
|
+
CONVERSATION_QUALITY_ROUNDS = 2;
|
|
1079
|
+
JSON_REPAIRS = 3;
|
|
1080
|
+
STARTER = "Hi! I'd like to propose something.";
|
|
1081
|
+
NUDGE = "(Please continue.)";
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
// ../shared/dist/cloud-setup.js
|
|
1086
|
+
function isGcpProjectId(value) {
|
|
1087
|
+
return PROJECT_ID.test(value);
|
|
1088
|
+
}
|
|
1089
|
+
function isGcpProjectNumber(value) {
|
|
1090
|
+
return PROJECT_NUMBER.test(value);
|
|
1091
|
+
}
|
|
1092
|
+
function isGcpRegion(value) {
|
|
1093
|
+
return GCP_REGIONS.includes(value);
|
|
1094
|
+
}
|
|
1095
|
+
function isGitHubLogin(value) {
|
|
1096
|
+
return GITHUB_LOGIN.test(value);
|
|
1097
|
+
}
|
|
1098
|
+
function gcpServiceAccountEmail(projectId) {
|
|
1099
|
+
return `${GCP_DEPLOY_ACCOUNT}@${projectId}.iam.gserviceaccount.com`;
|
|
1100
|
+
}
|
|
1101
|
+
function gcpWorkloadProvider(projectNumber) {
|
|
1102
|
+
return `projects/${projectNumber}/locations/global/workloadIdentityPools/${GCP_IDENTITY_POOL}/providers/${GCP_IDENTITY_PROVIDER}`;
|
|
1103
|
+
}
|
|
1104
|
+
function gcpStateBucket(projectId) {
|
|
1105
|
+
return `${projectId}-compbox-tfstate`;
|
|
1106
|
+
}
|
|
1107
|
+
function buildGcpSetupScript(input) {
|
|
1108
|
+
if (!isGcpProjectId(input.projectId))
|
|
1109
|
+
throw new Error("Not a Google Cloud project ID.");
|
|
1110
|
+
if (!isGcpRegion(input.region))
|
|
1111
|
+
throw new Error("Not a supported region.");
|
|
1112
|
+
if (input.githubOwners.length === 0)
|
|
1113
|
+
throw new Error("Connect GitHub first.");
|
|
1114
|
+
for (const owner of input.githubOwners) {
|
|
1115
|
+
if (!isGitHubLogin(owner))
|
|
1116
|
+
throw new Error(`Not a GitHub account name: ${owner}`);
|
|
1117
|
+
}
|
|
1118
|
+
const owners = [...new Set(input.githubOwners)];
|
|
1119
|
+
const condition = `assertion.repository_owner in [${owners.map((o) => `'${o}'`).join(", ")}]`;
|
|
1120
|
+
const pool = `--location=global --workload-identity-pool=${GCP_IDENTITY_POOL}`;
|
|
1121
|
+
const oidc = [
|
|
1122
|
+
"--issuer-uri=https://token.actions.githubusercontent.com",
|
|
1123
|
+
'--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner"',
|
|
1124
|
+
`--attribute-condition="${condition}"`
|
|
1125
|
+
].join(" ");
|
|
1126
|
+
return `# CompBox \u2192 Google Cloud (keyless). Paste into Google Cloud Shell.
|
|
1127
|
+
set -euo pipefail
|
|
1128
|
+
PROJECT=${input.projectId}
|
|
1129
|
+
REGION=${input.region}
|
|
1130
|
+
SA=${gcpServiceAccountEmail(input.projectId)}
|
|
1131
|
+
|
|
1132
|
+
gcloud config set project "$PROJECT"
|
|
1133
|
+
gcloud services enable run.googleapis.com artifactregistry.googleapis.com iam.googleapis.com \\
|
|
1134
|
+
iamcredentials.googleapis.com sts.googleapis.com cloudresourcemanager.googleapis.com
|
|
1135
|
+
NUMBER=$(gcloud projects describe "$PROJECT" --format='value(projectNumber)')
|
|
1136
|
+
|
|
1137
|
+
# The account deploys run as.
|
|
1138
|
+
gcloud iam service-accounts describe "$SA" >/dev/null 2>&1 || \\
|
|
1139
|
+
gcloud iam service-accounts create ${GCP_DEPLOY_ACCOUNT} --display-name="CompBox deploys"
|
|
1140
|
+
for ROLE in ${GCP_DEPLOY_ROLES.join(" ")}; do
|
|
1141
|
+
gcloud projects add-iam-policy-binding "$PROJECT" --member="serviceAccount:$SA" --role="$ROLE" --condition=None --quiet >/dev/null
|
|
1142
|
+
done
|
|
1143
|
+
|
|
1144
|
+
# Trust GitHub Actions from your GitHub only: ${owners.join(", ")}.
|
|
1145
|
+
gcloud iam workload-identity-pools describe ${GCP_IDENTITY_POOL} --location=global >/dev/null 2>&1 || \\
|
|
1146
|
+
gcloud iam workload-identity-pools create ${GCP_IDENTITY_POOL} --location=global --display-name="CompBox"
|
|
1147
|
+
if gcloud iam workload-identity-pools providers describe ${GCP_IDENTITY_PROVIDER} ${pool} >/dev/null 2>&1; then
|
|
1148
|
+
gcloud iam workload-identity-pools providers update-oidc ${GCP_IDENTITY_PROVIDER} ${pool} ${oidc}
|
|
1149
|
+
else
|
|
1150
|
+
gcloud iam workload-identity-pools providers create-oidc ${GCP_IDENTITY_PROVIDER} ${pool} ${oidc}
|
|
1151
|
+
fi
|
|
1152
|
+
for OWNER in ${owners.join(" ")}; do
|
|
1153
|
+
gcloud iam service-accounts add-iam-policy-binding "$SA" --role=roles/iam.workloadIdentityUser --quiet >/dev/null \\
|
|
1154
|
+
--member="principalSet://iam.googleapis.com/projects/$NUMBER/locations/global/workloadIdentityPools/${GCP_IDENTITY_POOL}/attribute.repository_owner/$OWNER"
|
|
1155
|
+
done
|
|
1156
|
+
|
|
1157
|
+
# Where each project's infrastructure state is kept.
|
|
1158
|
+
gcloud storage buckets describe "gs://${gcpStateBucket(input.projectId)}" >/dev/null 2>&1 || \\
|
|
1159
|
+
gcloud storage buckets create "gs://${gcpStateBucket(input.projectId)}" --location="$REGION" --uniform-bucket-level-access
|
|
1160
|
+
|
|
1161
|
+
echo
|
|
1162
|
+
echo "Done. Your project number is: $NUMBER"
|
|
1163
|
+
echo "Paste it into CompBox to finish."
|
|
1164
|
+
`;
|
|
1165
|
+
}
|
|
1166
|
+
var GCP_REGIONS, GCP_DEPLOY_ACCOUNT, GCP_IDENTITY_POOL, GCP_IDENTITY_PROVIDER, GCP_DEPLOY_ROLES, PROJECT_ID, PROJECT_NUMBER, GITHUB_LOGIN, GCP_CONNECT_STEPS, GCP_CONNECT_STEP_LABELS;
|
|
1167
|
+
var init_cloud_setup = __esm({
|
|
1168
|
+
"../shared/dist/cloud-setup.js"() {
|
|
1169
|
+
"use strict";
|
|
1170
|
+
GCP_REGIONS = [
|
|
1171
|
+
"us-central1",
|
|
1172
|
+
"us-east1",
|
|
1173
|
+
"us-east4",
|
|
1174
|
+
"us-west1",
|
|
1175
|
+
"us-west2",
|
|
1176
|
+
"northamerica-northeast1",
|
|
1177
|
+
"europe-west1",
|
|
1178
|
+
"europe-west2",
|
|
1179
|
+
"europe-west3",
|
|
1180
|
+
"europe-west4",
|
|
1181
|
+
"asia-south1",
|
|
1182
|
+
"asia-southeast1",
|
|
1183
|
+
"asia-northeast1",
|
|
1184
|
+
"australia-southeast1"
|
|
1185
|
+
];
|
|
1186
|
+
GCP_DEPLOY_ACCOUNT = "compbox-deploy";
|
|
1187
|
+
GCP_IDENTITY_POOL = "compbox";
|
|
1188
|
+
GCP_IDENTITY_PROVIDER = "github";
|
|
1189
|
+
GCP_DEPLOY_ROLES = [
|
|
1190
|
+
"roles/run.admin",
|
|
1191
|
+
"roles/artifactregistry.admin",
|
|
1192
|
+
"roles/iam.serviceAccountUser",
|
|
1193
|
+
"roles/iam.serviceAccountAdmin",
|
|
1194
|
+
"roles/storage.admin",
|
|
1195
|
+
"roles/serviceusage.serviceUsageConsumer"
|
|
1196
|
+
];
|
|
1197
|
+
PROJECT_ID = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
|
|
1198
|
+
PROJECT_NUMBER = /^\d{6,20}$/;
|
|
1199
|
+
GITHUB_LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
1200
|
+
GCP_CONNECT_STEPS = ["project", "services", "account", "identity", "trust", "storage"];
|
|
1201
|
+
GCP_CONNECT_STEP_LABELS = {
|
|
1202
|
+
project: "Checking your project",
|
|
1203
|
+
services: "Turning on Google Cloud services",
|
|
1204
|
+
account: "Creating the deploy account",
|
|
1205
|
+
identity: "Trusting your GitHub",
|
|
1206
|
+
trust: "Letting your GitHub deploy",
|
|
1207
|
+
storage: "Making a place for deploy state"
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
// ../shared/dist/cloud-connect.js
|
|
1213
|
+
function isAwsAccountId(value) {
|
|
1214
|
+
return AWS_ACCOUNT.test(value);
|
|
1215
|
+
}
|
|
1216
|
+
function isAwsRegion(value) {
|
|
1217
|
+
return AWS_REGION.test(value);
|
|
1218
|
+
}
|
|
1219
|
+
function isAwsRoleArn(value) {
|
|
1220
|
+
return AWS_ROLE_ARN.test(value);
|
|
1221
|
+
}
|
|
1222
|
+
function isS3BucketName(value) {
|
|
1223
|
+
return S3_BUCKET.test(value);
|
|
1224
|
+
}
|
|
1225
|
+
function buildAwsConnectTemplate() {
|
|
1226
|
+
const sub = (s) => ({ "Fn::Sub": s });
|
|
1227
|
+
return {
|
|
1228
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
1229
|
+
Description: "CompBox keyless deploys: GitHub Actions from your GitHub accounts deploy as a role. No keys are stored.",
|
|
1230
|
+
Parameters: {
|
|
1231
|
+
GitHubOwners: { Type: "CommaDelimitedList", Description: "GitHub accounts whose Actions may deploy." },
|
|
1232
|
+
CompBoxConnectUrl: { Type: "String", Description: "Where the stack tells CompBox it's done." },
|
|
1233
|
+
CompBoxConnectToken: { Type: "String", NoEcho: true, Description: "One-time link from CompBox." }
|
|
1234
|
+
},
|
|
1235
|
+
Resources: {
|
|
1236
|
+
SetupFunctionRole: {
|
|
1237
|
+
Type: "AWS::IAM::Role",
|
|
1238
|
+
Properties: {
|
|
1239
|
+
AssumeRolePolicyDocument: {
|
|
1240
|
+
Version: "2012-10-17",
|
|
1241
|
+
Statement: [{ Effect: "Allow", Principal: { Service: "lambda.amazonaws.com" }, Action: "sts:AssumeRole" }]
|
|
1242
|
+
},
|
|
1243
|
+
ManagedPolicyArns: [sub("arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole")],
|
|
1244
|
+
Policies: [
|
|
1245
|
+
{
|
|
1246
|
+
PolicyName: "github-oidc",
|
|
1247
|
+
PolicyDocument: {
|
|
1248
|
+
Version: "2012-10-17",
|
|
1249
|
+
Statement: [
|
|
1250
|
+
{
|
|
1251
|
+
Effect: "Allow",
|
|
1252
|
+
Action: [
|
|
1253
|
+
"iam:ListOpenIDConnectProviders",
|
|
1254
|
+
"iam:GetOpenIDConnectProvider",
|
|
1255
|
+
"iam:CreateOpenIDConnectProvider",
|
|
1256
|
+
"iam:AddClientIDToOpenIDConnectProvider"
|
|
1257
|
+
],
|
|
1258
|
+
Resource: "*"
|
|
1259
|
+
}
|
|
1260
|
+
]
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
]
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
SetupFunction: {
|
|
1267
|
+
Type: "AWS::Lambda::Function",
|
|
1268
|
+
Properties: {
|
|
1269
|
+
Runtime: "python3.12",
|
|
1270
|
+
Handler: "index.handler",
|
|
1271
|
+
Timeout: 60,
|
|
1272
|
+
Role: { "Fn::GetAtt": ["SetupFunctionRole", "Arn"] },
|
|
1273
|
+
Code: { ZipFile: AWS_SETUP_FUNCTION }
|
|
1274
|
+
}
|
|
1275
|
+
},
|
|
1276
|
+
GitHubOidc: {
|
|
1277
|
+
Type: "Custom::GitHubOidc",
|
|
1278
|
+
Properties: { ServiceToken: { "Fn::GetAtt": ["SetupFunction", "Arn"] }, Action: "oidc" }
|
|
1279
|
+
},
|
|
1280
|
+
StateBucket: {
|
|
1281
|
+
Type: "AWS::S3::Bucket",
|
|
1282
|
+
DeletionPolicy: "Retain",
|
|
1283
|
+
UpdateReplacePolicy: "Retain",
|
|
1284
|
+
Properties: {
|
|
1285
|
+
VersioningConfiguration: { Status: "Enabled" },
|
|
1286
|
+
PublicAccessBlockConfiguration: {
|
|
1287
|
+
BlockPublicAcls: true,
|
|
1288
|
+
BlockPublicPolicy: true,
|
|
1289
|
+
IgnorePublicAcls: true,
|
|
1290
|
+
RestrictPublicBuckets: true
|
|
1291
|
+
},
|
|
1292
|
+
BucketEncryption: { ServerSideEncryptionConfiguration: [{ ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } }] }
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1295
|
+
DeployRole: {
|
|
1296
|
+
Type: "AWS::IAM::Role",
|
|
1297
|
+
Properties: {
|
|
1298
|
+
Description: "CompBox deploys (GitHub Actions, keyless)",
|
|
1299
|
+
AssumeRolePolicyDocument: {
|
|
1300
|
+
Version: "2012-10-17",
|
|
1301
|
+
Statement: [
|
|
1302
|
+
{
|
|
1303
|
+
Effect: "Allow",
|
|
1304
|
+
Principal: { Federated: { "Fn::GetAtt": ["GitHubOidc", "Arn"] } },
|
|
1305
|
+
Action: "sts:AssumeRoleWithWebIdentity",
|
|
1306
|
+
Condition: {
|
|
1307
|
+
StringEquals: { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
|
|
1308
|
+
// repo:<owner>/* for every owner.
|
|
1309
|
+
StringLike: {
|
|
1310
|
+
"token.actions.githubusercontent.com:sub": {
|
|
1311
|
+
"Fn::Split": [",", { "Fn::Sub": ["repo:${Owners}/*", { Owners: { "Fn::Join": ["/*,repo:", { Ref: "GitHubOwners" }] } }] }]
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
]
|
|
1317
|
+
},
|
|
1318
|
+
ManagedPolicyArns: [
|
|
1319
|
+
sub("arn:${AWS::Partition}:iam::aws:policy/AWSAppRunnerFullAccess"),
|
|
1320
|
+
sub("arn:${AWS::Partition}:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser")
|
|
1321
|
+
],
|
|
1322
|
+
Policies: [
|
|
1323
|
+
{
|
|
1324
|
+
PolicyName: "compbox-deploy",
|
|
1325
|
+
PolicyDocument: {
|
|
1326
|
+
Version: "2012-10-17",
|
|
1327
|
+
Statement: [
|
|
1328
|
+
{
|
|
1329
|
+
Sid: "State",
|
|
1330
|
+
Effect: "Allow",
|
|
1331
|
+
Action: ["s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
|
|
1332
|
+
Resource: [{ "Fn::GetAtt": ["StateBucket", "Arn"] }, sub("${StateBucket.Arn}/*")]
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
Sid: "AppRoles",
|
|
1336
|
+
Effect: "Allow",
|
|
1337
|
+
Action: [
|
|
1338
|
+
"iam:GetRole",
|
|
1339
|
+
"iam:PassRole",
|
|
1340
|
+
"iam:CreateRole",
|
|
1341
|
+
"iam:DeleteRole",
|
|
1342
|
+
"iam:TagRole",
|
|
1343
|
+
"iam:AttachRolePolicy",
|
|
1344
|
+
"iam:DetachRolePolicy",
|
|
1345
|
+
"iam:PutRolePolicy",
|
|
1346
|
+
"iam:DeleteRolePolicy",
|
|
1347
|
+
"iam:ListAttachedRolePolicies",
|
|
1348
|
+
"iam:ListRolePolicies",
|
|
1349
|
+
"iam:GetRolePolicy"
|
|
1350
|
+
],
|
|
1351
|
+
Resource: sub("arn:${AWS::Partition}:iam::${AWS::AccountId}:role/compbox-*")
|
|
1352
|
+
}
|
|
1353
|
+
]
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
]
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
Connect: {
|
|
1360
|
+
Type: "Custom::CompBoxConnect",
|
|
1361
|
+
Properties: {
|
|
1362
|
+
ServiceToken: { "Fn::GetAtt": ["SetupFunction", "Arn"] },
|
|
1363
|
+
Action: "notify",
|
|
1364
|
+
Url: { Ref: "CompBoxConnectUrl" },
|
|
1365
|
+
Token: { Ref: "CompBoxConnectToken" },
|
|
1366
|
+
AccountId: { Ref: "AWS::AccountId" },
|
|
1367
|
+
Region: { Ref: "AWS::Region" },
|
|
1368
|
+
RoleArn: { "Fn::GetAtt": ["DeployRole", "Arn"] },
|
|
1369
|
+
Bucket: { Ref: "StateBucket" },
|
|
1370
|
+
// Re-tell CompBox when the GitHub accounts change.
|
|
1371
|
+
Owners: { "Fn::Join": [",", { Ref: "GitHubOwners" }] }
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
},
|
|
1375
|
+
Outputs: {
|
|
1376
|
+
DeployRoleArn: { Value: { "Fn::GetAtt": ["DeployRole", "Arn"] } },
|
|
1377
|
+
StateBucket: { Value: { Ref: "StateBucket" } }
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
function awsQuickCreateUrl(input) {
|
|
1382
|
+
if (!isAwsRegion(input.region))
|
|
1383
|
+
throw new Error("Not an AWS region.");
|
|
1384
|
+
const owners = uniqueOwners(input.githubOwners);
|
|
1385
|
+
const params = new URLSearchParams({
|
|
1386
|
+
templateURL: input.templateUrl,
|
|
1387
|
+
stackName: "CompBox",
|
|
1388
|
+
param_GitHubOwners: owners.join(","),
|
|
1389
|
+
param_CompBoxConnectUrl: input.connectUrl,
|
|
1390
|
+
param_CompBoxConnectToken: input.connectToken
|
|
1391
|
+
});
|
|
1392
|
+
return `https://${input.region}.console.aws.amazon.com/cloudformation/home?region=${input.region}#/stacks/quickcreate?${params.toString()}`;
|
|
1393
|
+
}
|
|
1394
|
+
function isAzureGuid(value) {
|
|
1395
|
+
return GUID.test(value);
|
|
1396
|
+
}
|
|
1397
|
+
function isAzureResourceGroup(value) {
|
|
1398
|
+
return RESOURCE_GROUP.test(value);
|
|
1399
|
+
}
|
|
1400
|
+
function isAzureLocation(value) {
|
|
1401
|
+
return AZURE_LOCATION.test(value);
|
|
1402
|
+
}
|
|
1403
|
+
function isAzureStorageAccount(value) {
|
|
1404
|
+
return STORAGE_ACCOUNT.test(value);
|
|
1405
|
+
}
|
|
1406
|
+
function azureGitHubSubject(owner) {
|
|
1407
|
+
return `repository_owner:${owner}`;
|
|
1408
|
+
}
|
|
1409
|
+
function buildAzureConnectTemplate(input) {
|
|
1410
|
+
const owners = uniqueOwners(input.githubOwners);
|
|
1411
|
+
const identityId = `[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', '${AZURE_DEPLOY_IDENTITY}')]`;
|
|
1412
|
+
const identityRef = `reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', '${AZURE_DEPLOY_IDENTITY}'), '2023-01-31')`;
|
|
1413
|
+
return {
|
|
1414
|
+
$schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
|
1415
|
+
contentVersion: "1.0.0.0",
|
|
1416
|
+
metadata: { description: "CompBox keyless deploys: GitHub Actions from your GitHub accounts deploy as a managed identity." },
|
|
1417
|
+
parameters: {
|
|
1418
|
+
location: { type: "string", defaultValue: "[resourceGroup().location]" },
|
|
1419
|
+
githubOwners: { type: "array", defaultValue: owners },
|
|
1420
|
+
connectUrl: { type: "string", defaultValue: input.connectUrl }
|
|
1421
|
+
},
|
|
1422
|
+
variables: {
|
|
1423
|
+
storageName: "[concat('cbstate', uniqueString(resourceGroup().id))]"
|
|
1424
|
+
},
|
|
1425
|
+
resources: [
|
|
1426
|
+
{
|
|
1427
|
+
type: "Microsoft.ManagedIdentity/userAssignedIdentities",
|
|
1428
|
+
apiVersion: "2023-01-31",
|
|
1429
|
+
name: AZURE_DEPLOY_IDENTITY,
|
|
1430
|
+
location: "[parameters('location')]"
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1433
|
+
type: "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials",
|
|
1434
|
+
apiVersion: "2023-01-31",
|
|
1435
|
+
name: `[concat('${AZURE_DEPLOY_IDENTITY}/github-', parameters('githubOwners')[copyIndex()])]`,
|
|
1436
|
+
// Azure rejects concurrent writes to one identity's credentials.
|
|
1437
|
+
copy: { name: "owners", count: "[length(parameters('githubOwners'))]", mode: "Serial", batchSize: 1 },
|
|
1438
|
+
dependsOn: [identityId],
|
|
1439
|
+
properties: {
|
|
1440
|
+
issuer: "https://token.actions.githubusercontent.com",
|
|
1441
|
+
subject: "[concat('repository_owner:', parameters('githubOwners')[copyIndex()])]",
|
|
1442
|
+
audiences: ["api://AzureADTokenExchange"]
|
|
1443
|
+
}
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
type: "Microsoft.Authorization/roleAssignments",
|
|
1447
|
+
apiVersion: "2022-04-01",
|
|
1448
|
+
name: `[guid(resourceGroup().id, '${AZURE_DEPLOY_IDENTITY}', 'contributor')]`,
|
|
1449
|
+
dependsOn: [identityId],
|
|
1450
|
+
properties: {
|
|
1451
|
+
roleDefinitionId: `[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '${AZURE_CONTRIBUTOR}')]`,
|
|
1452
|
+
principalId: `[${identityRef}.principalId]`,
|
|
1453
|
+
principalType: "ServicePrincipal"
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
type: "Microsoft.Storage/storageAccounts",
|
|
1458
|
+
apiVersion: "2023-01-01",
|
|
1459
|
+
name: "[variables('storageName')]",
|
|
1460
|
+
location: "[parameters('location')]",
|
|
1461
|
+
sku: { name: "Standard_LRS" },
|
|
1462
|
+
kind: "StorageV2",
|
|
1463
|
+
properties: { minimumTlsVersion: "TLS1_2", allowBlobPublicAccess: false, supportsHttpsTrafficOnly: true }
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
type: "Microsoft.Storage/storageAccounts/blobServices/containers",
|
|
1467
|
+
apiVersion: "2023-01-01",
|
|
1468
|
+
name: "[concat(variables('storageName'), '/default/tfstate')]",
|
|
1469
|
+
dependsOn: ["[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]"]
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
type: "Microsoft.Authorization/roleAssignments",
|
|
1473
|
+
apiVersion: "2022-04-01",
|
|
1474
|
+
name: `[guid(resourceGroup().id, '${AZURE_DEPLOY_IDENTITY}', 'state')]`,
|
|
1475
|
+
scope: "[concat('Microsoft.Storage/storageAccounts/', variables('storageName'))]",
|
|
1476
|
+
dependsOn: [identityId, "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]"],
|
|
1477
|
+
properties: {
|
|
1478
|
+
roleDefinitionId: `[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '${AZURE_BLOB_DATA_CONTRIBUTOR}')]`,
|
|
1479
|
+
principalId: `[${identityRef}.principalId]`,
|
|
1480
|
+
principalType: "ServicePrincipal"
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
{
|
|
1484
|
+
type: "Microsoft.Resources/deployments",
|
|
1485
|
+
apiVersion: "2022-09-01",
|
|
1486
|
+
name: "compbox-connect",
|
|
1487
|
+
dependsOn: [
|
|
1488
|
+
"owners",
|
|
1489
|
+
`[resourceId('Microsoft.Authorization/roleAssignments', guid(resourceGroup().id, '${AZURE_DEPLOY_IDENTITY}', 'contributor'))]`,
|
|
1490
|
+
"[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageName'), 'default', 'tfstate')]"
|
|
1491
|
+
],
|
|
1492
|
+
properties: {
|
|
1493
|
+
mode: "Incremental",
|
|
1494
|
+
templateLink: {
|
|
1495
|
+
uri: `[format('{0}&subscriptionId={1}&tenantId={2}&resourceGroup={3}&location={4}&clientId={5}&storageAccount={6}', parameters('connectUrl'), subscription().subscriptionId, subscription().tenantId, uriComponent(resourceGroup().name), uriComponent(parameters('location')), ${identityRef}.clientId, variables('storageName'))]`
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
],
|
|
1500
|
+
outputs: {
|
|
1501
|
+
clientId: { type: "string", value: `[${identityRef}.clientId]` },
|
|
1502
|
+
storageAccount: { type: "string", value: "[variables('storageName')]" }
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function azureDeployUrl(templateUrl) {
|
|
1507
|
+
return `https://portal.azure.com/#create/Microsoft.Template/uri/${encodeURIComponent(templateUrl)}`;
|
|
1508
|
+
}
|
|
1509
|
+
function uniqueOwners(owners) {
|
|
1510
|
+
if (owners.length === 0)
|
|
1511
|
+
throw new Error("Connect GitHub first.");
|
|
1512
|
+
for (const owner of owners) {
|
|
1513
|
+
if (!isGitHubLogin(owner))
|
|
1514
|
+
throw new Error(`Not a GitHub account name: ${owner}`);
|
|
1515
|
+
}
|
|
1516
|
+
return [...new Set(owners)];
|
|
1517
|
+
}
|
|
1518
|
+
var AWS_REGIONS, AWS_ACCOUNT, AWS_REGION, AWS_ROLE_ARN, S3_BUCKET, GITHUB_OIDC_THUMBPRINTS, AWS_SETUP_FUNCTION, GUID, RESOURCE_GROUP, AZURE_LOCATION, STORAGE_ACCOUNT, AZURE_DEPLOY_IDENTITY, AZURE_CONTRIBUTOR, AZURE_BLOB_DATA_CONTRIBUTOR, EMPTY_ARM_TEMPLATE;
|
|
1519
|
+
var init_cloud_connect = __esm({
|
|
1520
|
+
"../shared/dist/cloud-connect.js"() {
|
|
1521
|
+
"use strict";
|
|
1522
|
+
init_cloud_setup();
|
|
1523
|
+
AWS_REGIONS = [
|
|
1524
|
+
"us-east-1",
|
|
1525
|
+
"us-east-2",
|
|
1526
|
+
"us-west-2",
|
|
1527
|
+
"ca-central-1",
|
|
1528
|
+
"eu-west-1",
|
|
1529
|
+
"eu-west-2",
|
|
1530
|
+
"eu-central-1",
|
|
1531
|
+
"ap-south-1",
|
|
1532
|
+
"ap-southeast-1",
|
|
1533
|
+
"ap-southeast-2",
|
|
1534
|
+
"ap-northeast-1"
|
|
1535
|
+
];
|
|
1536
|
+
AWS_ACCOUNT = /^\d{12}$/;
|
|
1537
|
+
AWS_REGION = /^[a-z]{2}(?:-gov)?-[a-z]+-\d$/;
|
|
1538
|
+
AWS_ROLE_ARN = /^arn:aws[a-z-]*:iam::\d{12}:role\/[\w+=,.@/-]{1,512}$/;
|
|
1539
|
+
S3_BUCKET = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
1540
|
+
GITHUB_OIDC_THUMBPRINTS = ["6938fd4d98bab03faadb97b34396831e3780aea1", "1c58a3a8518e8759bf075b76b750d4f2df264fcd"];
|
|
1541
|
+
AWS_SETUP_FUNCTION = `import json, urllib.error, urllib.request
|
|
1542
|
+
import boto3
|
|
1543
|
+
|
|
1544
|
+
ISSUER = "token.actions.githubusercontent.com"
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
def respond(event, ok, data=None, reason=""):
|
|
1548
|
+
body = json.dumps({
|
|
1549
|
+
"Status": "SUCCESS" if ok else "FAILED",
|
|
1550
|
+
"Reason": (reason or "See the function's CloudWatch logs")[:1000],
|
|
1551
|
+
"PhysicalResourceId": event.get("PhysicalResourceId") or event["LogicalResourceId"],
|
|
1552
|
+
"StackId": event["StackId"],
|
|
1553
|
+
"RequestId": event["RequestId"],
|
|
1554
|
+
"LogicalResourceId": event["LogicalResourceId"],
|
|
1555
|
+
"Data": data or {},
|
|
1556
|
+
}).encode()
|
|
1557
|
+
req = urllib.request.Request(event["ResponseURL"], data=body, method="PUT", headers={"Content-Type": ""})
|
|
1558
|
+
urllib.request.urlopen(req, timeout=30)
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def github_oidc():
|
|
1562
|
+
iam = boto3.client("iam")
|
|
1563
|
+
for p in iam.list_open_id_connect_providers()["OpenIDConnectProviderList"]:
|
|
1564
|
+
arn = p["Arn"]
|
|
1565
|
+
if arn.endswith("/" + ISSUER):
|
|
1566
|
+
if "sts.amazonaws.com" not in iam.get_open_id_connect_provider(OpenIDConnectProviderArn=arn)["ClientIDList"]:
|
|
1567
|
+
iam.add_client_id_to_open_id_connect_provider(OpenIDConnectProviderArn=arn, ClientID="sts.amazonaws.com")
|
|
1568
|
+
return arn
|
|
1569
|
+
return iam.create_open_id_connect_provider(
|
|
1570
|
+
Url="https://" + ISSUER, ClientIDList=["sts.amazonaws.com"], ThumbprintList=${JSON.stringify(GITHUB_OIDC_THUMBPRINTS)}
|
|
1571
|
+
)["OpenIDConnectProviderArn"]
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
def notify(p):
|
|
1575
|
+
body = json.dumps({
|
|
1576
|
+
"token": p["Token"], "accountId": p["AccountId"], "region": p["Region"],
|
|
1577
|
+
"roleArn": p["RoleArn"], "stateBucket": p["Bucket"],
|
|
1578
|
+
}).encode()
|
|
1579
|
+
req = urllib.request.Request(p["Url"], data=body, method="POST", headers={"Content-Type": "application/json"})
|
|
1580
|
+
try:
|
|
1581
|
+
urllib.request.urlopen(req, timeout=30)
|
|
1582
|
+
except urllib.error.HTTPError as e:
|
|
1583
|
+
raise RuntimeError("CompBox said: " + e.read().decode(errors="replace")[:500])
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
def handler(event, context):
|
|
1587
|
+
try:
|
|
1588
|
+
props = event["ResourceProperties"]
|
|
1589
|
+
if event["RequestType"] == "Delete":
|
|
1590
|
+
return respond(event, True)
|
|
1591
|
+
if props["Action"] == "oidc":
|
|
1592
|
+
return respond(event, True, {"Arn": github_oidc()})
|
|
1593
|
+
try:
|
|
1594
|
+
notify(props)
|
|
1595
|
+
except Exception:
|
|
1596
|
+
# The connect link expires; a later stack update shouldn't fail on it.
|
|
1597
|
+
if event["RequestType"] != "Update":
|
|
1598
|
+
raise
|
|
1599
|
+
respond(event, True)
|
|
1600
|
+
except Exception as e:
|
|
1601
|
+
print(e)
|
|
1602
|
+
respond(event, False, reason=str(e))
|
|
1603
|
+
`;
|
|
1604
|
+
GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1605
|
+
RESOURCE_GROUP = /^[-\w.()]{0,89}[-\w()]$/;
|
|
1606
|
+
AZURE_LOCATION = /^[a-z0-9]{2,40}$/;
|
|
1607
|
+
STORAGE_ACCOUNT = /^[a-z0-9]{3,24}$/;
|
|
1608
|
+
AZURE_DEPLOY_IDENTITY = "compbox-deploy";
|
|
1609
|
+
AZURE_CONTRIBUTOR = "b24988ac-6180-42a0-ab88-20f7382dd24c";
|
|
1610
|
+
AZURE_BLOB_DATA_CONTRIBUTOR = "ba92f5b4-2d11-453d-a403-e96b0029c9fe";
|
|
1611
|
+
EMPTY_ARM_TEMPLATE = {
|
|
1612
|
+
$schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
|
1613
|
+
contentVersion: "1.0.0.0",
|
|
1614
|
+
resources: []
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
// ../shared/dist/stack-files.js
|
|
1620
|
+
function stackEnvironments(tier) {
|
|
1621
|
+
return STACK_TIERS[tier].environments;
|
|
1622
|
+
}
|
|
1623
|
+
function deployRunTitle(environment) {
|
|
1624
|
+
return `Deploy to ${environment}`;
|
|
1625
|
+
}
|
|
1626
|
+
function cloudRunUrl(service, projectNumber, region) {
|
|
1627
|
+
return `https://${service}-${projectNumber}.${region}.run.app`;
|
|
1628
|
+
}
|
|
1629
|
+
function stackAppName(repoName) {
|
|
1630
|
+
let slug = repoName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^[^a-z]+/, "").slice(0, 20).replace(/-+$/, "");
|
|
1631
|
+
if (slug.length < 2)
|
|
1632
|
+
slug = `app${slug}`.slice(0, 20);
|
|
1633
|
+
return slug;
|
|
1634
|
+
}
|
|
1635
|
+
function envSettings(env) {
|
|
1636
|
+
return env === "prod" ? { min: 0, max: 10 } : { min: 0, max: 2 };
|
|
1637
|
+
}
|
|
1638
|
+
function buildStackFiles(input) {
|
|
1639
|
+
const { tier, app, defaultBranch, cloud } = input;
|
|
1640
|
+
if (!APP.test(app))
|
|
1641
|
+
throw new Error("Not a valid app name.");
|
|
1642
|
+
if (!BRANCH.test(defaultBranch))
|
|
1643
|
+
throw new Error("Not a valid branch name.");
|
|
1644
|
+
const envs = stackEnvironments(tier);
|
|
1645
|
+
const registry = `${cloud.region}-docker.pkg.dev/${cloud.projectId}/${app}`;
|
|
1646
|
+
const files = [];
|
|
1647
|
+
files.push({
|
|
1648
|
+
path: "infra/versions.tf",
|
|
1649
|
+
content: `# Managed by CompBox. Changes here are applied on the next deploy.
|
|
1650
|
+
terraform {
|
|
1651
|
+
required_version = ">= 1.6"
|
|
1652
|
+
required_providers {
|
|
1653
|
+
google = {
|
|
1654
|
+
source = "hashicorp/google"
|
|
1655
|
+
version = "~> 6.0"
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
# Bucket and prefix are passed by the deploy workflow.
|
|
1659
|
+
backend "gcs" {}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
provider "google" {
|
|
1663
|
+
project = var.project_id
|
|
1664
|
+
region = var.region
|
|
1665
|
+
}
|
|
1666
|
+
`
|
|
1667
|
+
});
|
|
1668
|
+
files.push({
|
|
1669
|
+
path: "infra/variables.tf",
|
|
1670
|
+
content: `variable "project_id" {
|
|
1671
|
+
type = string
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
variable "region" {
|
|
1675
|
+
type = string
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
variable "app" {
|
|
1679
|
+
type = string
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
variable "environment" {
|
|
1683
|
+
type = string
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
variable "min_instances" {
|
|
1687
|
+
type = number
|
|
1688
|
+
default = 0
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
variable "max_instances" {
|
|
1692
|
+
type = number
|
|
1693
|
+
default = 2
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
# Only used the first time; every deploy sets the real image.
|
|
1697
|
+
variable "bootstrap_image" {
|
|
1698
|
+
type = string
|
|
1699
|
+
default = "us-docker.pkg.dev/cloudrun/container/hello"
|
|
1700
|
+
}
|
|
1701
|
+
`
|
|
1702
|
+
});
|
|
1703
|
+
files.push({
|
|
1704
|
+
path: "infra/main.tf",
|
|
1705
|
+
content: `locals {
|
|
1706
|
+
service = "\${var.app}-\${var.environment}"
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
# One image registry for the app, created with dev and shared by every environment.
|
|
1710
|
+
resource "google_artifact_registry_repository" "app" {
|
|
1711
|
+
count = var.environment == "dev" ? 1 : 0
|
|
1712
|
+
repository_id = var.app
|
|
1713
|
+
location = var.region
|
|
1714
|
+
format = "DOCKER"
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
# The account the app runs as in this environment (no permissions by default).
|
|
1718
|
+
resource "google_service_account" "runtime" {
|
|
1719
|
+
account_id = local.service
|
|
1720
|
+
display_name = "\${local.service} (CompBox)"
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
resource "google_cloud_run_v2_service" "app" {
|
|
1724
|
+
name = local.service
|
|
1725
|
+
location = var.region
|
|
1726
|
+
ingress = "INGRESS_TRAFFIC_ALL"
|
|
1727
|
+
deletion_protection = var.environment == "prod"
|
|
1728
|
+
|
|
1729
|
+
template {
|
|
1730
|
+
service_account = google_service_account.runtime.email
|
|
1731
|
+
scaling {
|
|
1732
|
+
min_instance_count = var.min_instances
|
|
1733
|
+
max_instance_count = var.max_instances
|
|
1734
|
+
}
|
|
1735
|
+
containers {
|
|
1736
|
+
image = var.bootstrap_image
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
# Deploys change the image; Terraform owns everything else.
|
|
1741
|
+
lifecycle {
|
|
1742
|
+
ignore_changes = [template[0].containers[0].image, client, client_version]
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
# Anyone with the link can open it. Remove this to keep the app private.
|
|
1747
|
+
resource "google_cloud_run_v2_service_iam_member" "public" {
|
|
1748
|
+
name = google_cloud_run_v2_service.app.name
|
|
1749
|
+
location = var.region
|
|
1750
|
+
role = "roles/run.invoker"
|
|
1751
|
+
member = "allUsers"
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
output "url" {
|
|
1755
|
+
value = google_cloud_run_v2_service.app.uri
|
|
1756
|
+
}
|
|
1757
|
+
`
|
|
1758
|
+
});
|
|
1759
|
+
for (const env of envs) {
|
|
1760
|
+
const s = envSettings(env);
|
|
1761
|
+
files.push({
|
|
1762
|
+
path: `infra/envs/${env}.tfvars`,
|
|
1763
|
+
content: `project_id = "${cloud.projectId}"
|
|
1764
|
+
region = "${cloud.region}"
|
|
1765
|
+
app = "${app}"
|
|
1766
|
+
environment = "${env}"
|
|
1767
|
+
min_instances = ${String(s.min)}
|
|
1768
|
+
max_instances = ${String(s.max)}
|
|
1769
|
+
`
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
const options = envs.map((e) => ` - ${e}`).join("\n");
|
|
1773
|
+
files.push({
|
|
1774
|
+
path: ".github/workflows/compbox.yml",
|
|
1775
|
+
content: `# Managed by CompBox: every push to ${defaultBranch} deploys to dev.${envs.length > 1 ? "\n# Stage and prod: Actions \u2192 CompBox \u2192 Run workflow. Prod waits for its environment's approval." : ""}
|
|
1776
|
+
name: CompBox
|
|
1777
|
+
run-name: Deploy to \${{ inputs.environment || 'dev' }}
|
|
1778
|
+
|
|
1779
|
+
on:
|
|
1780
|
+
push:
|
|
1781
|
+
branches: [${JSON.stringify(defaultBranch)}]
|
|
1782
|
+
workflow_dispatch:
|
|
1783
|
+
inputs:
|
|
1784
|
+
environment:
|
|
1785
|
+
description: Where to deploy
|
|
1786
|
+
type: choice
|
|
1787
|
+
default: dev
|
|
1788
|
+
options:
|
|
1789
|
+
${options}
|
|
1790
|
+
|
|
1791
|
+
permissions:
|
|
1792
|
+
contents: read
|
|
1793
|
+
id-token: write
|
|
1794
|
+
|
|
1795
|
+
jobs:
|
|
1796
|
+
deploy:
|
|
1797
|
+
uses: ./.github/workflows/compbox-deploy.yml
|
|
1798
|
+
with:
|
|
1799
|
+
environment: \${{ inputs.environment || 'dev' }}
|
|
1800
|
+
`
|
|
1801
|
+
});
|
|
1802
|
+
files.push({
|
|
1803
|
+
path: ".github/workflows/compbox-deploy.yml",
|
|
1804
|
+
content: `# Managed by CompBox. Signs in to Google Cloud without stored keys (Workload Identity).
|
|
1805
|
+
name: CompBox deploy
|
|
1806
|
+
|
|
1807
|
+
on:
|
|
1808
|
+
workflow_call:
|
|
1809
|
+
inputs:
|
|
1810
|
+
environment:
|
|
1811
|
+
type: string
|
|
1812
|
+
required: true
|
|
1813
|
+
|
|
1814
|
+
permissions:
|
|
1815
|
+
contents: read
|
|
1816
|
+
id-token: write
|
|
1817
|
+
|
|
1818
|
+
concurrency:
|
|
1819
|
+
group: compbox-\${{ inputs.environment }}
|
|
1820
|
+
cancel-in-progress: false
|
|
1821
|
+
|
|
1822
|
+
jobs:
|
|
1823
|
+
deploy:
|
|
1824
|
+
runs-on: ubuntu-latest
|
|
1825
|
+
environment:
|
|
1826
|
+
name: \${{ inputs.environment }}
|
|
1827
|
+
url: \${{ steps.deploy.outputs.url }}
|
|
1828
|
+
env:
|
|
1829
|
+
ENVIRONMENT: \${{ inputs.environment }}
|
|
1830
|
+
PROJECT_ID: ${cloud.projectId}
|
|
1831
|
+
REGION: ${cloud.region}
|
|
1832
|
+
APP: ${app}
|
|
1833
|
+
IMAGE: ${registry}/app:\${{ github.sha }}
|
|
1834
|
+
STATE_BUCKET: ${gcpStateBucket(cloud.projectId)}
|
|
1835
|
+
steps:
|
|
1836
|
+
- uses: actions/checkout@v4
|
|
1837
|
+
|
|
1838
|
+
- uses: google-github-actions/auth@v2
|
|
1839
|
+
with:
|
|
1840
|
+
workload_identity_provider: ${cloud.workloadProvider}
|
|
1841
|
+
service_account: ${cloud.serviceAccountEmail}
|
|
1842
|
+
|
|
1843
|
+
- uses: google-github-actions/setup-gcloud@v2
|
|
1844
|
+
|
|
1845
|
+
- uses: hashicorp/setup-terraform@v3
|
|
1846
|
+
|
|
1847
|
+
- name: Infrastructure
|
|
1848
|
+
working-directory: infra
|
|
1849
|
+
run: |
|
|
1850
|
+
terraform init -input=false -backend-config="bucket=$STATE_BUCKET" -backend-config="prefix=$APP/$ENVIRONMENT"
|
|
1851
|
+
terraform apply -input=false -auto-approve -var-file="envs/$ENVIRONMENT.tfvars"
|
|
1852
|
+
|
|
1853
|
+
- name: Build
|
|
1854
|
+
run: |
|
|
1855
|
+
if [ ! -f Dockerfile ]; then
|
|
1856
|
+
echo "::error::Add a Dockerfile at the repository root: CompBox deploys the app as a container that listens on \\$PORT."
|
|
1857
|
+
exit 1
|
|
1858
|
+
fi
|
|
1859
|
+
gcloud auth configure-docker "$REGION-docker.pkg.dev" --quiet
|
|
1860
|
+
docker build -t "$IMAGE" .
|
|
1861
|
+
docker push "$IMAGE"
|
|
1862
|
+
|
|
1863
|
+
- name: Deploy
|
|
1864
|
+
id: deploy
|
|
1865
|
+
run: |
|
|
1866
|
+
SERVICE="$APP-$ENVIRONMENT"
|
|
1867
|
+
gcloud run deploy "$SERVICE" --image "$IMAGE" --region "$REGION" --project "$PROJECT_ID" --quiet
|
|
1868
|
+
URL=$(gcloud run services describe "$SERVICE" --region "$REGION" --project "$PROJECT_ID" --format='value(status.url)')
|
|
1869
|
+
echo "url=$URL" >> "$GITHUB_OUTPUT"
|
|
1870
|
+
echo "### Live at $URL" >> "$GITHUB_STEP_SUMMARY"
|
|
1871
|
+
`
|
|
1872
|
+
});
|
|
1873
|
+
files.push({
|
|
1874
|
+
path: "infra/README.md",
|
|
1875
|
+
content: `# Infrastructure (managed by CompBox)
|
|
1876
|
+
|
|
1877
|
+
This project deploys to Google Cloud project \`${cloud.projectId}\` (${cloud.region}) as a Cloud Run service per environment: ${envs.map((e) => `\`${app}-${e}\``).join(", ")}.
|
|
1878
|
+
|
|
1879
|
+
- **Dev:** every push to \`${defaultBranch}\` deploys it.${envs.length > 1 ? "\n- **Stage and prod:** in GitHub, Actions \u2192 CompBox \u2192 Run workflow, then pick the environment. Add required reviewers to the `prod` environment (Settings \u2192 Environments) so prod waits for approval." : ""}
|
|
1880
|
+
- **The app:** a \`Dockerfile\` at the repository root, for a container that listens on \`$PORT\` (8080).
|
|
1881
|
+
- **Sign-in:** GitHub Actions sign in to Google Cloud through Workload Identity \u2014 there are no keys to store or rotate.
|
|
1882
|
+
- **Changes:** edit the Terraform here; the next deploy applies it. Services are public by default; remove the \`public\` resource in \`main.tf\` to keep one private.
|
|
1883
|
+
`
|
|
1884
|
+
});
|
|
1885
|
+
return files;
|
|
1886
|
+
}
|
|
1887
|
+
var STACK_TIERS, STACK_WORKFLOW, APP, BRANCH;
|
|
1888
|
+
var init_stack_files = __esm({
|
|
1889
|
+
"../shared/dist/stack-files.js"() {
|
|
1890
|
+
"use strict";
|
|
1891
|
+
init_cloud_setup();
|
|
1892
|
+
STACK_TIERS = {
|
|
1893
|
+
dev: { label: "Dev only", environments: ["dev"] },
|
|
1894
|
+
full: { label: "Dev, stage and prod", environments: ["dev", "stage", "prod"] }
|
|
1895
|
+
};
|
|
1896
|
+
STACK_WORKFLOW = "compbox.yml";
|
|
1897
|
+
APP = /^[a-z][a-z0-9-]{1,19}$/;
|
|
1898
|
+
BRANCH = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
// ../shared/dist/github-repo.js
|
|
1903
|
+
function githubRepoKey(remote) {
|
|
1904
|
+
if (typeof remote !== "string")
|
|
1905
|
+
return null;
|
|
1906
|
+
const normalized = normalizeGitRemote(remote);
|
|
1907
|
+
const match = /^https:\/\/([^/]+)\/(.*)$/.exec(normalized);
|
|
1908
|
+
if (!match)
|
|
1909
|
+
return null;
|
|
1910
|
+
const host = match[1];
|
|
1911
|
+
const path4 = match[2];
|
|
1912
|
+
if (host !== "github.com" || path4 === void 0)
|
|
1913
|
+
return null;
|
|
1914
|
+
const segments = path4.split("/");
|
|
1915
|
+
if (segments.length !== 2)
|
|
1916
|
+
return null;
|
|
1917
|
+
const [owner, name] = segments;
|
|
1918
|
+
if (!owner || !name)
|
|
1919
|
+
return null;
|
|
1920
|
+
return `${owner}/${name}`.toLowerCase();
|
|
1921
|
+
}
|
|
1922
|
+
var init_github_repo = __esm({
|
|
1923
|
+
"../shared/dist/github-repo.js"() {
|
|
1924
|
+
"use strict";
|
|
1925
|
+
init_mailbox();
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
// ../shared/dist/return-to.js
|
|
1930
|
+
function safeReturnTo(value) {
|
|
1931
|
+
if (typeof value !== "string")
|
|
1932
|
+
return null;
|
|
1933
|
+
if (value.length < 1 || value.length > RETURN_TO_MAX_LENGTH)
|
|
1934
|
+
return null;
|
|
1935
|
+
if (!value.startsWith("/"))
|
|
1936
|
+
return null;
|
|
1937
|
+
const second = value.charAt(1);
|
|
1938
|
+
if (second === "/" || second === "\\")
|
|
1939
|
+
return null;
|
|
1940
|
+
if (value.includes("\\"))
|
|
1941
|
+
return null;
|
|
1942
|
+
if (value.includes("://"))
|
|
1943
|
+
return null;
|
|
1944
|
+
for (let i = 0; i < value.length; i++) {
|
|
1945
|
+
const code = value.charCodeAt(i);
|
|
1946
|
+
if (code < 32 || code === 127)
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
1949
|
+
return value;
|
|
1950
|
+
}
|
|
1951
|
+
var RETURN_TO_MAX_LENGTH;
|
|
1952
|
+
var init_return_to = __esm({
|
|
1953
|
+
"../shared/dist/return-to.js"() {
|
|
1954
|
+
"use strict";
|
|
1955
|
+
RETURN_TO_MAX_LENGTH = 512;
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
|
|
833
1959
|
// ../shared/dist/index.js
|
|
834
1960
|
var dist_exports = {};
|
|
835
1961
|
__export(dist_exports, {
|
|
836
1962
|
AI_PROVIDERS: () => AI_PROVIDERS,
|
|
837
1963
|
APPEND_DESCRIPTION_MAX: () => APPEND_DESCRIPTION_MAX,
|
|
1964
|
+
AWS_REGIONS: () => AWS_REGIONS,
|
|
1965
|
+
AZURE_DEPLOY_IDENTITY: () => AZURE_DEPLOY_IDENTITY,
|
|
838
1966
|
COMPLETION_REPAIR_INSTRUCTION: () => COMPLETION_REPAIR_INSTRUCTION,
|
|
1967
|
+
CONVERSATION_QUALITY_ROUNDS: () => CONVERSATION_QUALITY_ROUNDS,
|
|
839
1968
|
DESCRIPTION_MAX: () => DESCRIPTION_MAX,
|
|
1969
|
+
EMPTY_ARM_TEMPLATE: () => EMPTY_ARM_TEMPLATE,
|
|
1970
|
+
GCP_CONNECT_STEPS: () => GCP_CONNECT_STEPS,
|
|
1971
|
+
GCP_CONNECT_STEP_LABELS: () => GCP_CONNECT_STEP_LABELS,
|
|
1972
|
+
GCP_DEPLOY_ACCOUNT: () => GCP_DEPLOY_ACCOUNT,
|
|
1973
|
+
GCP_DEPLOY_ROLES: () => GCP_DEPLOY_ROLES,
|
|
1974
|
+
GCP_IDENTITY_POOL: () => GCP_IDENTITY_POOL,
|
|
1975
|
+
GCP_IDENTITY_PROVIDER: () => GCP_IDENTITY_PROVIDER,
|
|
1976
|
+
GCP_REGIONS: () => GCP_REGIONS,
|
|
840
1977
|
INTERVIEW_DEFINITION: () => INTERVIEW_DEFINITION,
|
|
841
1978
|
INTERVIEW_FIELD_MIN_WORDS: () => INTERVIEW_FIELD_MIN_WORDS,
|
|
842
1979
|
INTERVIEW_STATUSES: () => INTERVIEW_STATUSES,
|
|
@@ -846,6 +1983,9 @@ __export(dist_exports, {
|
|
|
846
1983
|
MODEL_CATALOG: () => MODEL_CATALOG,
|
|
847
1984
|
ORG_ROLES: () => ORG_ROLES,
|
|
848
1985
|
PXPIPE_DEFAULT_PROXY_URL: () => PXPIPE_DEFAULT_PROXY_URL,
|
|
1986
|
+
RETURN_TO_MAX_LENGTH: () => RETURN_TO_MAX_LENGTH,
|
|
1987
|
+
STACK_TIERS: () => STACK_TIERS,
|
|
1988
|
+
STACK_WORKFLOW: () => STACK_WORKFLOW,
|
|
849
1989
|
START_EXECUTION_KINDS: () => START_EXECUTION_KINDS,
|
|
850
1990
|
SUBSCRIPTION_STATUSES: () => SUBSCRIPTION_STATUSES,
|
|
851
1991
|
SUMMARY_MAX: () => SUMMARY_MAX,
|
|
@@ -855,23 +1995,50 @@ __export(dist_exports, {
|
|
|
855
1995
|
agentSchema: () => agentSchema,
|
|
856
1996
|
aiProviderValues: () => aiProviderValues,
|
|
857
1997
|
aiRouteValues: () => aiRouteValues,
|
|
1998
|
+
awsQuickCreateUrl: () => awsQuickCreateUrl,
|
|
1999
|
+
azureDeployUrl: () => azureDeployUrl,
|
|
2000
|
+
azureGitHubSubject: () => azureGitHubSubject,
|
|
858
2001
|
buildAuditFeedback: () => buildAuditFeedback,
|
|
2002
|
+
buildAwsConnectTemplate: () => buildAwsConnectTemplate,
|
|
2003
|
+
buildAzureConnectTemplate: () => buildAzureConnectTemplate,
|
|
2004
|
+
buildConversationSystemPrompt: () => buildConversationSystemPrompt,
|
|
2005
|
+
buildGcpSetupScript: () => buildGcpSetupScript,
|
|
859
2006
|
buildInterviewSystemPrompt: () => buildInterviewSystemPrompt,
|
|
2007
|
+
buildStackFiles: () => buildStackFiles,
|
|
2008
|
+
cloudRunUrl: () => cloudRunUrl,
|
|
2009
|
+
complianceFromRecord: () => complianceFromRecord,
|
|
2010
|
+
deployRunTitle: () => deployRunTitle,
|
|
860
2011
|
err: () => err,
|
|
861
2012
|
extractCompletionJson: () => extractCompletionJson,
|
|
2013
|
+
gcpServiceAccountEmail: () => gcpServiceAccountEmail,
|
|
2014
|
+
gcpStateBucket: () => gcpStateBucket,
|
|
2015
|
+
gcpWorkloadProvider: () => gcpWorkloadProvider,
|
|
862
2016
|
getAnthropicProxyUrl: () => getAnthropicProxyUrl,
|
|
863
2017
|
getModelById: () => getModelById,
|
|
864
2018
|
getModelDisplayName: () => getModelDisplayName,
|
|
865
2019
|
getOpenAiProxyUrl: () => getOpenAiProxyUrl,
|
|
2020
|
+
githubRepoKey: () => githubRepoKey,
|
|
866
2021
|
instructionSchema: () => instructionSchema,
|
|
867
2022
|
interviewFieldKeys: () => interviewFieldKeys,
|
|
868
2023
|
interviewSectionKeys: () => interviewSectionKeys,
|
|
2024
|
+
isAwsAccountId: () => isAwsAccountId,
|
|
2025
|
+
isAwsRegion: () => isAwsRegion,
|
|
2026
|
+
isAwsRoleArn: () => isAwsRoleArn,
|
|
2027
|
+
isAzureGuid: () => isAzureGuid,
|
|
2028
|
+
isAzureLocation: () => isAzureLocation,
|
|
2029
|
+
isAzureResourceGroup: () => isAzureResourceGroup,
|
|
2030
|
+
isAzureStorageAccount: () => isAzureStorageAccount,
|
|
2031
|
+
isGcpProjectId: () => isGcpProjectId,
|
|
2032
|
+
isGcpProjectNumber: () => isGcpProjectNumber,
|
|
2033
|
+
isGcpRegion: () => isGcpRegion,
|
|
2034
|
+
isGitHubLogin: () => isGitHubLogin,
|
|
869
2035
|
isInterviewStatus: () => isInterviewStatus,
|
|
870
2036
|
isJobStatus: () => isJobStatus,
|
|
871
2037
|
isJobType: () => isJobType,
|
|
872
2038
|
isMachineStatus: () => isMachineStatus,
|
|
873
2039
|
isOrgRole: () => isOrgRole,
|
|
874
2040
|
isPxpipeEnabled: () => isPxpipeEnabled,
|
|
2041
|
+
isS3BucketName: () => isS3BucketName,
|
|
875
2042
|
isSessionErrorEvent: () => isSessionErrorEvent,
|
|
876
2043
|
isSessionFeatureEvent: () => isSessionFeatureEvent,
|
|
877
2044
|
isSessionLifecycleEvent: () => isSessionLifecycleEvent,
|
|
@@ -889,12 +2056,20 @@ __export(dist_exports, {
|
|
|
889
2056
|
normalizeGitRemote: () => normalizeGitRemote,
|
|
890
2057
|
ok: () => ok,
|
|
891
2058
|
probeProxyHealth: () => probeProxyHealth,
|
|
2059
|
+
renderConversationPrompt: () => renderConversationPrompt,
|
|
892
2060
|
resolveRouting: () => resolveRouting,
|
|
2061
|
+
runConversationStep: () => runConversationStep,
|
|
2062
|
+
safeReturnTo: () => safeReturnTo,
|
|
893
2063
|
safeValidateSkillPackManifest: () => safeValidateSkillPackManifest,
|
|
2064
|
+
savedContextFromRecord: () => savedContextFromRecord,
|
|
2065
|
+
savedMessage: () => savedMessage,
|
|
894
2066
|
skillPackManifestSchema: () => skillPackManifestSchema,
|
|
895
2067
|
skillSchema: () => skillSchema,
|
|
896
2068
|
sleep: () => sleep,
|
|
2069
|
+
stackAppName: () => stackAppName,
|
|
2070
|
+
stackEnvironments: () => stackEnvironments,
|
|
897
2071
|
thinkingLevelValues: () => thinkingLevelValues,
|
|
2072
|
+
toConversationMessages: () => toConversationMessages,
|
|
898
2073
|
validatePackName: () => validatePackName,
|
|
899
2074
|
validateSkillPackManifest: () => validateSkillPackManifest
|
|
900
2075
|
});
|
|
@@ -909,6 +2084,12 @@ var init_dist = __esm({
|
|
|
909
2084
|
init_pxpipe();
|
|
910
2085
|
init_interview_definition();
|
|
911
2086
|
init_interview_completion();
|
|
2087
|
+
init_interview_conversation();
|
|
2088
|
+
init_cloud_setup();
|
|
2089
|
+
init_cloud_connect();
|
|
2090
|
+
init_stack_files();
|
|
2091
|
+
init_github_repo();
|
|
2092
|
+
init_return_to();
|
|
912
2093
|
}
|
|
913
2094
|
});
|
|
914
2095
|
|
|
@@ -11089,7 +12270,7 @@ function withLegacyToolCallRewrite(transport) {
|
|
|
11089
12270
|
var SERVER_NAME = "compbox-mcp";
|
|
11090
12271
|
function resolveServerVersion() {
|
|
11091
12272
|
if (true) {
|
|
11092
|
-
return "1.
|
|
12273
|
+
return "1.21.0";
|
|
11093
12274
|
}
|
|
11094
12275
|
const require2 = createRequire(import.meta.url);
|
|
11095
12276
|
for (const candidate of ["../package.json", "./package.json"]) {
|