@mycompbox/compbox-mcp 1.20.0 → 1.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +953 -1
- 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
|
|
|
@@ -1050,14 +1082,898 @@ var init_interview_conversation = __esm({
|
|
|
1050
1082
|
}
|
|
1051
1083
|
});
|
|
1052
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
|
+
|
|
1053
1959
|
// ../shared/dist/index.js
|
|
1054
1960
|
var dist_exports = {};
|
|
1055
1961
|
__export(dist_exports, {
|
|
1056
1962
|
AI_PROVIDERS: () => AI_PROVIDERS,
|
|
1057
1963
|
APPEND_DESCRIPTION_MAX: () => APPEND_DESCRIPTION_MAX,
|
|
1964
|
+
AWS_REGIONS: () => AWS_REGIONS,
|
|
1965
|
+
AZURE_DEPLOY_IDENTITY: () => AZURE_DEPLOY_IDENTITY,
|
|
1058
1966
|
COMPLETION_REPAIR_INSTRUCTION: () => COMPLETION_REPAIR_INSTRUCTION,
|
|
1059
1967
|
CONVERSATION_QUALITY_ROUNDS: () => CONVERSATION_QUALITY_ROUNDS,
|
|
1060
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,
|
|
1061
1977
|
INTERVIEW_DEFINITION: () => INTERVIEW_DEFINITION,
|
|
1062
1978
|
INTERVIEW_FIELD_MIN_WORDS: () => INTERVIEW_FIELD_MIN_WORDS,
|
|
1063
1979
|
INTERVIEW_STATUSES: () => INTERVIEW_STATUSES,
|
|
@@ -1067,6 +1983,9 @@ __export(dist_exports, {
|
|
|
1067
1983
|
MODEL_CATALOG: () => MODEL_CATALOG,
|
|
1068
1984
|
ORG_ROLES: () => ORG_ROLES,
|
|
1069
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,
|
|
1070
1989
|
START_EXECUTION_KINDS: () => START_EXECUTION_KINDS,
|
|
1071
1990
|
SUBSCRIPTION_STATUSES: () => SUBSCRIPTION_STATUSES,
|
|
1072
1991
|
SUMMARY_MAX: () => SUMMARY_MAX,
|
|
@@ -1076,25 +1995,50 @@ __export(dist_exports, {
|
|
|
1076
1995
|
agentSchema: () => agentSchema,
|
|
1077
1996
|
aiProviderValues: () => aiProviderValues,
|
|
1078
1997
|
aiRouteValues: () => aiRouteValues,
|
|
1998
|
+
awsQuickCreateUrl: () => awsQuickCreateUrl,
|
|
1999
|
+
azureDeployUrl: () => azureDeployUrl,
|
|
2000
|
+
azureGitHubSubject: () => azureGitHubSubject,
|
|
1079
2001
|
buildAuditFeedback: () => buildAuditFeedback,
|
|
2002
|
+
buildAwsConnectTemplate: () => buildAwsConnectTemplate,
|
|
2003
|
+
buildAzureConnectTemplate: () => buildAzureConnectTemplate,
|
|
1080
2004
|
buildConversationSystemPrompt: () => buildConversationSystemPrompt,
|
|
2005
|
+
buildGcpSetupScript: () => buildGcpSetupScript,
|
|
1081
2006
|
buildInterviewSystemPrompt: () => buildInterviewSystemPrompt,
|
|
2007
|
+
buildStackFiles: () => buildStackFiles,
|
|
2008
|
+
cloudRunUrl: () => cloudRunUrl,
|
|
1082
2009
|
complianceFromRecord: () => complianceFromRecord,
|
|
2010
|
+
deployRunTitle: () => deployRunTitle,
|
|
1083
2011
|
err: () => err,
|
|
1084
2012
|
extractCompletionJson: () => extractCompletionJson,
|
|
2013
|
+
gcpServiceAccountEmail: () => gcpServiceAccountEmail,
|
|
2014
|
+
gcpStateBucket: () => gcpStateBucket,
|
|
2015
|
+
gcpWorkloadProvider: () => gcpWorkloadProvider,
|
|
1085
2016
|
getAnthropicProxyUrl: () => getAnthropicProxyUrl,
|
|
1086
2017
|
getModelById: () => getModelById,
|
|
1087
2018
|
getModelDisplayName: () => getModelDisplayName,
|
|
1088
2019
|
getOpenAiProxyUrl: () => getOpenAiProxyUrl,
|
|
2020
|
+
githubRepoKey: () => githubRepoKey,
|
|
1089
2021
|
instructionSchema: () => instructionSchema,
|
|
1090
2022
|
interviewFieldKeys: () => interviewFieldKeys,
|
|
1091
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,
|
|
1092
2035
|
isInterviewStatus: () => isInterviewStatus,
|
|
1093
2036
|
isJobStatus: () => isJobStatus,
|
|
1094
2037
|
isJobType: () => isJobType,
|
|
1095
2038
|
isMachineStatus: () => isMachineStatus,
|
|
1096
2039
|
isOrgRole: () => isOrgRole,
|
|
1097
2040
|
isPxpipeEnabled: () => isPxpipeEnabled,
|
|
2041
|
+
isS3BucketName: () => isS3BucketName,
|
|
1098
2042
|
isSessionErrorEvent: () => isSessionErrorEvent,
|
|
1099
2043
|
isSessionFeatureEvent: () => isSessionFeatureEvent,
|
|
1100
2044
|
isSessionLifecycleEvent: () => isSessionLifecycleEvent,
|
|
@@ -1115,12 +2059,15 @@ __export(dist_exports, {
|
|
|
1115
2059
|
renderConversationPrompt: () => renderConversationPrompt,
|
|
1116
2060
|
resolveRouting: () => resolveRouting,
|
|
1117
2061
|
runConversationStep: () => runConversationStep,
|
|
2062
|
+
safeReturnTo: () => safeReturnTo,
|
|
1118
2063
|
safeValidateSkillPackManifest: () => safeValidateSkillPackManifest,
|
|
1119
2064
|
savedContextFromRecord: () => savedContextFromRecord,
|
|
1120
2065
|
savedMessage: () => savedMessage,
|
|
1121
2066
|
skillPackManifestSchema: () => skillPackManifestSchema,
|
|
1122
2067
|
skillSchema: () => skillSchema,
|
|
1123
2068
|
sleep: () => sleep,
|
|
2069
|
+
stackAppName: () => stackAppName,
|
|
2070
|
+
stackEnvironments: () => stackEnvironments,
|
|
1124
2071
|
thinkingLevelValues: () => thinkingLevelValues,
|
|
1125
2072
|
toConversationMessages: () => toConversationMessages,
|
|
1126
2073
|
validatePackName: () => validatePackName,
|
|
@@ -1138,6 +2085,11 @@ var init_dist = __esm({
|
|
|
1138
2085
|
init_interview_definition();
|
|
1139
2086
|
init_interview_completion();
|
|
1140
2087
|
init_interview_conversation();
|
|
2088
|
+
init_cloud_setup();
|
|
2089
|
+
init_cloud_connect();
|
|
2090
|
+
init_stack_files();
|
|
2091
|
+
init_github_repo();
|
|
2092
|
+
init_return_to();
|
|
1141
2093
|
}
|
|
1142
2094
|
});
|
|
1143
2095
|
|
|
@@ -11318,7 +12270,7 @@ function withLegacyToolCallRewrite(transport) {
|
|
|
11318
12270
|
var SERVER_NAME = "compbox-mcp";
|
|
11319
12271
|
function resolveServerVersion() {
|
|
11320
12272
|
if (true) {
|
|
11321
|
-
return "1.
|
|
12273
|
+
return "1.21.1";
|
|
11322
12274
|
}
|
|
11323
12275
|
const require2 = createRequire(import.meta.url);
|
|
11324
12276
|
for (const candidate of ["../package.json", "./package.json"]) {
|