@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 +8 -6
- package/deploy/.drone.yml +60 -24
- package/deploy/helm/values.yaml +10 -0
- package/dist/cli.js +21 -69
- package/dist/cli.mjs +21 -69
- package/dist/config.d.mts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +21 -69
- package/dist/config.mjs +21 -69
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ SSR `start` 使用与构建一致的隐藏工作目录,使官方 serve 正确
|
|
|
126
126
|
|
|
127
127
|
## 可选的项目部署模板
|
|
128
128
|
|
|
129
|
-
|
|
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`
|
|
154
|
+
`values` 中的对象递归合并,列表整体替换。默认 Node 22.22.0、`pnpm build` 和所有发布环境开启,保持既有应用行为。公共 Drone 保留固定步骤,每个发布步骤在运行时检查 `environments` 布尔开关,关闭时直接结束,不创建命名空间或发布资源。
|
|
155
155
|
|
|
156
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
74
|
-
from_secret:
|
|
75
|
-
INFRA_K8S_CONFIG:
|
|
76
|
-
from_secret: infra-k8s-config
|
|
91
|
+
K8S_CONFIG:
|
|
92
|
+
from_secret: target-k8s-config
|
|
77
93
|
commands:
|
|
78
|
-
#
|
|
79
|
-
-
|
|
80
|
-
|
|
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
|
-
#
|
|
88
|
-
- cat deploy/helm/values.yaml
|
|
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
|
-
-
|
|
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:
|
|
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:
|
|
230
|
+
path: <<TARGET_K8S>>-k8s-config
|
|
195
231
|
name: config
|
|
196
232
|
---
|
|
197
233
|
kind: secret
|
package/deploy/helm/values.yaml
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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(
|
|
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
|
-
|
|
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();
|