@cloudbase/manager-node 5.8.1 → 5.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/context.js +2 -1
  3. package/lib/database/index.js +11 -6
  4. package/lib/deploy/DeployOrchestrator.js +30 -16
  5. package/lib/deploy/FunctionDeployer.js +269 -39
  6. package/lib/deploy/StaticDeployer.js +7 -1
  7. package/lib/deploy/function/builders/cloud.js +45 -348
  8. package/lib/deploy/function/cloud-build-service.js +2 -16
  9. package/lib/deploy/function/config-guard.js +42 -32
  10. package/lib/deploy/function/enterprise-tcr.js +194 -0
  11. package/lib/deploy/function/image-tag.js +31 -0
  12. package/lib/deploy/function/local-builder.js +170 -25
  13. package/lib/deploy/function/personal-tcr.js +153 -0
  14. package/lib/deploy/types.js +11 -0
  15. package/lib/hosting/index.js +27 -0
  16. package/lib/index.js +4 -3
  17. package/lib/projectValidator/index.js +22 -20
  18. package/lib/utils/cloud-api-request.js +26 -2
  19. package/package.json +2 -2
  20. package/types/context.d.ts +17 -3
  21. package/types/database/index.d.ts +1 -0
  22. package/types/deploy/DeployOrchestrator.d.ts +10 -0
  23. package/types/deploy/FunctionDeployer.d.ts +20 -2
  24. package/types/deploy/StaticDeployer.d.ts +11 -0
  25. package/types/deploy/function/builders/cloud.d.ts +6 -39
  26. package/types/deploy/function/enterprise-tcr.d.ts +32 -0
  27. package/types/deploy/function/image-tag.d.ts +10 -0
  28. package/types/deploy/function/local-builder.d.ts +35 -6
  29. package/types/deploy/function/personal-tcr.d.ts +35 -0
  30. package/types/deploy/types.d.ts +34 -8
  31. package/types/hosting/index.d.ts +13 -0
  32. package/types/index.d.ts +5 -0
  33. package/types/projectValidator/index.d.ts +3 -2
  34. package/types/utils/cloud-api-request.d.ts +3 -2
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.TCR_LOGIN_PUSH_SCRIPT = exports.TCR_PUSH_SCRIPT_ZIP_PATH = void 0;
7
6
  exports.buildImageOnCloud = buildImageOnCloud;
8
7
  const fs_1 = __importDefault(require("fs"));
9
8
  const path_1 = __importDefault(require("path"));
@@ -42,10 +41,6 @@ const POLL_MAX_INTERVAL = 30000;
42
41
  const POLL_TOTAL_TIMEOUT = 15 * 60 * 1000;
43
42
  /** 轮询临时错误最大连续重试次数,超过则判失败 */
44
43
  const POLL_MAX_TRANSIENT_ERRORS = 5;
45
- const TCR_REGISTRY_AUTH_EXIT_CODE = 42;
46
- const TCR_REGISTRY_INVALID_EXIT_CODE = 43;
47
- /** 个人版 TCR 自动创建目标仓库时,仓库数量已达到地域配额 */
48
- const TCR_PERSONAL_REPOSITORY_QUOTA_EXIT_CODE = 44;
49
44
  /** 仓库地址/命名空间/服务名等安全字符白名单:域名、路径、字母数字与常见分隔符 */
50
45
  const SAFE_REGISTRY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._\-/]*$/;
51
46
  /** tag 白名单:字母数字、下划线、点、中划线 */
@@ -113,257 +108,6 @@ function assertSafeBuildArgs(buildArgs) {
113
108
  }
114
109
  /** 个人版 CCR 默认域名 */
115
110
  const DEFAULT_PERSONAL_REGISTRY = 'ccr.ccs.tencentyun.com';
