@xfe-repo/web-app 2.0.0-beta.4 → 2.0.0-beta.5

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/README.md CHANGED
@@ -126,7 +126,7 @@ SSR `start` 使用与构建一致的隐藏工作目录,使官方 serve 正确
126
126
 
127
127
  ## 可选的项目部署模板
128
128
 
129
- 已有项目保持默认 Node 22、集群选择、发布环境和无状态部署方式。设置 `deploy.managed: true` 后,`xfe-web config` 将框架模板生成到项目 `deploy/`,不修改 npm 包中的 Helm values;项目只维护配置,生成文件由框架管理。Drone 入口需提交仓库,安装后的流水线再通过同一命令生成模板。
129
+ `xfe-web config` 仅合并并写入 values、复制公共模板,不解析或重建 Drone 步骤。`managed: true` 时写到项目 `deploy/`,不修改共享 npm 包模板;未启用时保持原有行为。
130
130
 
131
131
  ```json
132
132
  {
@@ -135,11 +135,11 @@ SSR `start` 使用与构建一致的隐藏工作目录,使官方 serve 正确
135
135
  "droneConfigPath": "deploy/.drone.yml",
136
136
  "targetK8s": "infra",
137
137
  "targetNamespace": "deployment-prod",
138
- "nodeVersion": "24.22.1",
139
- "environments": ["prod"],
140
138
  "hostsProd": ["app.example.com"],
141
- "buildCommand": "pnpm check && pnpm build && pnpm prune --prod",
142
139
  "values": {
140
+ "nodeVersion": "24.22.1",
141
+ "buildCommand": "pnpm check && pnpm build && pnpm prune --prod",
142
+ "environments": {"test": false, "stage": false, "prod": true, "stable": false},
143
143
  "replicas": 1,
144
144
  "strategy": {"type": "Recreate"},
145
145
  "persistence": {"enabled": true, "size": "20Gi", "mountPath": "/data"},
@@ -151,6 +151,8 @@ SSR `start` 使用与构建一致的隐藏工作目录,使官方 serve 正确
151
151
  }
152
152
  ```
153
153
 
154
- `values` 递归覆盖默认 Helm values;列表整体替换。可配置实例数、更新策略、容器资源、Pod/容器安全上下文、环境变量及来源、启动/就绪/存活探针、终止宽限期和 PVC。PVC 默认关闭,可指定 `existingClaim` 或 `storageClass`。应用负责声明自己的 `DATA_DIR` 等业务变量,框架不包含 OSS 或认证逻辑。
154
+ `values` 中的对象递归合并,列表整体替换。默认 Node 22.22.0、`pnpm build` 和所有发布环境开启,保持既有应用行为。公共 Drone 保留固定步骤,每个发布步骤在运行时检查 `environments` 布尔开关,关闭时直接结束,不创建命名空间或发布资源。
155
155
 
156
- `nodeVersion` 同时应用于安装、构建及运行镜像,省略时保留 22.22.0。`environments` 可选择 test/stage/prod/stable,省略时保持原流程;仅 prod 时不生成其他发布步骤。managed 正式发布使用 Helm 等待就绪,失败自动撤销资源变更。`virtualService.timeout` 可设置长连接超时,`corsPolicy: null` 可关闭默认跨域策略。密钥只通过运行环境提供,不放入 values 或构建命令。
156
+ 构建命令与 Docker 的 Node 参数由 Drone 直接读取生成的 values;实例、更新策略、PVC、环境变量、安全上下文、探针和路由由 Helm 读取。只有运行前就必须确定的 Node 镜像、集群 Secret,以及原有构建目录参数使用简单占位替换,不再有配置脚本中的流水线遍历或筛选。
157
+
158
+ PVC 默认关闭,支持 `existingClaim`、`storageClass`。`virtualService.timeout` 可配置长连接超时,`corsPolicy: null` 关闭默认跨域策略。应用负责自己的存储与认证业务变量,密钥通过环境来源注入,不写入 values 或构建命令。修改配置后重新生成并提交 Drone 入口。
package/deploy/.drone.yml CHANGED
@@ -18,7 +18,7 @@ resources:
18
18
  steps:
19
19
  - name: install
20
20
  pull: if-not-exists
21
- image: node:22.22.0-bookworm
21
+ image: node:<<NODE_VERSION>>-bookworm
22
22
  volumes:
23
23
  - name: node-modules
24
24
  path: /drone/src/node_modules
@@ -30,7 +30,7 @@ steps:
30
30
 
31
31
  - name: build
32
32
  pull: if-not-exists
33
- image: node:22.22.0-bookworm
33
+ image: node:<<NODE_VERSION>>-bookworm
34
34
  resources:
35
35
  limits:
36
36
  memory: 8192MiB
@@ -40,13 +40,31 @@ steps:
40
40
  commands:
41
41
  - corepack enable
42
42
  - corepack prepare pnpm@10.30.3 --activate
43
- - cp -r node_modules/@xfe-repo/web-app/deploy deploy
43
+ - |
44
+ if [ "$(node -p "Boolean(require('./xfe.json').deploy?.managed)")" = true ]; then
45
+ pnpm exec xfe-web config
46
+ else
47
+ cp -r node_modules/@xfe-repo/web-app/deploy deploy
48
+ fi
44
49
  - export NODE_OPTIONS="--max-old-space-size=8192"
45
- - export XFE_RELEASE=$DRONE_REPO_NAME@${DRONE_TAG:CI_COMMIT_SHA}
50
+ - export XFE_RELEASE="$DRONE_REPO_NAME@$DRONE_COMMIT_SHA"
51
+ - if [ -n "$DRONE_TAG" ]; then export XFE_RELEASE="$DRONE_REPO_NAME@$DRONE_TAG"; fi
46
52
  - export REACT_APP_RELEASE=$XFE_RELEASE
47
53
  - printf '%s' "$XFE_RELEASE" > deploy/static/release
48
54
  - cp deploy/static/release deploy/static/check-update
49
- - pnpm build
55
+ - |
56
+ node <<'NODE'
57
+ const fs = require('node:fs');
58
+ const {createRequire} = require('node:module');
59
+ const yaml = createRequire(require.resolve('@xfe-repo/web-app/package.json'))('yaml');
60
+ const values = yaml.parse(fs.readFileSync('deploy/helm/values.yaml', 'utf8'));
61
+ const dockerfile = fs.readFileSync('deploy/Dockerfile', 'utf8');
62
+ fs.writeFileSync('deploy/Dockerfile', dockerfile.replace(/^ARG XFE_NODE_VERSION=.*$/m, 'ARG XFE_NODE_VERSION=' + values.nodeVersion));
63
+ const {spawnSync} = require('node:child_process');
64
+ const result = spawnSync(values.buildCommand, {shell: true, stdio: 'inherit'});
65
+ if (result.error) throw result.error;
66
+ process.exit(result.status ?? 1);
67
+ NODE
50
68
  - cp -a node_modules deploy/modules
