@xfe-repo/web-app 2.0.0-beta.3 → 2.0.0-beta.4
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 +32 -0
- package/deploy/Dockerfile +2 -1
- package/deploy/Dockerfile.dockerignore +10 -0
- package/deploy/helm/Chart.yaml +1 -1
- package/deploy/helm/templates/deployment.yaml +50 -3
- package/deploy/helm/templates/pvc.yaml +17 -0
- package/deploy/helm/templates/virtualService.yaml +6 -4
- package/deploy/helm/values.yaml +11 -0
- package/dist/cli.js +88 -29
- package/dist/cli.mjs +88 -29
- package/dist/config.d.mts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +88 -29
- package/dist/config.mjs +88 -29
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -122,3 +122,35 @@ SSR `start` 使用与构建一致的隐藏工作目录,使官方 serve 正确
|
|
|
122
122
|
构建会先加载 `react-app-polyfill/stable`,并在应用原有 browserslist 的基础上,补充 Edge 18、Chrome 64、Firefox 60、Safari/iOS 12 及以上的 JS 编译目标,业务代码和第三方依赖使用同一目标。
|
|
123
123
|
|
|
124
124
|
这是 JS 兼容基线;具体组件样式和浏览器 Web API 仍需在目标浏览器验证。
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## 可选的项目部署模板
|
|
128
|
+
|
|
129
|
+
已有项目保持默认 Node 22、集群选择、发布环境和无状态部署方式。设置 `deploy.managed: true` 后,`xfe-web config` 将框架模板生成到项目 `deploy/`,不修改 npm 包中的 Helm values;项目只维护配置,生成文件由框架管理。Drone 入口需提交仓库,安装后的流水线再通过同一命令生成模板。
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"deploy": {
|
|
134
|
+
"managed": true,
|
|
135
|
+
"droneConfigPath": "deploy/.drone.yml",
|
|
136
|
+
"targetK8s": "infra",
|
|
137
|
+
"targetNamespace": "deployment-prod",
|
|
138
|
+
"nodeVersion": "24.22.1",
|
|
139
|
+
"environments": ["prod"],
|
|
140
|
+
"hostsProd": ["app.example.com"],
|
|
141
|
+
"buildCommand": "pnpm check && pnpm build && pnpm prune --prod",
|
|
142
|
+
"values": {
|
|
143
|
+
"replicas": 1,
|
|
144
|
+
"strategy": {"type": "Recreate"},
|
|
145
|
+
"persistence": {"enabled": true, "size": "20Gi", "mountPath": "/data"},
|
|
146
|
+
"podSecurityContext": {"fsGroup": 1000},
|
|
147
|
+
"env": [{"name": "DATA_DIR", "value": "/data"}],
|
|
148
|
+
"envFrom": [{"secretRef": {"name": "application-env"}}]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`values` 递归覆盖默认 Helm values;列表整体替换。可配置实例数、更新策略、容器资源、Pod/容器安全上下文、环境变量及来源、启动/就绪/存活探针、终止宽限期和 PVC。PVC 默认关闭,可指定 `existingClaim` 或 `storageClass`。应用负责声明自己的 `DATA_DIR` 等业务变量,框架不包含 OSS 或认证逻辑。
|
|
155
|
+
|
|
156
|
+
`nodeVersion` 同时应用于安装、构建及运行镜像,省略时保留 22.22.0。`environments` 可选择 test/stage/prod/stable,省略时保持原流程;仅 prod 时不生成其他发布步骤。managed 正式发布使用 Helm 等待就绪,失败自动撤销资源变更。`virtualService.timeout` 可设置长连接超时,`corsPolicy: null` 可关闭默认跨域策略。密钥只通过运行环境提供,不放入 values 或构建命令。
|
package/deploy/Dockerfile
CHANGED
package/deploy/helm/Chart.yaml
CHANGED
|
@@ -7,11 +7,15 @@ metadata:
|
|
|
7
7
|
kubernetes.io/change-cause: {{ or $.Values.tagContent $.Values.commitSHA }}
|
|
8
8
|
spec:
|
|
9
9
|
strategy:
|
|
10
|
+
{{- if .Values.strategy }}
|
|
11
|
+
{{- toYaml .Values.strategy | nindent 4 }}
|
|
12
|
+
{{- else }}
|
|
10
13
|
type: RollingUpdate
|
|
11
14
|
rollingUpdate:
|
|
12
15
|
maxSurge: 2
|
|
13
16
|
maxUnavailable: 50%
|
|
14
|
-
|
|
17
|
+
{{- end }}
|
|
18
|
+
replicas: {{ if hasKey .Values "replicas" }}{{ .Values.replicas }}{{ else if eq $.Values.envType "prod" }} 2 {{ else }} 1 {{ end }}
|
|
15
19
|
selector:
|
|
16
20
|
matchLabels:
|
|
17
21
|
app: {{ $.Values.app }}
|
|
@@ -20,6 +24,13 @@ spec:
|
|
|
20
24
|
labels:
|
|
21
25
|
app: {{ $.Values.app }}
|
|
22
26
|
spec:
|
|
27
|
+
{{- with .Values.podSecurityContext }}
|
|
28
|
+
securityContext:
|
|
29
|
+
{{- toYaml . | nindent 8 }}
|
|
30
|
+
{{- end }}
|
|
31
|
+
{{- with .Values.terminationGracePeriodSeconds }}
|
|
32
|
+
terminationGracePeriodSeconds: {{ . }}
|
|
33
|
+
{{- end }}
|
|
23
34
|
imagePullSecrets:
|
|
24
35
|
- name: {{ $.Values.imagePullSecret }}
|
|
25
36
|
containers:
|
|
@@ -27,9 +38,17 @@ spec:
|
|
|
27
38
|
imagePullPolicy: Always
|
|
28
39
|
image: "{{ $.Values.image }}"
|
|
29
40
|
resources:
|
|
41
|
+
{{- if .Values.resources }}
|
|
42
|
+
{{- toYaml .Values.resources | nindent 12 }}
|
|
43
|
+
{{- else }}
|
|
30
44
|
requests:
|
|
31
45
|
cpu: "0.01"
|
|
32
46
|
memory: "100Mi"
|
|
47
|
+
{{- end }}
|
|
48
|
+
{{- with .Values.containerSecurityContext }}
|
|
49
|
+
securityContext:
|
|
50
|
+
{{- toYaml . | nindent 12 }}
|
|
51
|
+
{{- end }}
|
|
33
52
|
env:
|
|
34
53
|
- name: HOST
|
|
35
54
|
value: "0.0.0.0"
|
|
@@ -39,9 +58,37 @@ spec:
|
|
|
39
58
|
- name: HOST_PREFIX
|
|
40
59
|
value: "{{ $.Values.hostPrefix }}"
|
|
41
60
|
{{end}}
|
|
42
|
-
{{-
|
|
61
|
+
{{- with .Values.env }}
|
|
62
|
+
{{- toYaml . | nindent 12 }}
|
|
63
|
+
{{- end }}
|
|
64
|
+
{{- if hasKey .Values "envFrom" }}
|
|
65
|
+
envFrom:
|
|
66
|
+
{{- toYaml .Values.envFrom | nindent 12 }}
|
|
67
|
+
{{- else if eq $.Values.targetK8s "saas" }}
|
|
43
68
|
envFrom:
|
|
44
69
|
- configMapRef:
|
|
45
70
|
name: "app-common-config"
|
|
46
71
|
{{- end }}
|
|
47
|
-
|
|
72
|
+
{{- with .Values.startupProbe }}
|
|
73
|
+
startupProbe:
|
|
74
|
+
{{- toYaml . | nindent 12 }}
|
|
75
|
+
{{- end }}
|
|
76
|
+
{{- with .Values.readinessProbe }}
|
|
77
|
+
readinessProbe:
|
|
78
|
+
{{- toYaml . | nindent 12 }}
|
|
79
|
+
{{- end }}
|
|
80
|
+
{{- with .Values.livenessProbe }}
|
|
81
|
+
livenessProbe:
|
|
82
|
+
{{- toYaml . | nindent 12 }}
|
|
83
|
+
{{- end }}
|
|
84
|
+
{{- if .Values.persistence.enabled }}
|
|
85
|
+
volumeMounts:
|
|
86
|
+
- name: data
|
|
87
|
+
mountPath: {{ .Values.persistence.mountPath | quote }}
|
|
88
|
+
{{- end }}
|
|
89
|
+
{{- if .Values.persistence.enabled }}
|
|
90
|
+
volumes:
|
|
91
|
+
- name: data
|
|
92
|
+
persistentVolumeClaim:
|
|
93
|
+
claimName: {{ default (printf "%s-data" .Values.app) .Values.persistence.existingClaim }}
|
|
94
|
+
{{- end }}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
|
|
2
|
+
apiVersion: v1
|
|
3
|
+
kind: PersistentVolumeClaim
|
|
4
|
+
metadata:
|
|
5
|
+
name: {{ .Values.app }}-data
|
|
6
|
+
namespace: {{ include "template.namespace" . }}
|
|
7
|
+
annotations:
|
|
8
|
+
helm.sh/resource-policy: keep
|
|
9
|
+
spec:
|
|
10
|
+
accessModes: [ReadWriteOnce]
|
|
11
|
+
{{- with .Values.persistence.storageClass }}
|
|
12
|
+
storageClassName: {{ . | quote }}
|
|
13
|
+
{{- end }}
|
|
14
|
+
resources:
|
|
15
|
+
requests:
|
|
16
|
+
storage: {{ .Values.persistence.size }}
|
|
17
|
+
{{- end }}
|
|
@@ -19,14 +19,16 @@ spec:
|
|
|
19
19
|
- match:
|
|
20
20
|
- uri:
|
|
21
21
|
prefix: /
|
|
22
|
+
{{- with .Values.virtualService.timeout }}
|
|
23
|
+
timeout: {{ . }}
|
|
24
|
+
{{- end }}
|
|
22
25
|
route:
|
|
23
26
|
- destination:
|
|
24
27
|
host: {{ .Values.app }}
|
|
25
28
|
port:
|
|
26
29
|
number: 80
|
|
30
|
+
{{- with .Values.virtualService.corsPolicy }}
|
|
27
31
|
corsPolicy:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
allowMethods:
|
|
31
|
-
- "*"
|
|
32
|
+
{{- toYaml . | nindent 12 }}
|
|
33
|
+
{{- end }}
|
|
32
34
|
{{- end }}
|
package/deploy/helm/values.yaml
CHANGED
|
@@ -7,6 +7,9 @@ envType: '<<ENV_TYPE>>'
|
|
|
7
7
|
hostPrefix: '<<HOST_PREFIX>>'
|
|
8
8
|
namespace: ''
|
|
9
9
|
virtualService:
|
|
10
|
+
corsPolicy:
|
|
11
|
+
allowOrigin: ['*']
|
|
12
|
+
allowMethods: ['*']
|
|
10
13
|
enabled: true
|
|
11
14
|
hosts:
|
|
12
15
|
test:
|
|
@@ -18,3 +21,11 @@ virtualService:
|
|
|
18
21
|
gateways:
|
|
19
22
|
- 'common-gateway-eshetang'
|
|
20
23
|
targetK8s: 'saas'
|
|
24
|
+
|
|
25
|
+
# 可选持久卷;省略时保持现有无状态 Web 应用的部署行为。
|
|
26
|
+
persistence:
|
|
27
|
+
enabled: false
|
|
28
|
+
mountPath: /data
|
|
29
|
+
size: 20Gi
|
|
30
|
+
existingClaim: ''
|
|
31
|
+
storageClass: ''
|
package/dist/cli.js
CHANGED
|
@@ -30,6 +30,71 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
|
|
32
32
|
// src/config.ts
|
|
33
|
+
function mergeDeployValues(base, overrides) {
|
|
34
|
+
const entries = Object.entries({ ...base, ...overrides }).map(
|
|
35
|
+
/** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
|
|
36
|
+
([key, value]) => {
|
|
37
|
+
const previous = base[key];
|
|
38
|
+
const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
|
|
39
|
+
return [key, mergeable ? mergeDeployValues(previous, value) : value];
|
|
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");
|
|
97
|
+
}
|
|
33
98
|
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;
|
|
34
99
|
var init_config = __esm({
|
|
35
100
|
"src/config.ts"() {
|
|
@@ -98,39 +163,33 @@ var init_config = __esm({
|
|
|
98
163
|
};
|
|
99
164
|
configDeploy = () => {
|
|
100
165
|
const xfeConfig = loadXfeConfig();
|
|
101
|
-
|
|
166
|
+
const deployment = xfeConfig.deploy;
|
|
167
|
+
if (!deployment) return;
|
|
102
168
|
const configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
103
169
|
const projectRoot = process.cwd();
|
|
170
|
+
const source = import_node_path.default.resolve(configDirectory, "../deploy");
|
|
171
|
+
const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
|
|
104
172
|
const buildDirectory = import_node_path.default.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(import_node_path.default.sep, "/");
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
173
|
+
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"));
|
|
177
|
+
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")
|
|
185
|
+
));
|
|
186
|
+
if (deployment.managed) {
|
|
187
|
+
if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
|
|
188
|
+
import_node_fs.default.cpSync(source, target, { recursive: true });
|
|
189
|
+
}
|
|
110
190
|
import_node_fs.default.mkdirSync(import_node_path.default.dirname(droneTarget), { recursive: true });
|
|
111
|
-
import_node_fs.default.writeFileSync(
|
|
112
|
-
|
|
113
|
-
const valuesYamlFile = import_node_fs.default.readFileSync(valuesYamlFilePath, "utf8");
|
|
114
|
-
const valuesYaml = import_yaml.default.parse(valuesYamlFile);
|
|
115
|
-
if (!valuesYaml.virtualService) valuesYaml.virtualService = {};
|
|
116
|
-
if (!valuesYaml.virtualService.hosts) valuesYaml.virtualService.hosts = { test: [], stage: [], prod: [] };
|
|
117
|
-
const {
|
|
118
|
-
virtualServiceEnabled = true,
|
|
119
|
-
hostsProd = [],
|
|
120
|
-
hostsStage = [],
|
|
121
|
-
hostsTest = [],
|
|
122
|
-
gateways = ["common-gateway-eshetang"]
|
|
123
|
-
} = xfeConfig.deploy;
|
|
124
|
-
const { targetK8s = "saas", targetNamespace = "" } = xfeConfig.deploy;
|
|
125
|
-
valuesYaml.virtualService.enabled = virtualServiceEnabled;
|
|
126
|
-
valuesYaml.virtualService.gateways = gateways;
|
|
127
|
-
valuesYaml.virtualService.hosts.prod = hostsProd;
|
|
128
|
-
valuesYaml.virtualService.hosts.stage = hostsStage;
|
|
129
|
-
valuesYaml.virtualService.hosts.test = hostsTest;
|
|
130
|
-
valuesYaml.targetK8s = targetK8s;
|
|
131
|
-
valuesYaml.namespace = targetNamespace;
|
|
132
|
-
const valuesYamlStr = import_yaml.default.stringify(valuesYaml, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" });
|
|
133
|
-
import_node_fs.default.writeFileSync(valuesYamlFilePath, valuesYamlStr, "utf8");
|
|
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" }));
|
|
192
|
+
import_node_fs.default.writeFileSync(droneTarget, drone);
|
|
134
193
|
};
|
|
135
194
|
getJwtEnv = async () => {
|
|
136
195
|
const xfeConfig = loadXfeConfig();
|
package/dist/cli.mjs
CHANGED
|
@@ -19,6 +19,71 @@ import fs from "fs";
|
|
|
19
19
|
import path from "path";
|
|
20
20
|
import { fileURLToPath } from "url";
|
|
21
21
|
import yaml from "yaml";
|
|
22
|
+
function mergeDeployValues(base, overrides) {
|
|
23
|
+
const entries = Object.entries({ ...base, ...overrides }).map(
|
|
24
|
+
/** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
|
|
25
|
+
([key, value]) => {
|
|
26
|
+
const previous = base[key];
|
|
27
|
+
const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
|
|
28
|
+
return [key, mergeable ? mergeDeployValues(previous, value) : value];
|
|
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");
|
|
86
|
+
}
|
|
22
87
|
var DEFAULT_BUILD_DIRECTORY, BUILD_DIRECTORY_ARGUMENT, protectedDirectories, loadXfeConfig, getBuildDirectory, getServerEntry, configDeploy, getJwtEnv;
|
|
23
88
|
var init_config = __esm({
|
|
24
89
|
"src/config.ts"() {
|
|
@@ -82,39 +147,33 @@ var init_config = __esm({
|
|
|
82
147
|
};
|
|
83
148
|
configDeploy = () => {
|
|
84
149
|
const xfeConfig = loadXfeConfig();
|
|
85
|
-
|
|
150
|
+
const deployment = xfeConfig.deploy;
|
|
151
|
+
if (!deployment) return;
|
|
86
152
|
const configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
|
87
153
|
const projectRoot = process.cwd();
|
|
154
|
+
const source = path.resolve(configDirectory, "../deploy");
|
|
155
|
+
const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
|
|
88
156
|
const buildDirectory = path.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(path.sep, "/");
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
157
|
+
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"));
|
|
161
|
+
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")
|
|
169
|
+
));
|
|
170
|
+
if (deployment.managed) {
|
|
171
|
+
if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
|
|
172
|
+
fs.cpSync(source, target, { recursive: true });
|
|
173
|
+
}
|
|
94
174
|
fs.mkdirSync(path.dirname(droneTarget), { recursive: true });
|
|
95
|
-
fs.writeFileSync(
|
|
96
|
-
|
|
97
|
-
const valuesYamlFile = fs.readFileSync(valuesYamlFilePath, "utf8");
|
|
98
|
-
const valuesYaml = yaml.parse(valuesYamlFile);
|
|
99
|
-
if (!valuesYaml.virtualService) valuesYaml.virtualService = {};
|
|
100
|
-
if (!valuesYaml.virtualService.hosts) valuesYaml.virtualService.hosts = { test: [], stage: [], prod: [] };
|
|
101
|
-
const {
|
|
102
|
-
virtualServiceEnabled = true,
|
|
103
|
-
hostsProd = [],
|
|
104
|
-
hostsStage = [],
|
|
105
|
-
hostsTest = [],
|
|
106
|
-
gateways = ["common-gateway-eshetang"]
|
|
107
|
-
} = xfeConfig.deploy;
|
|
108
|
-
const { targetK8s = "saas", targetNamespace = "" } = xfeConfig.deploy;
|
|
109
|
-
valuesYaml.virtualService.enabled = virtualServiceEnabled;
|
|
110
|
-
valuesYaml.virtualService.gateways = gateways;
|
|
111
|
-
valuesYaml.virtualService.hosts.prod = hostsProd;
|
|
112
|
-
valuesYaml.virtualService.hosts.stage = hostsStage;
|
|
113
|
-
valuesYaml.virtualService.hosts.test = hostsTest;
|
|
114
|
-
valuesYaml.targetK8s = targetK8s;
|
|
115
|
-
valuesYaml.namespace = targetNamespace;
|
|
116
|
-
const valuesYamlStr = yaml.stringify(valuesYaml, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" });
|
|
117
|
-
fs.writeFileSync(valuesYamlFilePath, valuesYamlStr, "utf8");
|
|
175
|
+
fs.writeFileSync(path.join(target, "helm/values.yaml"), yaml.stringify(configuredValues, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
|
|
176
|
+
fs.writeFileSync(droneTarget, drone);
|
|
118
177
|
};
|
|
119
178
|
getJwtEnv = async () => {
|
|
120
179
|
const xfeConfig = loadXfeConfig();
|
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
|
+
/** 默认保留既有部署;managed 模式将公共模板和覆盖 values 生成到项目 deploy 目录。 */
|
|
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
|
+
/** 默认保留既有部署;managed 模式将公共模板和覆盖 values 生成到项目 deploy 目录。 */
|
|
30
30
|
declare const configDeploy: () => void;
|
|
31
31
|
declare const getJwtEnv: () => Promise<Partial<JwtEnv>>;
|
|
32
32
|
|
package/dist/config.js
CHANGED
|
@@ -101,40 +101,99 @@ var getServerEntry = (projectRoot, webapp = {}) => {
|
|
|
101
101
|
};
|
|
102
102
|
var configDeploy = () => {
|
|
103
103
|
const xfeConfig = loadXfeConfig();
|
|
104
|
-
|
|
104
|
+
const deployment = xfeConfig.deploy;
|
|
105
|
+
if (!deployment) return;
|
|
105
106
|
const configDirectory = typeof __dirname === "string" ? __dirname : import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
106
107
|
const projectRoot = process.cwd();
|
|
108
|
+
const source = import_node_path.default.resolve(configDirectory, "../deploy");
|
|
109
|
+
const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
|
|
107
110
|
const buildDirectory = import_node_path.default.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(import_node_path.default.sep, "/");
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
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"));
|
|
115
|
+
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")
|
|
123
|
+
));
|
|
124
|
+
if (deployment.managed) {
|
|
125
|
+
if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
|
|
126
|
+
import_node_fs.default.cpSync(source, target, { recursive: true });
|
|
127
|
+
}
|
|
113
128
|
import_node_fs.default.mkdirSync(import_node_path.default.dirname(droneTarget), { recursive: true });
|
|
114
|
-
import_node_fs.default.writeFileSync(
|
|
115
|
-
|
|
116
|
-
const valuesYamlFile = import_node_fs.default.readFileSync(valuesYamlFilePath, "utf8");
|
|
117
|
-
const valuesYaml = import_yaml.default.parse(valuesYamlFile);
|
|
118
|
-
if (!valuesYaml.virtualService) valuesYaml.virtualService = {};
|
|
119
|
-
if (!valuesYaml.virtualService.hosts) valuesYaml.virtualService.hosts = { test: [], stage: [], prod: [] };
|
|
120
|
-
const {
|
|
121
|
-
virtualServiceEnabled = true,
|
|
122
|
-
hostsProd = [],
|
|
123
|
-
hostsStage = [],
|
|
124
|
-
hostsTest = [],
|
|
125
|
-
gateways = ["common-gateway-eshetang"]
|
|
126
|
-
} = xfeConfig.deploy;
|
|
127
|
-
const { targetK8s = "saas", targetNamespace = "" } = xfeConfig.deploy;
|
|
128
|
-
valuesYaml.virtualService.enabled = virtualServiceEnabled;
|
|
129
|
-
valuesYaml.virtualService.gateways = gateways;
|
|
130
|
-
valuesYaml.virtualService.hosts.prod = hostsProd;
|
|
131
|
-
valuesYaml.virtualService.hosts.stage = hostsStage;
|
|
132
|
-
valuesYaml.virtualService.hosts.test = hostsTest;
|
|
133
|
-
valuesYaml.targetK8s = targetK8s;
|
|
134
|
-
valuesYaml.namespace = targetNamespace;
|
|
135
|
-
const valuesYamlStr = import_yaml.default.stringify(valuesYaml, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" });
|
|
136
|
-
import_node_fs.default.writeFileSync(valuesYamlFilePath, valuesYamlStr, "utf8");
|
|
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" }));
|
|
130
|
+
import_node_fs.default.writeFileSync(droneTarget, drone);
|
|
137
131
|
};
|
|
132
|
+
function mergeDeployValues(base, overrides) {
|
|
133
|
+
const entries = Object.entries({ ...base, ...overrides }).map(
|
|
134
|
+
/** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
|
|
135
|
+
([key, value]) => {
|
|
136
|
+
const previous = base[key];
|
|
137
|
+
const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
|
|
138
|
+
return [key, mergeable ? mergeDeployValues(previous, value) : value];
|
|
139
|
+
}
|
|
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");
|
|
196
|
+
}
|
|
138
197
|
var getJwtEnv = async () => {
|
|
139
198
|
const xfeConfig = loadXfeConfig();
|
|
140
199
|
if (!xfeConfig?.jwt) return {};
|
package/dist/config.mjs
CHANGED
|
@@ -62,40 +62,99 @@ var getServerEntry = (projectRoot, webapp = {}) => {
|
|
|
62
62
|
};
|
|
63
63
|
var configDeploy = () => {
|
|
64
64
|
const xfeConfig = loadXfeConfig();
|
|
65
|
-
|
|
65
|
+
const deployment = xfeConfig.deploy;
|
|
66
|
+
if (!deployment) return;
|
|
66
67
|
const configDirectory = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
|
67
68
|
const projectRoot = process.cwd();
|
|
69
|
+
const source = path.resolve(configDirectory, "../deploy");
|
|
70
|
+
const target = deployment.managed ? getBuildDirectory(projectRoot, { buildDirectory: "deploy" }) : source;
|
|
68
71
|
const buildDirectory = path.relative(projectRoot, getBuildDirectory(projectRoot, xfeConfig.webapp)).replaceAll(path.sep, "/");
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
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"));
|
|
76
|
+
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")
|
|
84
|
+
));
|
|
85
|
+
if (deployment.managed) {
|
|
86
|
+
if (target === source) throw new Error("managed \u90E8\u7F72\u8F93\u51FA\u4E0D\u80FD\u8986\u76D6\u6846\u67B6\u6A21\u677F\u76EE\u5F55");
|
|
87
|
+
fs.cpSync(source, target, { recursive: true });
|
|
88
|
+
}
|
|
74
89
|
fs.mkdirSync(path.dirname(droneTarget), { recursive: true });
|
|
75
|
-
fs.writeFileSync(
|
|
76
|
-
|
|
77
|
-
const valuesYamlFile = fs.readFileSync(valuesYamlFilePath, "utf8");
|
|
78
|
-
const valuesYaml = yaml.parse(valuesYamlFile);
|
|
79
|
-
if (!valuesYaml.virtualService) valuesYaml.virtualService = {};
|
|
80
|
-
if (!valuesYaml.virtualService.hosts) valuesYaml.virtualService.hosts = { test: [], stage: [], prod: [] };
|
|
81
|
-
const {
|
|
82
|
-
virtualServiceEnabled = true,
|
|
83
|
-
hostsProd = [],
|
|
84
|
-
hostsStage = [],
|
|
85
|
-
hostsTest = [],
|
|
86
|
-
gateways = ["common-gateway-eshetang"]
|
|
87
|
-
} = xfeConfig.deploy;
|
|
88
|
-
const { targetK8s = "saas", targetNamespace = "" } = xfeConfig.deploy;
|
|
89
|
-
valuesYaml.virtualService.enabled = virtualServiceEnabled;
|
|
90
|
-
valuesYaml.virtualService.gateways = gateways;
|
|
91
|
-
valuesYaml.virtualService.hosts.prod = hostsProd;
|
|
92
|
-
valuesYaml.virtualService.hosts.stage = hostsStage;
|
|
93
|
-
valuesYaml.virtualService.hosts.test = hostsTest;
|
|
94
|
-
valuesYaml.targetK8s = targetK8s;
|
|
95
|
-
valuesYaml.namespace = targetNamespace;
|
|
96
|
-
const valuesYamlStr = yaml.stringify(valuesYaml, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" });
|
|
97
|
-
fs.writeFileSync(valuesYamlFilePath, valuesYamlStr, "utf8");
|
|
90
|
+
fs.writeFileSync(path.join(target, "helm/values.yaml"), yaml.stringify(configuredValues, { defaultStringType: "QUOTE_SINGLE", defaultKeyType: "PLAIN" }));
|
|
91
|
+
fs.writeFileSync(droneTarget, drone);
|
|
98
92
|
};
|
|
93
|
+
function mergeDeployValues(base, overrides) {
|
|
94
|
+
const entries = Object.entries({ ...base, ...overrides }).map(
|
|
95
|
+
/** 只递归普通对象,列表保持调用方声明的顺序与完整内容。 */
|
|
96
|
+
([key, value]) => {
|
|
97
|
+
const previous = base[key];
|
|
98
|
+
const mergeable = value && previous && typeof value === "object" && typeof previous === "object" && !Array.isArray(value) && !Array.isArray(previous);
|
|
99
|
+
return [key, mergeable ? mergeDeployValues(previous, value) : value];
|
|
100
|
+
}
|
|
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");
|
|
157
|
+
}
|
|
99
158
|
var getJwtEnv = async () => {
|
|
100
159
|
const xfeConfig = loadXfeConfig();
|
|
101
160
|
if (!xfeConfig?.jwt) return {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xfe-repo/web-app",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.4",
|
|
4
4
|
"bin": {
|
|
5
5
|
"xfe-web": "./bin/index.js"
|
|
6
6
|
},
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"yaml": "^2.4.2",
|
|
31
31
|
"tsx": "4.23.15",
|
|
32
32
|
"@xfe-repo/eslint-config": "2.0.0-beta.1",
|
|
33
|
-
"@xfe-repo/
|
|
34
|
-
"@xfe-repo/
|
|
33
|
+
"@xfe-repo/typescript-config": "2.0.0-beta.1",
|
|
34
|
+
"@xfe-repo/web-router": "2.0.0-beta.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/express": "5.0.6"
|