116
- /** 注入到构建上下文 zip 中的推送脚本相对路径(与 CustomSteps 的 push命令一致) */
117
- exports.TCR_PUSH_SCRIPT_ZIP_PATH = 'scripts/tcr-login-and-push.sh';
118
- /**
119
- * 注入到构建 zip 的 TCR/CCR 登录推送脚本(完整可运行)
120
- *
121
- * 对应构建方案(https://iwiki.woa.com/p/4020519533)「阶段 A」的 push 步骤:
122
- * push-image 步骤执行 `bash./scripts/tcr-login-and-push.sh`,该脚本必须随源码
123
- * 一同打进zip 根目录,否则云端会 `No such file or directory` 秒挂。
124
- *
125
- * 关键设计:
126
- * - 地域参数化:REGION 读环境变量 $TCR_REGION(由 buildEnv 按部署地域注入),
127
- * 避免硬编码 ap-shanghai跨地域时 AuthFailure.SignatureFailure。
128
- * - 双仓库兼容:设置了 $TCR_INSTANCE_ID 走企业版 TCR CreateInstanceToken;
129
- * 否则走个人版 CCR,用户名来自普通 Env,固定密码来自 CloudApp Secrets。
130
- * - 安全红线:凭证一律经 --password-stdin 传入,绝不出现在 ps/日志;
131
- * 不打印任何 $API_SECRET_* / token / $SECRET_TCR_PASSWORD_B64 或解码后的密码。
132
- *
133
- * 脚本内容为SDK 固定模板,不拼接任何用户输入(镜像地址等均由已校验的
134
- * 环境变量在容器内组装),无命令注入面。
135
- */
136
- exports.TCR_LOGIN_PUSH_SCRIPT = `#!/usr/bin/env bash
137
- # scripts/tcr-login-and-push.sh
138
- # 由 CloudBase SDK 自动注入:企业版使用 STS 换取临时 token,个人版使用 CloudApp Secret
139
- set -euo pipefail
140
-
141
- : "\${TCR_REGISTRY:?TCR_REGISTRY 未设置}"
142
- : "\${TCR_NAMESPACE:?TCR_NAMESPACE 未设置}"
143
- : "\${CLOUDBASE_SERVICE_NAME:?平台变量缺失}"
144
- : "\${CLOUDBASE_VERSION_NAME:?平台变量缺失}"
145
-
146
- REGION="\${TCR_REGION:-ap-shanghai}"
147
- IMAGE="\${TCR_REGISTRY}/\${TCR_NAMESPACE}/\${CLOUDBASE_SERVICE_NAME}:\${CLOUDBASE_VERSION_NAME}"
148
- IS_PERSONAL_REGISTRY=0
149
-
150
- if [ -n "\${TCR_INSTANCE_ID:-}" ]; then
151
- : "\${API_SECRET_ID:?STS 凭证缺失}"
152
- : "\${API_SECRET_KEY:?STS 凭证缺失}"
153
- : "\${API_TOKEN:?STS 凭证缺失}"
154
-
155
- # ---- 企业版 TCR:CreateInstanceToken 换登录态 ----
156
- HOST="tcr.tencentcloudapi.com"
157
- SERVICE="tcr"
158
- VERSION="2019-09-24"
159
- ACTION="CreateInstanceToken"
160
- ALGORITHM="TC3-HMAC-SHA256"
161
- TIMESTAMP=$(date +%s)
162
- DATE=$(date -u -d "@\${TIMESTAMP}" +"%Y-%m-%d" 2>/dev/null || date -u -r "\${TIMESTAMP}" +"%Y-%m-%d")
163
-
164
- # 构建阶段只需要一次 docker login/push,使用 1 小时临时凭证,避免创建长期凭证。
165
- PAYLOAD=$(printf '{"RegistryId":"%s","TokenType":"temp"}' "\${TCR_INSTANCE_ID}")
166
- HASHED_PAYLOAD=$(printf '%s' "\${PAYLOAD}" | openssl dgst -sha256 -hex | awk '{print $NF}')
167
- CANONICAL_HEADERS="content-type:application/json; charset=utf-8\\nhost:\${HOST}\\nx-tc-action:$(echo "\${ACTION}" | tr '[:upper:]' '[:lower:]')\\n"
168
- SIGNED_HEADERS="content-type;host;x-tc-action"
169
- CANONICAL_REQUEST="POST\\n/\\n\\n\${CANONICAL_HEADERS}\\n\${SIGNED_HEADERS}\\n\${HASHED_PAYLOAD}"
170
-
171
- CREDENTIAL_SCOPE="\${DATE}/\${SERVICE}/tc3_request"
172
- HASHED_CR=$(printf "\${CANONICAL_REQUEST}" | openssl dgst -sha256 -hex | awk '{print $NF}')
173
- STRING_TO_SIGN="\${ALGORITHM}\\n\${TIMESTAMP}\\n\${CREDENTIAL_SCOPE}\\n\${HASHED_CR}"
174
-
175
- secret_date=$(printf '%s' "\${DATE}" | openssl dgst -sha256 -hmac "TC3\${API_SECRET_KEY}" -hex | awk '{print $NF}')
176
- secret_svc=$(printf '%s' "\${SERVICE}" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"\${secret_date}" -hex | awk '{print $NF}')
177
- secret_sign=$(printf '%s' "tc3_request" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"\${secret_svc}" -hex | awk '{print $NF}')
178
- SIGNATURE=$(printf "\${STRING_TO_SIGN}" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"\${secret_sign}" -hex | awk '{print $NF}')
179
-
180
- AUTHZ="\${ALGORITHM} Credential=\${API_SECRET_ID}/\${CREDENTIAL_SCOPE}, SignedHeaders=\${SIGNED_HEADERS}, Signature=\${SIGNATURE}"
181
-
182
- RESP=$(curl -sS -X POST "https://\${HOST}" \\
183
- -H "Authorization: \${AUTHZ}" \\
184
- -H "Content-Type: application/json; charset=utf-8" \\
185
- -H "Host: \${HOST}" \\
186
- -H "X-TC-Action: \${ACTION}" \\
187
- -H "X-TC-Timestamp: \${TIMESTAMP}" \\
188
- -H "X-TC-Version: \${VERSION}" \\
189
- -H "X-TC-Region: \${REGION}" \\
190
- -H "X-TC-Token: \${API_TOKEN}" \\
191
- --data "\${PAYLOAD}")
192
-
193
- # CloudApp custom Runner 已提供 awk(上面的 TC3 签名也依赖它),但不保证安装 jq。
194
- # 这里先限定顶层 Response 对象,再解析其中的非空 JSON 字符串字段;凭证字段
195
- # 不允许 JSON 转义,避免用 sed/grep 对认证响应做宽松匹配。
196
- extract_json_string() {
197
- local field="$1"
198
- printf '%s' "\${RESP}" | JSON_FIELD="\${field}" awk '
199
- BEGIN {
200
- field = ENVIRON["JSON_FIELD"]
201
- quote = sprintf("%c", 34)
202
- backslash = sprintf("%c", 92)
203
- json = ""
204
- }
205
- { json = json $0 }
206
- END {
207
- response_needle = quote "Response" quote "[[:space:]]*:[[:space:]]*\\{"
208
- if (!match(json, response_needle)) exit 1
209
- response_start = RSTART + RLENGTH - 1
210
- response = ""
211
- depth = 0
212
- in_string = 0
213
- escaped = 0
214
- for (i = response_start; i <= length(json); i++) {
215
- ch = substr(json, i, 1)
216
- response = response ch
217
- if (in_string) {
218
- if (escaped) escaped = 0
219
- else if (ch == backslash) escaped = 1
220
- else if (ch == quote) in_string = 0
221
- continue
222
- }
223
- if (ch == quote) in_string = 1
224
- else if (ch == "{") depth++
225
- else if (ch == "}") {
226
- depth--
227
- if (depth == 0) break
228
- }
229
- }
230
- if (depth != 0) exit 1
231
-
232
- depth = 1
233
- for (i = 2; i < length(response); i++) {
234
- ch = substr(response, i, 1)
235
- if (ch == "{") depth++
236
- else if (ch == "}") depth--
237
- else if (ch == quote) {
238
- key = ""
239
- escaped = 0
240
- for (j = i + 1; j <= length(response); j++) {
241
- current = substr(response, j, 1)
242
- if (escaped) {
243
- escaped = 0
244
- key = key current
245
- } else if (current == backslash) {
246
- escaped = 1
247
- key = key current
248
- } else if (current == quote) break
249
- else key = key current
250
- }
251
- if (j > length(response)) exit 1
252
- k = j + 1
253
- while (substr(response, k, 1) ~ /[[:space:]]/) k++
254
- if (depth == 1 && substr(response, k, 1) == ":" && key == field) {
255
- k++
256
- while (substr(response, k, 1) ~ /[[:space:]]/) k++
257
- if (substr(response, k, 1) != quote) exit 1
258
- value = ""
259
- for (k = k + 1; k <= length(response); k++) {
260
- current = substr(response, k, 1)
261
- if (current == quote) {
262
- if (length(value) == 0) exit 1
263
- printf "%s", value
264
- exit 0
265
- }
266
- if (current == backslash) exit 2
267
- value = value current
268
- }
269
- exit 1
270
- }
271
- i = j
272
- }
273
- }
274
- exit 1
275
- }
276
- '
277
- }
278
-
279
- extract_tcr_error_code() {
280
- printf '%s' "\${RESP}" | awk '
281
- BEGIN {
282
- quote = sprintf("%c", 34)
283
- json = ""
284
- }
285
- { json = json $0 }
286
- END {
287
- error_needle = quote "Error" quote "[[:space:]]*:[[:space:]]*\\{"
288
- if (!match(json, error_needle)) exit 1
289
- error_json = substr(json, RSTART + RLENGTH)
290
- code_needle = quote "Code" quote "[[:space:]]*:[[:space:]]*" quote
291
- if (!match(error_json, code_needle)) exit 1
292
- rest = substr(error_json, RSTART + RLENGTH)
293
- value = ""
294
- for (i = 1; i <= length(rest); i++) {
295
- ch = substr(rest, i, 1)
296
- if (ch == quote) {
297
- if (length(value) == 0) exit 1
298
- printf "%s", value
299
- exit 0
300
- }
301
- if (ch == backslash) exit 1
302
- value = value ch
303
- }
304
- exit 1
305
- }
306
- '
307
- }
308
-
309
- ERROR_CODE=$(extract_tcr_error_code || true)
310
- if [ -n "\${ERROR_CODE}" ]; then
311
- case "\${ERROR_CODE}" in
312
- AuthFailure*|UnauthorizedOperation*|FailedOperation.NoPermission*)
313
- echo "企业版 TCR 鉴权失败,请确认当前身份有权访问该实例并调用 CreateInstanceToken" >&2
314
- exit ${TCR_REGISTRY_AUTH_EXIT_CODE}
315
- ;;
316
- ResourceNotFound*|InvalidParameterValue.RegistryId*)
317
- echo "企业版 TCR 实例不存在或实例 ID 无效" >&2
318
- exit ${TCR_REGISTRY_INVALID_EXIT_CODE}
319
- ;;
320
- *)
321
- echo "企业版 TCR 获取登录凭证失败:\${ERROR_CODE}" >&2
322
- exit 1
323
- ;;
324
- esac
325
- fi
326
-
327
- if ! TOKEN_USER=$(extract_json_string "Username"); then
328
- ERROR_CODE=$(extract_tcr_error_code || true)
329
- echo "CreateInstanceToken 未返回有效 Username\${ERROR_CODE:+,错误码:\${ERROR_CODE}}" >&2
330
- exit ${TCR_REGISTRY_AUTH_EXIT_CODE}
331
- fi
332
- if ! TOKEN_PASS=$(extract_json_string "Token"); then
333
- ERROR_CODE=$(extract_tcr_error_code || true)
334
- echo "CreateInstanceToken 未返回有效 Token\${ERROR_CODE:+,错误码:\${ERROR_CODE}}" >&2
335
- exit ${TCR_REGISTRY_AUTH_EXIT_CODE}
336
- fi
337
-
338
- printf '%s' "\${TOKEN_PASS}" | docker login -u "\${TOKEN_USER}" --password-stdin "\${TCR_REGISTRY}"
339
- else
340
- # ---- 个人版 CCR:账号 ID + 固定密码(Base64 封装后由 CloudApp Secrets 注入) ----
341
- IS_PERSONAL_REGISTRY=1
342
- : "\${TCR_USERNAME:?TCR_USERNAME 未设置}"
343
- : "\${SECRET_TCR_PASSWORD_B64:?TCR_PASSWORD 未设置}"
344
- printf '%s' "\${SECRET_TCR_PASSWORD_B64}" | base64 --decode | docker login -u "\${TCR_USERNAME}" --password-stdin "\${TCR_REGISTRY}"
345
- fi
346
-
347
- PUSH_LOG=$(mktemp)
348
- trap 'rm -f "\${PUSH_LOG}"' EXIT
349
- set +e
350
- docker push "\${IMAGE}" 2>&1 | tee "\${PUSH_LOG}"
351
- PUSH_STATUS=\${PIPESTATUS[0]}
352
- set -e
353
-
354
- if [ "\${PUSH_STATUS}" -ne 0 ]; then
355
- if [ "\${IS_PERSONAL_REGISTRY}" -eq 1 ] && {
356
- grep -Fq 'LimitExceeded.ErrRepoMaxLimit' "\${PUSH_LOG}" ||
357
- grep -Fq '用户仓库已经达到最大配额' "\${PUSH_LOG}"
358
- }; then
359
- echo "个人版 TCR 镜像仓库数量已达到当前地域配额,无法自动创建目标仓库" >&2
360
- exit ${TCR_PERSONAL_REPOSITORY_QUOTA_EXIT_CODE}
361
- fi
362
- exit 1
363
- fi
364
-
365
- echo "::output::tag=\${CLOUDBASE_VERSION_NAME}"
366
- `;
367
111
  function resolveTarget(config) {
368
112
  const build = config.build;
369
113
  const repository = (build.repository || config.name).trim();
@@ -384,10 +128,6 @@ function resolveTarget(config) {
384
128
  let hasExplicitRegistry = false;
385
129
  let namespace = (build.namespace || '').trim();
386
130
  let imageName = repository;
387
- if (!isEnterprise && !namespace) {
388
- throw new error_1.CloudBaseError('个人版 TCR 云端构建缺少 build.namespace;请在 cloudbaserc 中显式声明,' +
389
- '推荐使用 "{{env.TCB_TCR_NAMESPACE}}"', { code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID });
390
- }
391
131
  if (repository.includes('/')) {
392
132
  assertSafeRegistryToken(repository, 'build.repository');
393
133
  const parts = repository.split('/').filter(Boolean);
@@ -465,14 +205,23 @@ function resolveCloudBuildOptions(config) {
465
205
  *
466
206
  * Dockerfile、platform 和 build arg value 通过 CloudApp Env 注入;只有通过严格
467
207
  * ENV key 校验的 build arg key 会进入固定 command。forceBuild 显式映射为 --no-cache。
208
+ *
209
+ * push-image 直接在 CloudApp 构建容器内执行 docker login 与 docker push:
210
+ * 登录凭证一律经 --password-stdin 传入,不落 ps/日志,也不再向构建上下文注入脚本文件。
468
211
  */
469
- function buildCustomSteps(options) {
212
+ function buildCustomSteps(options, isEnterpriseRegistry) {
470
213
  const buildArgs = Object.keys(options.buildArgs)
471
214
  .sort()
472
215
  .map(key => `--build-arg ${key}="$BUILD_ARG_${key}"`)
473
216
  .join(' ');
474
217
  const noCache = options.forceBuild ? ' --no-cache' : '';
475
218
  const extraArgs = buildArgs ? ` ${buildArgs}` : '';
219
+ const image = '"$TCR_REGISTRY/$TCR_NAMESPACE/$CLOUDBASE_SERVICE_NAME:$CLOUDBASE_VERSION_NAME"';
220
+ // 企业版临时登录凭证由 manager-node 在 CreateCloudApp 前换取并经 CloudApp Secrets 注入;
221
+ // 个人版沿用账号 ID 与 Base64 封装的固定密码 Secret,容器内解码后直接送入 stdin。
222
+ const loginCommand = isEnterpriseRegistry
223
+ ? 'printf \'%s\' "$SECRET_TCR_TOKEN_PASSWORD" | docker login -u "$SECRET_TCR_TOKEN_USERNAME" --password-stdin "$TCR_REGISTRY"'
224
+ : 'printf \'%s\' "$SECRET_TCR_PASSWORD_B64" | base64 --decode | docker login -u "$TCR_USERNAME" --password-stdin "$TCR_REGISTRY"';
476
225
  return [
477
226
  {
478
227
  name: 'build-image',
@@ -481,24 +230,17 @@ function buildCustomSteps(options) {
481
230
  ' -f "$CLOUDBASE_DOCKERFILE"' +
482
231
  noCache +
483
232
  extraArgs +
484
- ' -t $TCR_REGISTRY/$TCR_NAMESPACE/$CLOUDBASE_SERVICE_NAME:$CLOUDBASE_VERSION_NAME .'
233
+ ` -t ${image} .`
485
234
  },
486
235
  {
487
236
  name: 'push-image',
488
- command: 'bash ./scripts/tcr-login-and-push.sh'
237
+ command: `${loginCommand} && docker push ${image}`
489
238
  }
490
239
  ];
491
240
  }
492
- /**
493
- * 组装 custom 构建的环境变量(用户可控值已校验,此处只做键值透传)
494
- *
495
- * - TCR_REGION:部署地域,供推送脚本换取镜像仓库登录态时签名/请求用,
496
- * 缺省交由脚本回退默认地域;参数化后跨地域不会 SignatureFailure。
497
- * - TCR_INSTANCE_ID:仅企业版 TCR 注入,脚本据此走 CreateInstanceToken 分支;
498
- * 个人版 CCR 不注入,脚本走 STS 直接 docker login 分支。
499
- */
500
- function buildEnv(target, buildOptions, region) {
501
- const env = [
241
+ /** 组装 custom 构建的环境变量(用户可控值已校验,此处只做键值透传) */
242
+ function buildEnv(target, buildOptions) {
243
+ return [
502
244
  { key: 'TCR_REGISTRY', value: target.registry },
503
245
  { key: 'TCR_NAMESPACE', value: target.namespace },
504
246
  { key: 'CLOUDBASE_DOCKERFILE', value: buildOptions.dockerfile },
@@ -508,23 +250,25 @@ function buildEnv(target, buildOptions, region) {
508
250
  value
509
251
  }))
510
252
  ];
511
- if (region) {
512
- env.push({ key: 'TCR_REGION', value: region });
513
- }
514
- if (target.registryId) {
515
- assertSafeRegistryToken(target.registryId, 'registryId');
516
- env.push({ key: 'TCR_INSTANCE_ID', value: target.registryId });
517
- }
518
- return env;
519
253
  }
520
254
  /**
521
- * 组装个人版 TCR 凭证。
522
- * username 可作为普通构建变量;password 必须进入 CloudApp Secrets。
255
+ * 组装 TCR 登录凭证。
256
+ * 企业版使用 manager-node 预先换取的临时 Username/Token;个人版 username 可作为
257
+ * 普通构建变量,password 必须经 Base64 封装后进入 CloudApp Secrets。
523
258
  */
524
- function buildRegistryCredentials(config, target) {
259
+ function buildRegistryCredentials(config, target, enterpriseCredential) {
525
260
  var _a;
526
261
  if (target.registryId) {
527
- return { env: [] };
262
+ if (!(enterpriseCredential === null || enterpriseCredential === void 0 ? void 0 : enterpriseCredential.username) || !(enterpriseCredential === null || enterpriseCredential === void 0 ? void 0 : enterpriseCredential.token)) {
263
+ throw new error_1.CloudBaseError('企业版 TCR 云端构建缺少临时登录凭证,请确认当前身份具备 CreateInstanceToken 权限', { code: types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_MISSING });
264
+ }
265
+ return {
266
+ env: [],
267
+ secrets: [
268
+ { name: 'TCR_TOKEN_USERNAME', value: enterpriseCredential.username },
269
+ { name: 'TCR_TOKEN_PASSWORD', value: enterpriseCredential.token }
270
+ ]
271
+ };
528
272
  }
529
273
  const credential = config.build.registryCredential;
530
274
  if (!credential) {
@@ -581,35 +325,9 @@ function resolveImageUri(target, versionName, artifacts) {
581
325
  const imageUri = `${target.registry}/${target.namespace}/${target.serviceName}:${versionName}`;
582
326
  return { imageUri, imageDigest: imageArtifact === null || imageArtifact === void 0 ? void 0 : imageArtifact.Digest };
583
327
  }
584
- /** 判断企业版 TCR 推送步骤是否因实例鉴权失败 */
585
- function isRegistryAuthFailure(steps) {
586
- return (steps || []).some(step => {
587
- var _a;
588
- return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' &&
589
- ((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed' &&
590
- step.ExitCode === TCR_REGISTRY_AUTH_EXIT_CODE;
591
- });
592
- }
593
- /** 判断企业版 TCR 推送步骤是否因实例 ID 无效失败 */
594
- function isRegistryIdFailure(steps) {
595
- return (steps || []).some(step => {
596
- var _a;
597
- return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' &&
598
- ((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed' &&
599
- step.ExitCode === TCR_REGISTRY_INVALID_EXIT_CODE;
600
- });
601
- }
602
- /** 判断个人版 TCR 是否因自动创建目标仓库时达到仓库数量配额而失败 */
603
- function isPersonalRepositoryQuotaFailure(target, steps) {
604
- if (target.registryId) {
605
- return false;
606
- }
607
- return (steps || []).some(step => {
608
- var _a;
609
- return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' &&
610
- ((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed' &&
611
- step.ExitCode === TCR_PERSONAL_REPOSITORY_QUOTA_EXIT_CODE;
612
- });
328
+ /** 判断失败是否发生在 push-image 步骤,用于给出针对性的排查引导 */
329
+ function isPushStepFailure(steps) {
330
+ return (steps || []).some(step => { var _a; return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' && ((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed'; });
613
331
  }
614
332
  /** 汇总失败 Step 的定位信息(脱敏:只取步骤名/状态/退出码,不含日志明文) */
615
333
  function summarizeFailedSteps(steps) {
@@ -629,25 +347,19 @@ function summarizeFailedSteps(steps) {
629
347
  *
630
348
  * @param config 规范化的 cloud 部署配置
631
349
  * @param service 注入的外部能力(打包/tcb API/COS 上传)
632
- * @param options 可选项:日志回调 onLog、部署地域 region
350
+ * @param options 可选项:日志回调 onLog、企业版临时登录凭证
633
351
  */
634
352
  async function buildImageOnCloud(config, service, options = {}) {
635
- const { onLog, region } = options;
353
+ const { onLog, enterpriseCredential } = options;
636
354
  const target = resolveTarget(config);
637
355
  const buildOptions = resolveCloudBuildOptions(config);
638
356
  // 在打包和上传前完成真实消费字段校验,避免无效配置触发远端资源操作。
639
- const registryCredentials = buildRegistryCredentials(config, target);
357
+ const registryCredentials = buildRegistryCredentials(config, target, enterpriseCredential);
640
358
  const serviceName = target.serviceName;
641
359
  const cwd = resolveCloudBuildContext(config, buildOptions.dockerfile);
642
- // 1. 打包构建上下文(注入 push 脚本,不污染用户源码目录)
360
+ // 1. 只打包用户构建上下文;登录推送逻辑已收敛到 CloudApp CustomStep 命令
643
361
  onLog === null || onLog === void 0 ? void 0 : onLog('打包构建上下文');
644
- const zipPath = await service.packContext(cwd, [
645
- {
646
- zipPath: exports.TCR_PUSH_SCRIPT_ZIP_PATH,
647
- content: exports.TCR_LOGIN_PUSH_SCRIPT,
648
- mode: 0o755
649
- }
650
- ]);
362
+ const zipPath = await service.packContext(cwd);
651
363
  try {
652
364
  // 2. 获取 COS 上传凭证
653
365
  onLog === null || onLog === void 0 ? void 0 : onLog('获取 COS 上传凭证');
@@ -665,12 +377,9 @@ async function buildImageOnCloud(config, service, options = {}) {
665
377
  const created = await service.createBuild({
666
378
  serviceName,
667
379
  cosTimestamp: cosInfo.unixTimestamp,
668
- env: [
669
- ...buildEnv(target, buildOptions, region),
670
- ...registryCredentials.env
671
- ],
380
+ env: [...buildEnv(target, buildOptions), ...registryCredentials.env],
672
381
  secrets: registryCredentials.secrets,
673
- customSteps: buildCustomSteps(buildOptions)
382
+ customSteps: buildCustomSteps(buildOptions, Boolean(target.registryId))
674
383
  });
675
384
  if (!(created === null || created === void 0 ? void 0 : created.versionName)) {
676
385
  throw new error_1.CloudBaseError('触发云端构建失败:返回缺少 VersionName', {
@@ -693,27 +402,15 @@ async function buildImageOnCloud(config, service, options = {}) {
693
402
  const reqId = finalStatus.requestId
694
403
  ? `,requestId=${finalStatus.requestId}`
695
404
  : '';
696
- const registryAuthFailure = isRegistryAuthFailure(finalStatus.steps);
697
- const registryIdFailure = isRegistryIdFailure(finalStatus.steps);
698
- const personalRepositoryQuotaFailure = isPersonalRepositoryQuotaFailure(target, finalStatus.steps);
405
+ // 登录凭证与目标仓库均在触发构建前由 manager-node 校验并准备,
406
+ // 因此这里只区分取消与失败,具体原因以构建日志中的 docker 输出为准。
699
407
  const code = finalStatus.status === 'CANCELED'
700
408
  ? types_1.FUNCTION_DEPLOY_ERROR.CLOUD_BUILD_CANCELED
701
- : registryAuthFailure
702
- ? types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_AUTH_FAILED
703
- : registryIdFailure
704
- ? types_1.FUNCTION_DEPLOY_ERROR.IMAGE_REGISTRY_ID_INVALID
705
- : personalRepositoryQuotaFailure
706
- ? types_1.FUNCTION_DEPLOY_ERROR.CLOUD_PERSONAL_REGISTRY_REPOSITORY_QUOTA_EXCEEDED
707
- : types_1.FUNCTION_DEPLOY_ERROR.CLOUD_BUILD_FAILED;
708
- const guidance = finalStatus.status === 'CANCELED'
409
+ : types_1.FUNCTION_DEPLOY_ERROR.CLOUD_BUILD_FAILED;
410
+ const guidance = finalStatus.status === 'CANCELED' || !isPushStepFailure(finalStatus.steps)
709
411
  ? ''
710
- : registryAuthFailure
711
- ? ';企业版 TCR 鉴权失败,请确认实例属于当前账号/地域,且当前身份拥有 CreateInstanceToken 和仓库推送权限'
712
- : registryIdFailure
713
- ? ';企业版 TCR 实例不存在或实例 ID 无效,请检查 build.registryId 对应的环境变量和实例状态'
714
- : personalRepositoryQuotaFailure
715
- ? ';个人版 TCR 当前地域的镜像仓库数量已达到配额,目标仓库无法自动创建。请在容器镜像服务个人版中删除不再使用的仓库(删除前请备份其中镜像),或将 build.repository 改为已有仓库;如需更多仓库,请切换企业版 TCR。实际配额请以控制台为准'
716
- : '';
412
+ : ';推送镜像失败,请在云开发控制台查看构建日志中的 docker login/push 输出,' +
413
+ '并确认镜像仓库凭证、命名空间与仓库配额';
717
414
  throw new error_1.CloudBaseError(`云端构建未成功(${finalStatus.status})${detail}${reqId}${guidance}`, { code });
718
415
  }
719
416
  // 6. 得到不可变镜像地址与digest
@@ -60,32 +60,18 @@ const DEFAULT_IGNORE = [
60
60
  function createCloudBuildService(cloudApp, options = {}) {
61
61
  const ignore = [...DEFAULT_IGNORE, ...(options.ignore || [])];
62
62
  return {
63
- async packContext(cwd, extraEntries) {
63
+ async packContext(cwd) {
64
64
  if (!fs_1.default.existsSync(cwd) || !fs_1.default.statSync(cwd).isDirectory()) {
65
65
  throw new error_1.CloudBaseError(`构建上下文目录不存在或不是目录:${cwd}`, {
66
66
  code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_CONTEXT_INVALID
67
67
  });
68
68
  }
69
69
  const zipPath = path_1.default.join(os_1.default.tmpdir(), `fn-cloud-build-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`);
70
- const injectedEntries = (extraEntries || []).map(entry => ({
71
- zipPath: entry.zipPath.replace(/\\/g, '/').replace(/^\.\//, ''),
72
- content: entry.content,
73
- mode: entry.mode
74
- }));
75
- // 源码中若存在与 SDK 注入文件同名的路径,必须从 glob 中排除,
76
- // 避免 ZIP 出现重复 entry 后由解压顺序决定最终执行内容。
77
- const contextIgnore = [
78
- ...ignore,
79
- ...injectedEntries.map(entry => entry.zipPath)
80
- ];
81
- // 把 SDK 注入的辅助文件(如 push 脚本)随源码一起打进zip,
82
- // 通过临时文件注入,绝不写入用户源码目录。
83
70
  try {
84
71
  await (0, utils_1.compressToZip)({
85
72
  dirPath: cwd,
86
73
  outputPath: zipPath,
87
- ignore: contextIgnore,
88
- extraEntries: injectedEntries
74
+ ignore
89
75
  });
90
76
  }
91
77
  catch (error) {