51
69
 
52
70
  - name: image-build
@@ -70,26 +88,31 @@ steps:
70
88
  - name: pre-deploy
71
89
  image: xhj-prod-registry-vpc.cn-hangzhou.cr.aliyuncs.com/xhj-image-common/deploy-helm:latest
72
90
  environment:
73
- SAAS_K8S_CONFIG:
74
- from_secret: saas-k8s-config
75
- INFRA_K8S_CONFIG:
76
- from_secret: infra-k8s-config
91
+ K8S_CONFIG:
92
+ from_secret: target-k8s-config
77
93
  commands:
78
- # 判断values.yaml中的targetK8s字段,选择目标集群
79
- - export TARGET_K8S=$(cat deploy/helm/values.yaml | grep targetK8s | awk '{print $2}' | sed 's/\"//g')
80
- # 将$INFRA_K8S_CONFIG或$SAAS_K8S_CONFIG写入kube_config
81
- - echo "$TARGET_K8S" | grep -q "infra" && echo "$INFRA_K8S_CONFIG" > kube_config || echo "$SAAS_K8S_CONFIG" > kube_config
94
+ # 集群凭据通过生成入口选定,运行流程不再注入其他集群的凭据。
95
+ - umask 077
96
+ - printf '%s' "$K8S_CONFIG" > kube_config
82
97
  # 将环境类型写入values.yaml
83
98
  - appName="`echo ${DRONE_REPO//\//-}`"
84
99
  - sed -i "s/<<APP_NAME>>/$appName/g" deploy/helm/values.yaml
85
100
  - sed -i "s/<<CI_COMMIT_SHA>>/$CI_COMMIT_SHA/g" deploy/helm/values.yaml
86
101
  - sed -i "s/<<TAG_CONTENT>>/$DRONE_TAG/g" deploy/helm/values.yaml
87
- # 展示values.yaml 与 TARGET_K8S
88
- - cat deploy/helm/values.yaml && echo $TARGET_K8S
102
+ # 展示非敏感部署配置
103
+ - cat deploy/helm/values.yaml
89
104
 
90
105
  - name: deploy-to-test
91
106
  image: xhj-prod-registry-vpc.cn-hangzhou.cr.aliyuncs.com/xhj-image-common/deploy-helm:latest
107
+ environment:
108
+ DEPLOY_ENV: test
92
109
  commands:
110
+ - &environmentEnabled |
111
+ test -f deploy/helm/values.yaml || exit 1
112
+ if ! sed -n '/^environments:/,/^[^ ]/p' deploy/helm/values.yaml | grep -q "^ $DEPLOY_ENV: true$"; then
113
+ echo "跳过未启用的发布环境:$DEPLOY_ENV"
114
+ exit 0
115
+ fi
93
116
  - export KUBECONFIG=$PWD/kube_config
94
117
  - cp deploy/helm/values.yaml deploy/helm/values.test.yaml
95
118
  - sed -i "s/<<ENV_TYPE>>/test/g" deploy/helm/values.test.yaml
@@ -106,7 +129,10 @@ steps:
106
129
 
107
130
  - name: deploy-to-stage
108
131
  image: xhj-prod-registry-vpc.cn-hangzhou.cr.aliyuncs.com/xhj-image-common/deploy-helm:latest
132
+ environment:
133
+ DEPLOY_ENV: stage
109
134
  commands:
135
+ - *environmentEnabled
110
136
  - cp deploy/helm/values.yaml deploy/helm/values.stage.yaml
111
137
  - sed -i "s/<<ENV_TYPE>>/stage/g" deploy/helm/values.stage.yaml
112
138
  - sed -i "s/<<APP_SUFFIX>>//g" deploy/helm/values.stage.yaml
@@ -120,13 +146,24 @@ steps:
120
146
 
121
147
  - name: deploy-to-prod
122
148
  image: xhj-prod-registry-vpc.cn-hangzhou.cr.aliyuncs.com/xhj-image-common/deploy-helm:latest
149
+ environment:
150
+ DEPLOY_ENV: prod
123
151
  commands:
152
+ - *environmentEnabled
124
153
  - cp deploy/helm/values.yaml deploy/helm/values.prod.yaml
125
154
  - sed -i "s/<<ENV_TYPE>>/prod/g" deploy/helm/values.prod.yaml
126
155
  - sed -i "s/<<APP_SUFFIX>>//g" deploy/helm/values.prod.yaml
127
156
  - sed -i "s/<<HOST_PREFIX>>//g" deploy/helm/values.prod.yaml
128
157
  - helm template deploy/helm --values deploy/helm/values.prod.yaml
129
- - helm template deploy/helm --values deploy/helm/values.prod.yaml | kubectl --kubeconfig=kube_config apply -f -
158
+ - |
159
+ if grep -q '^managed: true$' deploy/helm/values.yaml; then
160
+ namespace=$(awk '$1 == "namespace:" { gsub(/[\047\042]/, "", $2); print $2 }' deploy/helm/values.yaml)
161
+ if [ -z "$namespace" ]; then namespace=deployment-prod; fi
162
+ chart_name=$(printf '%s' "$DRONE_REPO" | tr '/' '-')
163
+ helm upgrade --install "$chart_name" deploy/helm --values deploy/helm/values.prod.yaml --namespace "$namespace" --create-namespace --kubeconfig=kube_config --atomic --wait --timeout 5m
164
+ else
165
+ helm template deploy/helm --values deploy/helm/values.prod.yaml | kubectl --kubeconfig=kube_config apply -f -
166
+ fi
130
167
  when:
131
168
  ref:
132
169
  include:
@@ -135,7 +172,10 @@ steps:
135
172
  - name: deploy-to-stable
136
173
  image: xhj-prod-registry-vpc.cn-hangzhou.cr.aliyuncs.com/xhj-image-common/deploy-helm:latest
137
174
  failure: ignore
175
+ environment:
176
+ DEPLOY_ENV: stable
138
177
  commands:
178
+ - *environmentEnabled
139
179
  - cp deploy/helm/values.yaml deploy/helm/values.test.yaml
140
180
  - sed -i "s/<<ENV_TYPE>>/test/g" deploy/helm/values.test.yaml
141
181
  - sed -i "s/<<APP_SUFFIX>>/-stable/g" deploy/helm/values.test.yaml
@@ -150,11 +190,13 @@ steps:
150
190
  - name: push-helm-chart
151
191
  image: xhj-prod-registry-vpc.cn-hangzhou.cr.aliyuncs.com/xhj-image-common/deploy-helm:latest
152
192
  environment:
193
+ DEPLOY_ENV: prod
153
194
  USERNAME:
154
195
  from_secret: username
155
196
  PASSWORD:
156
197
  from_secret: password
157
198
  commands:
199
+ - *environmentEnabled
158
200
  - chartName="${DRONE_REPO//\//-}"
159
201
  - chartVersion="0.0.0-$DRONE_TAG"
160
202
  - cp deploy/helm/values.prod.yaml deploy/helm/values.yaml
@@ -183,15 +225,9 @@ image_pull_secrets:
183
225
 
184
226
  ---
185
227
  kind: secret
186
- name: saas-k8s-config
187
- get:
188
- path: saas-k8s-config
189
- name: config
190
- ---
191
- kind: secret
192
- name: infra-k8s-config
228
+ name: target-k8s-config
193
229
  get:
194
- path: infra-k8s-config
230
+ path: <<TARGET_K8S>>-k8s-config
195
231
  name: config
196
232
  ---
197
233
  kind: secret
@@ -29,3 +29,13 @@ persistence:
29
29
  size: 20Gi
30
30
  existingClaim: ''
31
31
  storageClass: ''
32
+
33
+ # 构建与发布配置由 Drone 在运行时读取,默认保持原有发布范围。
34
+ managed: false
35
+ nodeVersion: '22.22.0'
36
+ buildCommand: pnpm build
37
+ environments:
38
+ test: true
39
+ stage: true
40
+ prod: true
41
+ stable: true
package/dist/cli.js CHANGED
@@ -31,69 +31,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
 
32
32
  // src/config.ts
33
33
  function mergeDeployValues(base, overrides) {
34
- const entries = Object.entries({ ...base, ...overrides }).map(
35
- /** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
34
+ return Object.fromEntries(Object.entries({ ...base, ...overrides }).map(
35
+ /** 仅合并普通对象,列表保持声明的顺序。 */
36
36
  ([key, value]) => {
37
37
  const previous = base[key];
38
38
  const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
39
39
  return [key, mergeable ? mergeDeployValues(previous, value) : value];
40
40
  }
41
- );
42
- return Object.fromEntries(entries);
43
- }
44
- function configureDeployPipeline(template, deployment) {
45
- if (!deployment.managed && !deployment.nodeVersion && !deployment.environments && !deployment.buildCommand) return template;
46
- const documents = import_yaml.default.parseAllDocuments(template).map(
47
- /** 保留镜像和集群 Secret 定义。 */
48
- (document) => document.toJSON()
49
- );
50
- const pipeline = documents[0];
51
- const environments = deployment.environments;
52
- const known = ["test", "stage", "prod", "stable"];
53
- if (environments && (!Array.isArray(environments) || !environments.length || environments.some(
54
- /** 拒绝未声明的发布环境名称。 */
55
- (environment) => typeof environment !== "string" || !known.includes(environment)
56
- ))) throw new Error("deploy.environments \u5FC5\u987B\u662F test/stage/prod/stable \u7684\u975E\u7A7A\u6570\u7EC4");
57
- if (deployment.nodeVersion && !/^\d+\.\d+\.\d+$/.test(deployment.nodeVersion)) throw new Error("deploy.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
58
- if (deployment.managed && deployment.targetNamespace && !/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(deployment.targetNamespace)) throw new Error("deploy.targetNamespace \u5FC5\u987B\u662F\u6709\u6548 Kubernetes \u547D\u540D\u7A7A\u95F4");
59
- const selected = environments ? new Set(environments) : void 0;
60
- const stepEnvironments = /* @__PURE__ */ new Map([["deploy-to-test", "test"], ["deploy-to-stage", "stage"], ["deploy-to-prod", "prod"], ["deploy-to-stable", "stable"], ["push-helm-chart", "prod"]]);
61
- pipeline.steps = pipeline.steps.filter(
62
- /** 通用安装和构建步骤保留,发布步骤按显式环境选择。 */
63
- (step) => !selected || !stepEnvironments.has(step.name) || selected.has(stepEnvironments.get(step.name))
64
- );
65
- for (const step of pipeline.steps) {
66
- if (deployment.nodeVersion && ["install", "build"].includes(step.name)) step.image = `node:${deployment.nodeVersion}-bookworm`;
67
- if (deployment.nodeVersion && step.name === "image-build") step.settings.build_args.push(`XFE_NODE_VERSION=${deployment.nodeVersion}`);
68
- if (step.name === "build") step.commands = step.commands.map(
69
- /** managed 构建复用配置生成器;应用自定义检查仍由应用命令负责。 */
70
- (command2) => {
71
- if (deployment.managed && command2 === "cp -r node_modules/@xfe-repo/web-app/deploy deploy") return "pnpm exec xfe-web config";
72
- return command2 === "pnpm build" ? deployment.buildCommand || command2 : command2;
73
- }
74
- );
75
- if (deployment.managed && deployment.targetK8s === "infra" && step.name === "pre-deploy") delete step.environment.SAAS_K8S_CONFIG;
76
- if (deployment.managed && step.name === "deploy-to-prod") step.commands = step.commands.map(
77
- /** 正式 Helm 部署等待探针并在失败时撤销资源变更。 */
78
- (command2) => command2.includes("| kubectl") ? `helm upgrade --install "\${DRONE_REPO//\\//-}" deploy/helm --values deploy/helm/values.prod.yaml --namespace ${deployment.targetNamespace || "deployment-prod"} --create-namespace --kubeconfig=kube_config --atomic --wait --timeout 5m` : command2
79
- );
80
- }
81
- if (selected && !selected.has("test")) {
82
- pipeline.trigger.event = ["tag"];
83
- const tags = [...new Set([...selected].map(
84
- /** prod 与 stable 共用正式标签,其余使用各自的标签。 */
85
- (environment) => environment === "stage" ? "refs/tags/stage-*" : "refs/tags/release-*"
86
- ))];
87
- pipeline.trigger.ref = { include: tags };
88
- }
89
- const result = deployment.managed && deployment.targetK8s === "infra" ? documents.filter(
90
- /** infra 独立生成的流水线不要求额外的 saas kubeconfig。 */
91
- (document) => document.kind !== "secret" || document.name !== "saas-k8s-config"
92
- ) : documents;
93
- return result.map(
94
- /** 序列化多文档流水线,保留 Kubernetes Runner 的 Secret 引用。 */
95
- (document) => import_yaml.default.stringify(document)
96
- ).join("---\n");
41
+ ));
97
42
  }
98
43
  var import_node_fs, import_node_path, import_node_url, import_yaml, import_meta, DEFAULT_BUILD_DIRECTORY, BUILD_DIRECTORY_ARGUMENT, protectedDirectories, loadXfeConfig, getBuildDirectory, getServerEntry, configDeploy, getJwtEnv;
99
44
  var init_config = __esm({
@@ -171,24 +116,31 @@ var init_config = __esm({
171
116
  const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
172
117
  const buildDirectory = import_node_path.default.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(import_node_path.default.sep, "/");
173
118
  const droneTarget = import_node_path.default.resolve(projectRoot, deployment.droneConfigPath || "./.drone.yml");
174
- const template = import_node_fs.default.readFileSync(import_node_path.default.join(source, ".drone.yml"), "utf8").replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
175
- const drone = configureDeployPipeline(template, deployment);
176
- const values = import_yaml.default.parse(import_node_fs.default.readFileSync(import_node_path.default.join(source, "helm/values.yaml"), "utf8"));
119
+ const defaults = import_yaml.default.parse(import_node_fs.default.readFileSync(import_node_path.default.join(source, "helm/values.yaml"), "utf8"));
177
120
  const { virtualServiceEnabled = true, hostsProd = [], hostsStage = [], hostsTest = [], gateways = ["common-gateway-eshetang"] } = deployment;
178
- values.virtualService = { ...values.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
179
- values.targetK8s = deployment.targetK8s || "saas";
180
- values.namespace = deployment.targetNamespace || "";
181
- const configuredValues = mergeDeployValues(values, deployment.values || {});
182
- if (deployment.managed && deployment.environments) configuredValues.virtualService.hosts = Object.fromEntries(Object.entries(configuredValues.virtualService.hosts).filter(
183
- /** 托管部署仅保留已选择环境的域名。 */
184
- ([environment]) => deployment.environments.includes(environment) || environment === "test" && deployment.environments.includes("stable")
121
+ defaults.virtualService = { ...defaults.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
122
+ defaults.targetK8s = deployment.targetK8s || "saas";
123
+ defaults.namespace = deployment.targetNamespace || "";
124
+ const values = mergeDeployValues(defaults, deployment.values || {});
125
+ values.managed = Boolean(deployment.managed);
126
+ values.nodeVersion = deployment.values?.nodeVersion || "22.22.0";
127
+ if (!/^\d+\.\d+\.\d+$/.test(values.nodeVersion)) throw new Error("deploy.values.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
128
+ if (!["saas", "infra"].includes(values.targetK8s)) throw new Error("deploy.targetK8s \u53EA\u80FD\u4E3A saas \u6216 infra");
129
+ if (!values.environments || typeof values.environments !== "object" || Array.isArray(values.environments) || Object.values(values.environments).some(
130
+ /** 发布开关必须是布尔值,避免误执行未启用环境。 */
131
+ (enabled) => typeof enabled !== "boolean"
132
+ )) throw new Error("deploy.values.environments \u5FC5\u987B\u662F\u5E03\u5C14\u5F00\u5173\u5BF9\u8C61");
133
+ if (deployment.managed) values.virtualService.hosts = Object.fromEntries(Object.entries(values.virtualService.hosts).filter(
134
+ /** 不生成没有配置域名的环境条目。 */
135
+ ([, hosts]) => Array.isArray(hosts) && hosts.length
185
136
  ));
137
+ const drone = import_node_fs.default.readFileSync(import_node_path.default.join(source, ".drone.yml"), "utf8").replaceAll("<<NODE_VERSION>>", values.nodeVersion).replaceAll("<<TARGET_K8S>>", values.targetK8s).replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
186
138
  if (deployment.managed) {
187
139
  if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
188
140
  import_node_fs.default.cpSync(source, target, { recursive: true });
189
141
  }
190
142
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(droneTarget), { recursive: true });
191
- import_node_fs.default.writeFileSync(import_node_path.default.join(target, "helm/values.yaml"), import_yaml.default.stringify(configuredValues, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
143
+ import_node_fs.default.writeFileSync(import_node_path.default.join(target, "helm/values.yaml"), import_yaml.default.stringify(values, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
192
144
  import_node_fs.default.writeFileSync(droneTarget, drone);
193
145
  };
194
146
  getJwtEnv = async () => {
package/dist/cli.mjs CHANGED
@@ -20,69 +20,14 @@ import path from "path";
20
20
  import { fileURLToPath } from "url";
21
21
  import yaml from "yaml";
22
22
  function mergeDeployValues(base, overrides) {
23
- const entries = Object.entries({ ...base, ...overrides }).map(
24
- /** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
23
+ return Object.fromEntries(Object.entries({ ...base, ...overrides }).map(
24
+ /** 仅合并普通对象,列表保持声明的顺序。 */
25
25
  ([key, value]) => {
26
26
  const previous = base[key];
27
27
  const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
28
28
  return [key, mergeable ? mergeDeployValues(previous, value) : value];
29
29
  }
30
- );
31
- return Object.fromEntries(entries);
32
- }
33
- function configureDeployPipeline(template, deployment) {
34
- if (!deployment.managed && !deployment.nodeVersion && !deployment.environments && !deployment.buildCommand) return template;
35
- const documents = yaml.parseAllDocuments(template).map(
36
- /** 保留镜像和集群 Secret 定义。 */
37
- (document) => document.toJSON()
38
- );
39
- const pipeline = documents[0];
40
- const environments = deployment.environments;
41
- const known = ["test", "stage", "prod", "stable"];
42
- if (environments && (!Array.isArray(environments) || !environments.length || environments.some(
43
- /** 拒绝未声明的发布环境名称。 */
44
- (environment) => typeof environment !== "string" || !known.includes(environment)
45
- ))) throw new Error("deploy.environments \u5FC5\u987B\u662F test/stage/prod/stable \u7684\u975E\u7A7A\u6570\u7EC4");
46
- if (deployment.nodeVersion && !/^\d+\.\d+\.\d+$/.test(deployment.nodeVersion)) throw new Error("deploy.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
47
- if (deployment.managed && deployment.targetNamespace && !/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(deployment.targetNamespace)) throw new Error("deploy.targetNamespace \u5FC5\u987B\u662F\u6709\u6548 Kubernetes \u547D\u540D\u7A7A\u95F4");
48
- const selected = environments ? new Set(environments) : void 0;
49
- const stepEnvironments = /* @__PURE__ */ new Map([["deploy-to-test", "test"], ["deploy-to-stage", "stage"], ["deploy-to-prod", "prod"], ["deploy-to-stable", "stable"], ["push-helm-chart", "prod"]]);
50
- pipeline.steps = pipeline.steps.filter(
51
- /** 通用安装和构建步骤保留,发布步骤按显式环境选择。 */
52
- (step) => !selected || !stepEnvironments.has(step.name) || selected.has(stepEnvironments.get(step.name))
53
- );
54
- for (const step of pipeline.steps) {
55
- if (deployment.nodeVersion && ["install", "build"].includes(step.name)) step.image = `node:${deployment.nodeVersion}-bookworm`;
56
- if (deployment.nodeVersion && step.name === "image-build") step.settings.build_args.push(`XFE_NODE_VERSION=${deployment.nodeVersion}`);
57
- if (step.name === "build") step.commands = step.commands.map(
58
- /** managed 构建复用配置生成器;应用自定义检查仍由应用命令负责。 */
59
- (command2) => {
60
- if (deployment.managed && command2 === "cp -r node_modules/@xfe-repo/web-app/deploy deploy") return "pnpm exec xfe-web config";
61
- return command2 === "pnpm build" ? deployment.buildCommand || command2 : command2;
62
- }
63
- );
64
- if (deployment.managed && deployment.targetK8s === "infra" && step.name === "pre-deploy") delete step.environment.SAAS_K8S_CONFIG;
65
- if (deployment.managed && step.name === "deploy-to-prod") step.commands = step.commands.map(
66
- /** 正式 Helm 部署等待探针并在失败时撤销资源变更。 */
67
- (command2) => command2.includes("| kubectl") ? `helm upgrade --install "\${DRONE_REPO//\\//-}" deploy/helm --values deploy/helm/values.prod.yaml --namespace ${deployment.targetNamespace || "deployment-prod"} --create-namespace --kubeconfig=kube_config --atomic --wait --timeout 5m` : command2
68
- );
69
- }
70
- if (selected && !selected.has("test")) {
71
- pipeline.trigger.event = ["tag"];
72
- const tags = [...new Set([...selected].map(
73
- /** prod 与 stable 共用正式标签,其余使用各自的标签。 */
74
- (environment) => environment === "stage" ? "refs/tags/stage-*" : "refs/tags/release-*"
75
- ))];
76
- pipeline.trigger.ref = { include: tags };
77
- }
78
- const result = deployment.managed && deployment.targetK8s === "infra" ? documents.filter(
79
- /** infra 独立生成的流水线不要求额外的 saas kubeconfig。 */
80
- (document) => document.kind !== "secret" || document.name !== "saas-k8s-config"
81
- ) : documents;
82
- return result.map(
83
- /** 序列化多文档流水线,保留 Kubernetes Runner 的 Secret 引用。 */
84
- (document) => yaml.stringify(document)
85
- ).join("---\n");
30
+ ));
86
31
  }
87
32
  var DEFAULT_BUILD_DIRECTORY, BUILD_DIRECTORY_ARGUMENT, protectedDirectories, loadXfeConfig, getBuildDirectory, getServerEntry, configDeploy, getJwtEnv;
88
33
  var init_config = __esm({
@@ -155,24 +100,31 @@ var init_config = __esm({
155
100
  const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
156
101
  const buildDirectory = path.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(path.sep, "/");
157
102
  const droneTarget = path.resolve(projectRoot, deployment.droneConfigPath || "./.drone.yml");
158
- const template = fs.readFileSync(path.join(source, ".drone.yml"), "utf8").replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
159
- const drone = configureDeployPipeline(template, deployment);
160
- const values = yaml.parse(fs.readFileSync(path.join(source, "helm/values.yaml"), "utf8"));
103
+ const defaults = yaml.parse(fs.readFileSync(path.join(source, "helm/values.yaml"), "utf8"));
161
104
  const { virtualServiceEnabled = true, hostsProd = [], hostsStage = [], hostsTest = [], gateways = ["common-gateway-eshetang"] } = deployment;
162
- values.virtualService = { ...values.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
163
- values.targetK8s = deployment.targetK8s || "saas";
164
- values.namespace = deployment.targetNamespace || "";
165
- const configuredValues = mergeDeployValues(values, deployment.values || {});
166
- if (deployment.managed && deployment.environments) configuredValues.virtualService.hosts = Object.fromEntries(Object.entries(configuredValues.virtualService.hosts).filter(
167
- /** 托管部署仅保留已选择环境的域名。 */
168
- ([environment]) => deployment.environments.includes(environment) || environment === "test" && deployment.environments.includes("stable")
105
+ defaults.virtualService = { ...defaults.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
106
+ defaults.targetK8s = deployment.targetK8s || "saas";
107
+ defaults.namespace = deployment.targetNamespace || "";
108
+ const values = mergeDeployValues(defaults, deployment.values || {});
109
+ values.managed = Boolean(deployment.managed);
110
+ values.nodeVersion = deployment.values?.nodeVersion || "22.22.0";
111
+ if (!/^\d+\.\d+\.\d+$/.test(values.nodeVersion)) throw new Error("deploy.values.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
112
+ if (!["saas", "infra"].includes(values.targetK8s)) throw new Error("deploy.targetK8s \u53EA\u80FD\u4E3A saas \u6216 infra");
113
+ if (!values.environments || typeof values.environments !== "object" || Array.isArray(values.environments) || Object.values(values.environments).some(
114
+ /** 发布开关必须是布尔值,避免误执行未启用环境。 */
115
+ (enabled) => typeof enabled !== "boolean"
116
+ )) throw new Error("deploy.values.environments \u5FC5\u987B\u662F\u5E03\u5C14\u5F00\u5173\u5BF9\u8C61");
117
+ if (deployment.managed) values.virtualService.hosts = Object.fromEntries(Object.entries(values.virtualService.hosts).filter(
118
+ /** 不生成没有配置域名的环境条目。 */
119
+ ([, hosts]) => Array.isArray(hosts) && hosts.length
169
120
  ));
121
+ const drone = fs.readFileSync(path.join(source, ".drone.yml"), "utf8").replaceAll("<<NODE_VERSION>>", values.nodeVersion).replaceAll("<<TARGET_K8S>>", values.targetK8s).replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
170
122
  if (deployment.managed) {
171
123
  if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
172
124
  fs.cpSync(source, target, { recursive: true });
173
125
  }
174
126
  fs.mkdirSync(path.dirname(droneTarget), { recursive: true });
175
- fs.writeFileSync(path.join(target, "helm/values.yaml"), yaml.stringify(configuredValues, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
127
+ fs.writeFileSync(path.join(target, "helm/values.yaml"), yaml.stringify(values, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
176
128
  fs.writeFileSync(droneTarget, drone);
177
129
  };
178
130
  getJwtEnv = async () => {
package/dist/config.d.mts CHANGED
@@ -26,7 +26,7 @@ declare const loadXfeConfig: (root?: string) => XfeConfig;
26
26
  declare const getBuildDirectory: (projectRoot: string, webapp?: WebAppConfig) => string;
27
27
  /** 生产仅校验入口约定而不读取源码,构建阶段由编译器检查文件存在。 */
28
28
  declare const getServerEntry: (projectRoot: string, webapp?: WebAppConfig) => string | undefined;
29
- /** 默认保留既有部署;managed 模式将公共模板和覆盖 values 生成到项目 deploy 目录。 */
29
+ /** 合并部署 values 并生成公共模板,不改写流水线步骤或执行逻辑。 */
30
30
  declare const configDeploy: () => void;
31
31
  declare const getJwtEnv: () => Promise<Partial<JwtEnv>>;
32
32
 
package/dist/config.d.ts CHANGED
@@ -26,7 +26,7 @@ declare const loadXfeConfig: (root?: string) => XfeConfig;
26
26
  declare const getBuildDirectory: (projectRoot: string, webapp?: WebAppConfig) => string;
27
27
  /** 生产仅校验入口约定而不读取源码,构建阶段由编译器检查文件存在。 */
28
28
  declare const getServerEntry: (projectRoot: string, webapp?: WebAppConfig) => string | undefined;
29
- /** 默认保留既有部署;managed 模式将公共模板和覆盖 values 生成到项目 deploy 目录。 */
29
+ /** 合并部署 values 并生成公共模板,不改写流水线步骤或执行逻辑。 */
30
30
  declare const configDeploy: () => void;
31
31
  declare const getJwtEnv: () => Promise<Partial<JwtEnv>>;
32
32
 
package/dist/config.js CHANGED
@@ -109,90 +109,42 @@ var configDeploy = () => {
109
109
  const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
110
110
  const buildDirectory = import_node_path.default.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(import_node_path.default.sep, "/");
111
111
  const droneTarget = import_node_path.default.resolve(projectRoot, deployment.droneConfigPath || "./.drone.yml");
112
- const template = import_node_fs.default.readFileSync(import_node_path.default.join(source, ".drone.yml"), "utf8").replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
113
- const drone = configureDeployPipeline(template, deployment);
114
- const values = import_yaml.default.parse(import_node_fs.default.readFileSync(import_node_path.default.join(source, "helm/values.yaml"), "utf8"));
112
+ const defaults = import_yaml.default.parse(import_node_fs.default.readFileSync(import_node_path.default.join(source, "helm/values.yaml"), "utf8"));
115
113
  const { virtualServiceEnabled = true, hostsProd = [], hostsStage = [], hostsTest = [], gateways = ["common-gateway-eshetang"] } = deployment;
116
- values.virtualService = { ...values.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
117
- values.targetK8s = deployment.targetK8s || "saas";
118
- values.namespace = deployment.targetNamespace || "";
119
- const configuredValues = mergeDeployValues(values, deployment.values || {});
120
- if (deployment.managed && deployment.environments) configuredValues.virtualService.hosts = Object.fromEntries(Object.entries(configuredValues.virtualService.hosts).filter(
121
- /** 托管部署仅保留已选择环境的域名。 */
122
- ([environment]) => deployment.environments.includes(environment) || environment === "test" && deployment.environments.includes("stable")
114
+ defaults.virtualService = { ...defaults.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
115
+ defaults.targetK8s = deployment.targetK8s || "saas";
116
+ defaults.namespace = deployment.targetNamespace || "";
117
+ const values = mergeDeployValues(defaults, deployment.values || {});
118
+ values.managed = Boolean(deployment.managed);
119
+ values.nodeVersion = deployment.values?.nodeVersion || "22.22.0";
120
+ if (!/^\d+\.\d+\.\d+$/.test(values.nodeVersion)) throw new Error("deploy.values.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
121
+ if (!["saas", "infra"].includes(values.targetK8s)) throw new Error("deploy.targetK8s \u53EA\u80FD\u4E3A saas \u6216 infra");
122
+ if (!values.environments || typeof values.environments !== "object" || Array.isArray(values.environments) || Object.values(values.environments).some(
123
+ /** 发布开关必须是布尔值,避免误执行未启用环境。 */
124
+ (enabled) => typeof enabled !== "boolean"
125
+ )) throw new Error("deploy.values.environments \u5FC5\u987B\u662F\u5E03\u5C14\u5F00\u5173\u5BF9\u8C61");
126
+ if (deployment.managed) values.virtualService.hosts = Object.fromEntries(Object.entries(values.virtualService.hosts).filter(
127
+ /** 不生成没有配置域名的环境条目。 */
128
+ ([, hosts]) => Array.isArray(hosts) && hosts.length
123
129
  ));
130
+ const drone = import_node_fs.default.readFileSync(import_node_path.default.join(source, ".drone.yml"), "utf8").replaceAll("<<NODE_VERSION>>", values.nodeVersion).replaceAll("<<TARGET_K8S>>", values.targetK8s).replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
124
131
  if (deployment.managed) {
125
132
  if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
126
133
  import_node_fs.default.cpSync(source, target, { recursive: true });
127
134
  }
128
135
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(droneTarget), { recursive: true });
129
- import_node_fs.default.writeFileSync(import_node_path.default.join(target, "helm/values.yaml"), import_yaml.default.stringify(configuredValues, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
136
+ import_node_fs.default.writeFileSync(import_node_path.default.join(target, "helm/values.yaml"), import_yaml.default.stringify(values, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
130
137
  import_node_fs.default.writeFileSync(droneTarget, drone);
131
138
  };
132
139
  function mergeDeployValues(base, overrides) {
133
- const entries = Object.entries({ ...base, ...overrides }).map(
134
- /** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
140
+ return Object.fromEntries(Object.entries({ ...base, ...overrides }).map(
141
+ /** 仅合并普通对象,列表保持声明的顺序。 */
135
142
  ([key, value]) => {
136
143
  const previous = base[key];
137
144
  const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
138
145
  return [key, mergeable ? mergeDeployValues(previous, value) : value];
139
146
  }
140
- );
141
- return Object.fromEntries(entries);
142
- }
143
- function configureDeployPipeline(template, deployment) {
144
- if (!deployment.managed && !deployment.nodeVersion && !deployment.environments && !deployment.buildCommand) return template;
145
- const documents = import_yaml.default.parseAllDocuments(template).map(
146
- /** 保留镜像和集群 Secret 定义。 */
147
- (document) => document.toJSON()
148
- );
149
- const pipeline = documents[0];
150
- const environments = deployment.environments;
151
- const known = ["test", "stage", "prod", "stable"];
152
- if (environments && (!Array.isArray(environments) || !environments.length || environments.some(
153
- /** 拒绝未声明的发布环境名称。 */
154
- (environment) => typeof environment !== "string" || !known.includes(environment)
155
- ))) throw new Error("deploy.environments \u5FC5\u987B\u662F test/stage/prod/stable \u7684\u975E\u7A7A\u6570\u7EC4");
156
- if (deployment.nodeVersion && !/^\d+\.\d+\.\d+$/.test(deployment.nodeVersion)) throw new Error("deploy.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
157
- if (deployment.managed && deployment.targetNamespace && !/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(deployment.targetNamespace)) throw new Error("deploy.targetNamespace \u5FC5\u987B\u662F\u6709\u6548 Kubernetes \u547D\u540D\u7A7A\u95F4");
158
- const selected = environments ? new Set(environments) : void 0;
159
- const stepEnvironments = /* @__PURE__ */ new Map([["deploy-to-test", "test"], ["deploy-to-stage", "stage"], ["deploy-to-prod", "prod"], ["deploy-to-stable", "stable"], ["push-helm-chart", "prod"]]);
160
- pipeline.steps = pipeline.steps.filter(
161
- /** 通用安装和构建步骤保留,发布步骤按显式环境选择。 */
162
- (step) => !selected || !stepEnvironments.has(step.name) || selected.has(stepEnvironments.get(step.name))
163
- );
164
- for (const step of pipeline.steps) {
165
- if (deployment.nodeVersion && ["install", "build"].includes(step.name)) step.image = `node:${deployment.nodeVersion}-bookworm`;
166
- if (deployment.nodeVersion && step.name === "image-build") step.settings.build_args.push(`XFE_NODE_VERSION=${deployment.nodeVersion}`);
167
- if (step.name === "build") step.commands = step.commands.map(
168
- /** managed 构建复用配置生成器;应用自定义检查仍由应用命令负责。 */
169
- (command) => {
170
- if (deployment.managed && command === "cp -r node_modules/@xfe-repo/web-app/deploy deploy") return "pnpm exec xfe-web config";
171
- return command === "pnpm build" ? deployment.buildCommand || command : command;
172
- }
173
- );
174
- if (deployment.managed && deployment.targetK8s === "infra" && step.name === "pre-deploy") delete step.environment.SAAS_K8S_CONFIG;
175
- if (deployment.managed && step.name === "deploy-to-prod") step.commands = step.commands.map(
176
- /** 正式 Helm 部署等待探针并在失败时撤销资源变更。 */
177
- (command) => command.includes("| kubectl") ? `helm upgrade --install "\${DRONE_REPO//\\//-}" deploy/helm --values deploy/helm/values.prod.yaml --namespace ${deployment.targetNamespace || "deployment-prod"} --create-namespace --kubeconfig=kube_config --atomic --wait --timeout 5m` : command
178
- );
179
- }
180
- if (selected && !selected.has("test")) {
181
- pipeline.trigger.event = ["tag"];
182
- const tags = [...new Set([...selected].map(
183
- /** prod 与 stable 共用正式标签,其余使用各自的标签。 */
184
- (environment) => environment === "stage" ? "refs/tags/stage-*" : "refs/tags/release-*"
185
- ))];
186
- pipeline.trigger.ref = { include: tags };
187
- }
188
- const result = deployment.managed && deployment.targetK8s === "infra" ? documents.filter(
189
- /** infra 独立生成的流水线不要求额外的 saas kubeconfig。 */
190
- (document) => document.kind !== "secret" || document.name !== "saas-k8s-config"
191
- ) : documents;
192
- return result.map(
193
- /** 序列化多文档流水线,保留 Kubernetes Runner 的 Secret 引用。 */
194
- (document) => import_yaml.default.stringify(document)
195
- ).join("---\n");
147
+ ));
196
148
  }
197
149
  var getJwtEnv = async () => {
198
150
  const xfeConfig = loadXfeConfig();
package/dist/config.mjs CHANGED
@@ -70,90 +70,42 @@ var configDeploy = () => {
70
70
  const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
71
71
  const buildDirectory = path.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(path.sep, "/");
72
72
  const droneTarget = path.resolve(projectRoot, deployment.droneConfigPath || "./.drone.yml");
73
- const template = fs.readFileSync(path.join(source, ".drone.yml"), "utf8").replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
74
- const drone = configureDeployPipeline(template, deployment);
75
- const values = yaml.parse(fs.readFileSync(path.join(source, "helm/values.yaml"), "utf8"));
73
+ const defaults = yaml.parse(fs.readFileSync(path.join(source, "helm/values.yaml"), "utf8"));
76
74
  const { virtualServiceEnabled = true, hostsProd = [], hostsStage = [], hostsTest = [], gateways = ["common-gateway-eshetang"] } = deployment;
77
- values.virtualService = { ...values.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
78
- values.targetK8s = deployment.targetK8s || "saas";
79
- values.namespace = deployment.targetNamespace || "";
80
- const configuredValues = mergeDeployValues(values, deployment.values || {});
81
- if (deployment.managed && deployment.environments) configuredValues.virtualService.hosts = Object.fromEntries(Object.entries(configuredValues.virtualService.hosts).filter(
82
- /** 托管部署仅保留已选择环境的域名。 */
83
- ([environment]) => deployment.environments.includes(environment) || environment === "test" && deployment.environments.includes("stable")
75
+ defaults.virtualService = { ...defaults.virtualService, enabled: virtualServiceEnabled, hosts: { prod: hostsProd, stage: hostsStage, test: hostsTest }, gateways };
76
+ defaults.targetK8s = deployment.targetK8s || "saas";
77
+ defaults.namespace = deployment.targetNamespace || "";
78
+ const values = mergeDeployValues(defaults, deployment.values || {});
79
+ values.managed = Boolean(deployment.managed);
80
+ values.nodeVersion = deployment.values?.nodeVersion || "22.22.0";
81
+ if (!/^\d+\.\d+\.\d+$/.test(values.nodeVersion)) throw new Error("deploy.values.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
82
+ if (!["saas", "infra"].includes(values.targetK8s)) throw new Error("deploy.targetK8s \u53EA\u80FD\u4E3A saas \u6216 infra");
83
+ if (!values.environments || typeof values.environments !== "object" || Array.isArray(values.environments) || Object.values(values.environments).some(
84
+ /** 发布开关必须是布尔值,避免误执行未启用环境。 */
85
+ (enabled) => typeof enabled !== "boolean"
86
+ )) throw new Error("deploy.values.environments \u5FC5\u987B\u662F\u5E03\u5C14\u5F00\u5173\u5BF9\u8C61");
87
+ if (deployment.managed) values.virtualService.hosts = Object.fromEntries(Object.entries(values.virtualService.hosts).filter(
88
+ /** 不生成没有配置域名的环境条目。 */
89
+ ([, hosts]) => Array.isArray(hosts) && hosts.length
84
90
  ));
91
+ const drone = fs.readFileSync(path.join(source, ".drone.yml"), "utf8").replaceAll("<<NODE_VERSION>>", values.nodeVersion).replaceAll("<<TARGET_K8S>>", values.targetK8s).replace(JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${DEFAULT_BUILD_DIRECTORY}`), JSON.stringify(`${BUILD_DIRECTORY_ARGUMENT}=${buildDirectory}`));
85
92
  if (deployment.managed) {
86
93
  if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
87
94
  fs.cpSync(source, target, { recursive: true });
88
95
  }
89
96
  fs.mkdirSync(path.dirname(droneTarget), { recursive: true });
90
- fs.writeFileSync(path.join(target, "helm/values.yaml"), yaml.stringify(configuredValues, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
97
+ fs.writeFileSync(path.join(target, "helm/values.yaml"), yaml.stringify(values, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
91
98
  fs.writeFileSync(droneTarget, drone);
92
99
  };
93
100
  function mergeDeployValues(base, overrides) {
94
- const entries = Object.entries({ ...base, ...overrides }).map(
95
- /** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
101
+ return Object.fromEntries(Object.entries({ ...base, ...overrides }).map(
102
+ /** 仅合并普通对象,列表保持声明的顺序。 */
96
103
  ([key, value]) => {
97
104
  const previous = base[key];
98
105
  const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
99
106
  return [key, mergeable ? mergeDeployValues(previous, value) : value];
100
107
  }
101
- );
102
- return Object.fromEntries(entries);
103
- }
104
- function configureDeployPipeline(template, deployment) {
105
- if (!deployment.managed && !deployment.nodeVersion && !deployment.environments && !deployment.buildCommand) return template;
106
- const documents = yaml.parseAllDocuments(template).map(
107
- /** 保留镜像和集群 Secret 定义。 */
108
- (document) => document.toJSON()
109
- );
110
- const pipeline = documents[0];
111
- const environments = deployment.environments;
112
- const known = ["test", "stage", "prod", "stable"];
113
- if (environments && (!Array.isArray(environments) || !environments.length || environments.some(
114
- /** 拒绝未声明的发布环境名称。 */
115
- (environment) => typeof environment !== "string" || !known.includes(environment)
116
- ))) throw new Error("deploy.environments \u5FC5\u987B\u662F test/stage/prod/stable \u7684\u975E\u7A7A\u6570\u7EC4");
117
- if (deployment.nodeVersion && !/^\d+\.\d+\.\d+$/.test(deployment.nodeVersion)) throw new Error("deploy.nodeVersion \u5FC5\u987B\u662F\u5B8C\u6574 Node \u7248\u672C\u53F7");
118
- if (deployment.managed && deployment.targetNamespace && !/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(deployment.targetNamespace)) throw new Error("deploy.targetNamespace \u5FC5\u987B\u662F\u6709\u6548 Kubernetes \u547D\u540D\u7A7A\u95F4");
119
- const selected = environments ? new Set(environments) : void 0;
120
- const stepEnvironments = /* @__PURE__ */ new Map([["deploy-to-test", "test"], ["deploy-to-stage", "stage"], ["deploy-to-prod", "prod"], ["deploy-to-stable", "stable"], ["push-helm-chart", "prod"]]);
121
- pipeline.steps = pipeline.steps.filter(
122
- /** 通用安装和构建步骤保留,发布步骤按显式环境选择。 */
123
- (step) => !selected || !stepEnvironments.has(step.name) || selected.has(stepEnvironments.get(step.name))
124
- );
125
- for (const step of pipeline.steps) {
126
- if (deployment.nodeVersion && ["install", "build"].includes(step.name)) step.image = `node:${deployment.nodeVersion}-bookworm`;
127
- if (deployment.nodeVersion && step.name === "image-build") step.settings.build_args.push(`XFE_NODE_VERSION=${deployment.nodeVersion}`);
128
- if (step.name === "build") step.commands = step.commands.map(
129
- /** managed 构建复用配置生成器;应用自定义检查仍由应用命令负责。 */
130
- (command) => {
131
- if (deployment.managed && command === "cp -r node_modules/@xfe-repo/web-app/deploy deploy") return "pnpm exec xfe-web config";
132
- return command === "pnpm build" ? deployment.buildCommand || command : command;
133
- }
134
- );
135
- if (deployment.managed && deployment.targetK8s === "infra" && step.name === "pre-deploy") delete step.environment.SAAS_K8S_CONFIG;
136
- if (deployment.managed && step.name === "deploy-to-prod") step.commands = step.commands.map(
137
- /** 正式 Helm 部署等待探针并在失败时撤销资源变更。 */
138
- (command) => command.includes("| kubectl") ? `helm upgrade --install "\${DRONE_REPO//\\//-}" deploy/helm --values deploy/helm/values.prod.yaml --namespace ${deployment.targetNamespace || "deployment-prod"} --create-namespace --kubeconfig=kube_config --atomic --wait --timeout 5m` : command
139
- );
140
- }
141
- if (selected && !selected.has("test")) {
142
- pipeline.trigger.event = ["tag"];
143
- const tags = [...new Set([...selected].map(
144
- /** prod 与 stable 共用正式标签,其余使用各自的标签。 */
145
- (environment) => environment === "stage" ? "refs/tags/stage-*" : "refs/tags/release-*"
146
- ))];
147
- pipeline.trigger.ref = { include: tags };
148
- }
149
- const result = deployment.managed && deployment.targetK8s === "infra" ? documents.filter(
150
- /** infra 独立生成的流水线不要求额外的 saas kubeconfig。 */
151
- (document) => document.kind !== "secret" || document.name !== "saas-k8s-config"
152
- ) : documents;
153
- return result.map(
154
- /** 序列化多文档流水线,保留 Kubernetes Runner 的 Secret 引用。 */
155
- (document) => yaml.stringify(document)
156
- ).join("---\n");
108
+ ));
157
109
  }
158
110
  var getJwtEnv = async () => {
159
111
  const xfeConfig = loadXfeConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfe-repo/web-app",
3
- "version": "2.0.0-beta.4",
3
+ "version": "2.0.0-beta.5",
4
4
  "bin": {
5
5
  "xfe-web": "./bin/index.js"
6
6
  },