@optima-chat/dev-skills 0.12.0 → 0.13.0
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.
|
@@ -28,6 +28,19 @@ optima-cn-deploy --list # 列出全部可发服务
|
|
|
28
28
|
5. 失败排查:命令输出的 flow.aliyun.com 链接里看具体 step 日志;常见原因是该分支/仓的 Codeup mirror 凭证问题(命令会在 mirror 步就报错并中止,不会构建旧代码)。
|
|
29
29
|
6. 成功标准:最后一行 `✓ SAE ImageUrl tag=<sha>` —— 流水线绿且 SAE 真的钉到了目标 commit。只看流水线绿不算完成。
|
|
30
30
|
|
|
31
|
+
|
|
32
|
+
## cn-prod vtag 发版(⚠️ 待 svenyang 首次验证通过后再日常使用)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
optima-cn-deploy <service> --vtag cn-v1.2.3 # 先 stage 同 tag 验证
|
|
36
|
+
optima-cn-deploy <service> --env prod --vtag cn-v1.2.3 # prod 发版
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- prod 是 vtag 制:必须先 `git tag cn-vX.Y.Z && git push`,工具拒绝无 vtag / 裸 v* / 带 --branch 的 prod 请求
|
|
40
|
+
- prod 流水线构建后**停在人工卡点**(xbfool/svenyang 在云效控制台审批),之后 DB 迁移 + digest 钉死部署自动走完
|
|
41
|
+
- 成功标准:prod SAE ImageUrl 为 `@sha256:` digest 寻址(工具最后一行校验)
|
|
42
|
+
- 用户请求发 cn-prod 时:确认已有 stage 验证过的 cn-v tag;没有则先引导走 stage
|
|
43
|
+
|
|
31
44
|
## 前置依赖
|
|
32
45
|
|
|
33
46
|
- `aliyun` CLI 已配置(profile 默认 `aliyun-optima`,可用环境变量 `OPTIMA_ALIYUN_PROFILE` 覆盖为自己的 RAM 用户 profile)
|
|
@@ -35,5 +48,5 @@ optima-cn-deploy --list # 列出全部可发服务
|
|
|
35
48
|
|
|
36
49
|
## 边界
|
|
37
50
|
|
|
38
|
-
-
|
|
51
|
+
- cn-prod 仅走 vtag 制(见上),且有人工卡点闸门;日常无 vtag 的构建只发 cn-stage。
|
|
39
52
|
- 服务注册表是 optima-terraform `alicloud/stacks/cn-prod-buildbox/yunxiao/` 的快照;新增服务先在那边 gen-pipelines 建好流水线,再同步本工具的 SERVICES 表。
|
|
@@ -28,6 +28,19 @@ optima-cn-deploy --list # 列出全部可发服务
|
|
|
28
28
|
5. 失败排查:命令输出的 flow.aliyun.com 链接里看具体 step 日志;常见原因是该分支/仓的 Codeup mirror 凭证问题(命令会在 mirror 步就报错并中止,不会构建旧代码)。
|
|
29
29
|
6. 成功标准:最后一行 `✓ SAE ImageUrl tag=<sha>` —— 流水线绿且 SAE 真的钉到了目标 commit。只看流水线绿不算完成。
|
|
30
30
|
|
|
31
|
+
|
|
32
|
+
## cn-prod vtag 发版(⚠️ 待 svenyang 首次验证通过后再日常使用)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
optima-cn-deploy <service> --vtag cn-v1.2.3 # 先 stage 同 tag 验证
|
|
36
|
+
optima-cn-deploy <service> --env prod --vtag cn-v1.2.3 # prod 发版
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- prod 是 vtag 制:必须先 `git tag cn-vX.Y.Z && git push`,工具拒绝无 vtag / 裸 v* / 带 --branch 的 prod 请求
|
|
40
|
+
- prod 流水线构建后**停在人工卡点**(xbfool/svenyang 在云效控制台审批),之后 DB 迁移 + digest 钉死部署自动走完
|
|
41
|
+
- 成功标准:prod SAE ImageUrl 为 `@sha256:` digest 寻址(工具最后一行校验)
|
|
42
|
+
- 用户请求发 cn-prod 时:确认已有 stage 验证过的 cn-v tag;没有则先引导走 stage
|
|
43
|
+
|
|
31
44
|
## 前置依赖
|
|
32
45
|
|
|
33
46
|
- `aliyun` CLI 已配置(profile 默认 `aliyun-optima`,可用环境变量 `OPTIMA_ALIYUN_PROFILE` 覆盖为自己的 RAM 用户 profile)
|
|
@@ -35,5 +48,5 @@ optima-cn-deploy --list # 列出全部可发服务
|
|
|
35
48
|
|
|
36
49
|
## 边界
|
|
37
50
|
|
|
38
|
-
-
|
|
51
|
+
- cn-prod 仅走 vtag 制(见上),且有人工卡点闸门;日常无 vtag 的构建只发 cn-stage。
|
|
39
52
|
- 服务注册表是 optima-terraform `alicloud/stacks/cn-prod-buildbox/yunxiao/` 的快照;新增服务先在那边 gen-pipelines 建好流水线,再同步本工具的 SERVICES 表。
|
package/bin/helpers/cn-deploy.ts
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* optima-cn-deploy user-auth --branch feat/xxx # 非 main 分支构建+部署到 cn-stage
|
|
19
19
|
* optima-cn-deploy gateway-core --no-wait # 只触发不等待
|
|
20
20
|
* optima-cn-deploy --list # 列出全部可发服务
|
|
21
|
+
* optima-cn-deploy billing --vtag cn-v1.2.3 # stage 按版本 tag 构建(发版前验证)
|
|
22
|
+
* optima-cn-deploy billing --env prod --vtag cn-v1.2.3
|
|
23
|
+
* # cn-prod vtag 发版:构建→停在人工卡点(云效控制台审批)→迁移→digest 钉死部署
|
|
21
24
|
*
|
|
22
25
|
* 前置: aliyun CLI(profile 默认 aliyun-optima,可用 OPTIMA_ALIYUN_PROFILE 覆盖)+ gh 已登录。
|
|
23
26
|
* 流水线定义的单一信源在 optima-terraform yunxiao/(gen-pipelines.py);本表为其快照,
|
|
@@ -31,28 +34,28 @@ const PROFILE = process.env.OPTIMA_ALIYUN_PROFILE || 'aliyun-optima';
|
|
|
31
34
|
const CODEUP_BASE = `https://codeup.aliyun.com/${ORG}`;
|
|
32
35
|
|
|
33
36
|
// 服务注册表(快照自 optima-terraform services.stage.env + gen-pipelines.py,2026-07-13)
|
|
34
|
-
interface Svc { pipelineId: number; repo: string; saeAppId?: string; }
|
|
37
|
+
interface Svc { pipelineId: number; repo: string; saeAppId?: string; prodSaeAppId?: string; }
|
|
35
38
|
const SERVICES: Record<string, Svc> = {
|
|
36
|
-
'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055' },
|
|
37
|
-
'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37' },
|
|
38
|
-
'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921' },
|
|
39
|
-
'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb' },
|
|
40
|
-
'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72' },
|
|
41
|
-
'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68' },
|
|
42
|
-
'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2' },
|
|
43
|
-
'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108' },
|
|
44
|
-
'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859' },
|
|
45
|
-
'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d' },
|
|
46
|
-
'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c' },
|
|
47
|
-
'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa' },
|
|
48
|
-
'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60' },
|
|
49
|
-
'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807' },
|
|
50
|
-
'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9' },
|
|
51
|
-
'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5' },
|
|
52
|
-
'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921' },
|
|
53
|
-
'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2' },
|
|
54
|
-
'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb' },
|
|
55
|
-
'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6' },
|
|
39
|
+
'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055', prodSaeAppId: '9211db38-a255-4393-ae92-7a7bb41b583d' },
|
|
40
|
+
'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37', prodSaeAppId: '6e290c73-a646-43ef-9da5-ad0b2e7eff73' },
|
|
41
|
+
'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921', prodSaeAppId: '6c31cf82-8802-4d45-b6a2-e9d7c83ccce9' },
|
|
42
|
+
'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb', prodSaeAppId: 'eb782ed9-c468-4e81-a0fe-87e5b4264192' },
|
|
43
|
+
'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72', prodSaeAppId: 'd40597b6-a98c-4063-9297-8b9fdfa8add4' },
|
|
44
|
+
'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68', prodSaeAppId: '762dd7a5-7fe8-41ed-9332-cfd31b0e087e' },
|
|
45
|
+
'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2', prodSaeAppId: '2c9ee19a-5062-4205-9f5b-6ae3f8062a23' },
|
|
46
|
+
'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108', prodSaeAppId: 'a08ce23f-3d3e-4d89-a2cf-53c8adba614e' },
|
|
47
|
+
'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859', prodSaeAppId: 'c6bc5a78-b27f-46e2-825a-eaa338c23645' },
|
|
48
|
+
'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d', prodSaeAppId: '732dfc8c-ed78-43f7-a722-c614359ff1a1' },
|
|
49
|
+
'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c', prodSaeAppId: '69e513fd-29bf-4355-a275-5eba82b21136' },
|
|
50
|
+
'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa', prodSaeAppId: '868d1a69-12c9-4bb7-868b-3e89c26784b5' },
|
|
51
|
+
'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60', prodSaeAppId: '2adc941e-374b-406a-9863-e53d40bc9500' },
|
|
52
|
+
'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807', prodSaeAppId: 'f5bb7e82-e57c-4bd4-83ae-2c25f8d38647' },
|
|
53
|
+
'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9', prodSaeAppId: 'e120e70c-b2c1-49ef-a66c-9016fd2d807d' },
|
|
54
|
+
'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5', prodSaeAppId: 'e0f9b92c-00af-4ade-9b07-15bba48c14cc' },
|
|
55
|
+
'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921', prodSaeAppId: '55a63ee7-fb75-46e4-99f3-20854a699237' },
|
|
56
|
+
'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2', prodSaeAppId: 'd6fbf9de-fed6-4165-978f-7b3a0a456acc' },
|
|
57
|
+
'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb', prodSaeAppId: '54093f99-37bf-4354-abd7-f6e76abdbcb6' },
|
|
58
|
+
'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6', prodSaeAppId: '5a820992-e11b-404a-9e58-8f607d8e0c5f' },
|
|
56
59
|
};
|
|
57
60
|
|
|
58
61
|
function sh(cmd: string, args: string[]): string {
|
|
@@ -88,10 +91,24 @@ async function main() {
|
|
|
88
91
|
const bIdx = argv.indexOf('--branch');
|
|
89
92
|
const branch = bIdx >= 0 ? argv[bIdx + 1] : 'main';
|
|
90
93
|
const noWait = argv.includes('--no-wait');
|
|
94
|
+
const eIdx = argv.indexOf('--env');
|
|
95
|
+
const envName = eIdx >= 0 ? argv[eIdx + 1] : 'stage';
|
|
96
|
+
const vIdx = argv.indexOf('--vtag');
|
|
97
|
+
const vtag = vIdx >= 0 ? argv[vIdx + 1] : '';
|
|
98
|
+
if (!['stage', 'prod'].includes(envName)) { console.error('✗ --env 只支持 stage|prod'); process.exit(1); }
|
|
99
|
+
if (vtag && !/^cn-v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(vtag)) {
|
|
100
|
+
console.error(`✗ vtag 格式必须 cn-vX.Y.Z(得到 ${vtag});裸 v* 会误触 AWS prod,绝不放行`); process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
if (envName === 'prod') {
|
|
103
|
+
// prod = vtag 制:先 stage 同 tag 验证,再发 prod;流水线内还有人工卡点
|
|
104
|
+
if (!vtag) { console.error('✗ --env prod 必须带 --vtag cn-vX.Y.Z(先 stage 同 tag 验证)'); process.exit(1); }
|
|
105
|
+
if (branch !== 'main') { console.error('✗ prod 发布按 vtag,不接受 --branch'); process.exit(1); }
|
|
106
|
+
}
|
|
107
|
+
const ref = vtag || branch;
|
|
91
108
|
|
|
92
109
|
// 1. GitHub HEAD + mirror 同步
|
|
93
|
-
const ghSha = sh('gh', ['api', `repos/Optima-Chat/${svc.repo}/commits/${
|
|
94
|
-
if (!ghSha) { console.error(`✗ GitHub 上无 ${svc.repo}@${
|
|
110
|
+
const ghSha = sh('gh', ['api', `repos/Optima-Chat/${svc.repo}/commits/${ref}`, '--jq', '.sha']);
|
|
111
|
+
if (!ghSha) { console.error(`✗ GitHub 上无 ${svc.repo}@${ref}(或 gh 未登录,tag 需已 push)`); process.exit(1); }
|
|
95
112
|
const repos = devops('ListRepositories', { perPage: '100' });
|
|
96
113
|
const repoId = (repos.result || []).find((r: any) => r.name === svc.repo)?.Id;
|
|
97
114
|
if (!repoId) { console.error(`✗ Codeup 无 mirror 仓 ${svc.repo}`); process.exit(1); }
|
|
@@ -99,29 +116,44 @@ async function main() {
|
|
|
99
116
|
devops('TriggerRepositoryMirrorSync', { repositoryId: String(repoId), account: 'xbfool', token: ghTok });
|
|
100
117
|
let synced = false;
|
|
101
118
|
for (let i = 0; i < 30; i++) {
|
|
102
|
-
const b = devops('GetBranchInfo', { repositoryId: String(repoId), branchName:
|
|
119
|
+
const b = devops('GetBranchInfo', { repositoryId: String(repoId), branchName: ref });
|
|
103
120
|
if (b?.result?.commit?.id === ghSha) { synced = true; break; }
|
|
104
121
|
await sleep(10000);
|
|
105
122
|
}
|
|
106
123
|
if (!synced) { console.error('✗ 300s 内 Codeup mirror 未追平 GitHub,中止(检查 mirror 凭证)'); process.exit(1); }
|
|
107
|
-
console.log(`✓ mirror 已追平 ${svc.repo}@${
|
|
124
|
+
console.log(`✓ mirror 已追平 ${svc.repo}@${ref} = ${ghSha.slice(0, 10)}`);
|
|
108
125
|
|
|
109
|
-
// 2. 触发(
|
|
110
|
-
|
|
111
|
-
if (
|
|
126
|
+
// 2. 触发(凭证由云效变量组供给,无需注入)。prod 流水线 id 按名实时解析(不硬编码)。
|
|
127
|
+
let pipelineId = svc.pipelineId;
|
|
128
|
+
if (envName === 'prod') {
|
|
129
|
+
const lp = devops('ListPipelines', { maxResults: '100' });
|
|
130
|
+
const hit = (lp.pipelines || []).find((p: any) => p.pipelineName === `${svcName}-cn-prod`);
|
|
131
|
+
if (!hit) { console.error(`✗ 云效无 ${svcName}-cn-prod 流水线`); process.exit(1); }
|
|
132
|
+
pipelineId = hit.pipelineId;
|
|
133
|
+
}
|
|
134
|
+
const kv: Record<string, string> = { pipelineId: String(pipelineId) };
|
|
135
|
+
if (vtag) {
|
|
136
|
+
kv.params = JSON.stringify({
|
|
137
|
+
runningTags: { [`${CODEUP_BASE}/${svc.repo}.git`]: vtag },
|
|
138
|
+
envs: { VTAG: vtag },
|
|
139
|
+
});
|
|
140
|
+
} else if (branch !== 'main') {
|
|
112
141
|
kv.params = JSON.stringify({ runningBranchs: { [`${CODEUP_BASE}/${svc.repo}.git`]: branch } });
|
|
113
142
|
}
|
|
114
143
|
const start = devops('StartPipelineRun', kv);
|
|
115
144
|
const runId = start.pipelineRunId;
|
|
116
145
|
if (!runId) { console.error(`✗ 启动失败: ${JSON.stringify(start).slice(0, 150)}`); process.exit(1); }
|
|
117
|
-
console.log(`▶ ${svcName} run#${runId} 已启动 (
|
|
118
|
-
console.log(` https://flow.aliyun.com/pipelines/${
|
|
146
|
+
console.log(`▶ ${svcName} run#${runId} 已启动 (ref=${ref}, env=${envName})`);
|
|
147
|
+
console.log(` https://flow.aliyun.com/pipelines/${pipelineId}/current`);
|
|
148
|
+
if (envName === 'prod') {
|
|
149
|
+
console.log('⏸ 构建完成后会停在『发布审批』人工卡点 —— 需 xbfool/svenyang 在云效控制台通过,之后迁移+digest 部署自动走完');
|
|
150
|
+
}
|
|
119
151
|
if (noWait) return;
|
|
120
152
|
|
|
121
153
|
// 3. 轮询
|
|
122
154
|
let status = '';
|
|
123
155
|
for (;;) {
|
|
124
|
-
const d = devops('GetPipelineRun', { pipelineId: String(
|
|
156
|
+
const d = devops('GetPipelineRun', { pipelineId: String(pipelineId), pipelineRunId: String(runId) });
|
|
125
157
|
status = d?.pipelineRun?.status || status;
|
|
126
158
|
if (['SUCCESS', 'FAIL', 'CANCELED', 'ERROR'].includes(status)) break;
|
|
127
159
|
await sleep(45000);
|
|
@@ -129,19 +161,26 @@ async function main() {
|
|
|
129
161
|
console.log(`${status === 'SUCCESS' ? '✅' : '❌'} ${svcName} run#${runId}: ${status}`);
|
|
130
162
|
if (status !== 'SUCCESS') process.exit(1);
|
|
131
163
|
|
|
132
|
-
// 4. SAE ImageUrl 校验(空返回重试守卫)
|
|
133
|
-
|
|
164
|
+
// 4. SAE ImageUrl 校验(空返回重试守卫)。prod 是 digest 寻址,校验含 @sha256 即部署成功。
|
|
165
|
+
const appId = envName === 'prod' ? svc.prodSaeAppId : svc.saeAppId;
|
|
166
|
+
if (appId) {
|
|
134
167
|
let img = '';
|
|
135
168
|
for (let i = 0; i < 3; i++) {
|
|
136
169
|
const raw = sh('aliyun', ['sae', '--profile', PROFILE, '--region', 'cn-beijing',
|
|
137
|
-
'DescribeApplicationConfig', '--AppId',
|
|
170
|
+
'DescribeApplicationConfig', '--AppId', appId]);
|
|
138
171
|
try { img = JSON.parse(raw)?.Data?.ImageUrl || ''; break; } catch { await sleep(3000); }
|
|
139
172
|
}
|
|
140
173
|
if (!img) { console.log('⚠️ SAE 配置查询连续空返回,跳过 ImageUrl 校验(流水线本身已 SUCCESS)'); return; }
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
174
|
+
if (envName === 'prod') {
|
|
175
|
+
const ok = img.includes('@sha256:');
|
|
176
|
+
console.log(`${ok ? '✓' : '✗'} SAE ImageUrl(prod 应为 digest 寻址): ${img.split('/').pop()}`);
|
|
177
|
+
if (!ok) process.exit(1);
|
|
178
|
+
} else {
|
|
179
|
+
const tag = img.split(':').pop() || '';
|
|
180
|
+
const ok = ghSha.startsWith(tag);
|
|
181
|
+
console.log(`${ok ? '✓' : '✗'} SAE ImageUrl tag=${tag} vs 目标 ${ghSha.slice(0, 10)}`);
|
|
182
|
+
if (!ok) process.exit(1);
|
|
183
|
+
}
|
|
145
184
|
}
|
|
146
185
|
}
|
|
147
186
|
|
|
@@ -20,6 +20,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
20
20
|
* optima-cn-deploy user-auth --branch feat/xxx # 非 main 分支构建+部署到 cn-stage
|
|
21
21
|
* optima-cn-deploy gateway-core --no-wait # 只触发不等待
|
|
22
22
|
* optima-cn-deploy --list # 列出全部可发服务
|
|
23
|
+
* optima-cn-deploy billing --vtag cn-v1.2.3 # stage 按版本 tag 构建(发版前验证)
|
|
24
|
+
* optima-cn-deploy billing --env prod --vtag cn-v1.2.3
|
|
25
|
+
* # cn-prod vtag 发版:构建→停在人工卡点(云效控制台审批)→迁移→digest 钉死部署
|
|
23
26
|
*
|
|
24
27
|
* 前置: aliyun CLI(profile 默认 aliyun-optima,可用 OPTIMA_ALIYUN_PROFILE 覆盖)+ gh 已登录。
|
|
25
28
|
* 流水线定义的单一信源在 optima-terraform yunxiao/(gen-pipelines.py);本表为其快照,
|
|
@@ -31,26 +34,26 @@ const ENDPOINT = 'devops.cn-hangzhou.aliyuncs.com';
|
|
|
31
34
|
const PROFILE = process.env.OPTIMA_ALIYUN_PROFILE || 'aliyun-optima';
|
|
32
35
|
const CODEUP_BASE = `https://codeup.aliyun.com/${ORG}`;
|
|
33
36
|
const SERVICES = {
|
|
34
|
-
'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055' },
|
|
35
|
-
'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37' },
|
|
36
|
-
'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921' },
|
|
37
|
-
'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb' },
|
|
38
|
-
'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72' },
|
|
39
|
-
'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68' },
|
|
40
|
-
'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2' },
|
|
41
|
-
'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108' },
|
|
42
|
-
'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859' },
|
|
43
|
-
'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d' },
|
|
44
|
-
'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c' },
|
|
45
|
-
'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa' },
|
|
46
|
-
'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60' },
|
|
47
|
-
'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807' },
|
|
48
|
-
'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9' },
|
|
49
|
-
'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5' },
|
|
50
|
-
'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921' },
|
|
51
|
-
'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2' },
|
|
52
|
-
'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb' },
|
|
53
|
-
'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6' },
|
|
37
|
+
'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055', prodSaeAppId: '9211db38-a255-4393-ae92-7a7bb41b583d' },
|
|
38
|
+
'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37', prodSaeAppId: '6e290c73-a646-43ef-9da5-ad0b2e7eff73' },
|
|
39
|
+
'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921', prodSaeAppId: '6c31cf82-8802-4d45-b6a2-e9d7c83ccce9' },
|
|
40
|
+
'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb', prodSaeAppId: 'eb782ed9-c468-4e81-a0fe-87e5b4264192' },
|
|
41
|
+
'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72', prodSaeAppId: 'd40597b6-a98c-4063-9297-8b9fdfa8add4' },
|
|
42
|
+
'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68', prodSaeAppId: '762dd7a5-7fe8-41ed-9332-cfd31b0e087e' },
|
|
43
|
+
'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2', prodSaeAppId: '2c9ee19a-5062-4205-9f5b-6ae3f8062a23' },
|
|
44
|
+
'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108', prodSaeAppId: 'a08ce23f-3d3e-4d89-a2cf-53c8adba614e' },
|
|
45
|
+
'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859', prodSaeAppId: 'c6bc5a78-b27f-46e2-825a-eaa338c23645' },
|
|
46
|
+
'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d', prodSaeAppId: '732dfc8c-ed78-43f7-a722-c614359ff1a1' },
|
|
47
|
+
'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c', prodSaeAppId: '69e513fd-29bf-4355-a275-5eba82b21136' },
|
|
48
|
+
'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa', prodSaeAppId: '868d1a69-12c9-4bb7-868b-3e89c26784b5' },
|
|
49
|
+
'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60', prodSaeAppId: '2adc941e-374b-406a-9863-e53d40bc9500' },
|
|
50
|
+
'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807', prodSaeAppId: 'f5bb7e82-e57c-4bd4-83ae-2c25f8d38647' },
|
|
51
|
+
'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9', prodSaeAppId: 'e120e70c-b2c1-49ef-a66c-9016fd2d807d' },
|
|
52
|
+
'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5', prodSaeAppId: 'e0f9b92c-00af-4ade-9b07-15bba48c14cc' },
|
|
53
|
+
'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921', prodSaeAppId: '55a63ee7-fb75-46e4-99f3-20854a699237' },
|
|
54
|
+
'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2', prodSaeAppId: 'd6fbf9de-fed6-4165-978f-7b3a0a456acc' },
|
|
55
|
+
'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb', prodSaeAppId: '54093f99-37bf-4354-abd7-f6e76abdbcb6' },
|
|
56
|
+
'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6', prodSaeAppId: '5a820992-e11b-404a-9e58-8f607d8e0c5f' },
|
|
54
57
|
};
|
|
55
58
|
function sh(cmd, args) {
|
|
56
59
|
try {
|
|
@@ -90,10 +93,34 @@ async function main() {
|
|
|
90
93
|
const bIdx = argv.indexOf('--branch');
|
|
91
94
|
const branch = bIdx >= 0 ? argv[bIdx + 1] : 'main';
|
|
92
95
|
const noWait = argv.includes('--no-wait');
|
|
96
|
+
const eIdx = argv.indexOf('--env');
|
|
97
|
+
const envName = eIdx >= 0 ? argv[eIdx + 1] : 'stage';
|
|
98
|
+
const vIdx = argv.indexOf('--vtag');
|
|
99
|
+
const vtag = vIdx >= 0 ? argv[vIdx + 1] : '';
|
|
100
|
+
if (!['stage', 'prod'].includes(envName)) {
|
|
101
|
+
console.error('✗ --env 只支持 stage|prod');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
if (vtag && !/^cn-v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(vtag)) {
|
|
105
|
+
console.error(`✗ vtag 格式必须 cn-vX.Y.Z(得到 ${vtag});裸 v* 会误触 AWS prod,绝不放行`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
if (envName === 'prod') {
|
|
109
|
+
// prod = vtag 制:先 stage 同 tag 验证,再发 prod;流水线内还有人工卡点
|
|
110
|
+
if (!vtag) {
|
|
111
|
+
console.error('✗ --env prod 必须带 --vtag cn-vX.Y.Z(先 stage 同 tag 验证)');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
if (branch !== 'main') {
|
|
115
|
+
console.error('✗ prod 发布按 vtag,不接受 --branch');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const ref = vtag || branch;
|
|
93
120
|
// 1. GitHub HEAD + mirror 同步
|
|
94
|
-
const ghSha = sh('gh', ['api', `repos/Optima-Chat/${svc.repo}/commits/${
|
|
121
|
+
const ghSha = sh('gh', ['api', `repos/Optima-Chat/${svc.repo}/commits/${ref}`, '--jq', '.sha']);
|
|
95
122
|
if (!ghSha) {
|
|
96
|
-
console.error(`✗ GitHub 上无 ${svc.repo}@${
|
|
123
|
+
console.error(`✗ GitHub 上无 ${svc.repo}@${ref}(或 gh 未登录,tag 需已 push)`);
|
|
97
124
|
process.exit(1);
|
|
98
125
|
}
|
|
99
126
|
const repos = devops('ListRepositories', { perPage: '100' });
|
|
@@ -106,7 +133,7 @@ async function main() {
|
|
|
106
133
|
devops('TriggerRepositoryMirrorSync', { repositoryId: String(repoId), account: 'xbfool', token: ghTok });
|
|
107
134
|
let synced = false;
|
|
108
135
|
for (let i = 0; i < 30; i++) {
|
|
109
|
-
const b = devops('GetBranchInfo', { repositoryId: String(repoId), branchName:
|
|
136
|
+
const b = devops('GetBranchInfo', { repositoryId: String(repoId), branchName: ref });
|
|
110
137
|
if (b?.result?.commit?.id === ghSha) {
|
|
111
138
|
synced = true;
|
|
112
139
|
break;
|
|
@@ -117,10 +144,26 @@ async function main() {
|
|
|
117
144
|
console.error('✗ 300s 内 Codeup mirror 未追平 GitHub,中止(检查 mirror 凭证)');
|
|
118
145
|
process.exit(1);
|
|
119
146
|
}
|
|
120
|
-
console.log(`✓ mirror 已追平 ${svc.repo}@${
|
|
121
|
-
// 2. 触发(
|
|
122
|
-
|
|
123
|
-
if (
|
|
147
|
+
console.log(`✓ mirror 已追平 ${svc.repo}@${ref} = ${ghSha.slice(0, 10)}`);
|
|
148
|
+
// 2. 触发(凭证由云效变量组供给,无需注入)。prod 流水线 id 按名实时解析(不硬编码)。
|
|
149
|
+
let pipelineId = svc.pipelineId;
|
|
150
|
+
if (envName === 'prod') {
|
|
151
|
+
const lp = devops('ListPipelines', { maxResults: '100' });
|
|
152
|
+
const hit = (lp.pipelines || []).find((p) => p.pipelineName === `${svcName}-cn-prod`);
|
|
153
|
+
if (!hit) {
|
|
154
|
+
console.error(`✗ 云效无 ${svcName}-cn-prod 流水线`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
pipelineId = hit.pipelineId;
|
|
158
|
+
}
|
|
159
|
+
const kv = { pipelineId: String(pipelineId) };
|
|
160
|
+
if (vtag) {
|
|
161
|
+
kv.params = JSON.stringify({
|
|
162
|
+
runningTags: { [`${CODEUP_BASE}/${svc.repo}.git`]: vtag },
|
|
163
|
+
envs: { VTAG: vtag },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else if (branch !== 'main') {
|
|
124
167
|
kv.params = JSON.stringify({ runningBranchs: { [`${CODEUP_BASE}/${svc.repo}.git`]: branch } });
|
|
125
168
|
}
|
|
126
169
|
const start = devops('StartPipelineRun', kv);
|
|
@@ -129,14 +172,17 @@ async function main() {
|
|
|
129
172
|
console.error(`✗ 启动失败: ${JSON.stringify(start).slice(0, 150)}`);
|
|
130
173
|
process.exit(1);
|
|
131
174
|
}
|
|
132
|
-
console.log(`▶ ${svcName} run#${runId} 已启动 (
|
|
133
|
-
console.log(` https://flow.aliyun.com/pipelines/${
|
|
175
|
+
console.log(`▶ ${svcName} run#${runId} 已启动 (ref=${ref}, env=${envName})`);
|
|
176
|
+
console.log(` https://flow.aliyun.com/pipelines/${pipelineId}/current`);
|
|
177
|
+
if (envName === 'prod') {
|
|
178
|
+
console.log('⏸ 构建完成后会停在『发布审批』人工卡点 —— 需 xbfool/svenyang 在云效控制台通过,之后迁移+digest 部署自动走完');
|
|
179
|
+
}
|
|
134
180
|
if (noWait)
|
|
135
181
|
return;
|
|
136
182
|
// 3. 轮询
|
|
137
183
|
let status = '';
|
|
138
184
|
for (;;) {
|
|
139
|
-
const d = devops('GetPipelineRun', { pipelineId: String(
|
|
185
|
+
const d = devops('GetPipelineRun', { pipelineId: String(pipelineId), pipelineRunId: String(runId) });
|
|
140
186
|
status = d?.pipelineRun?.status || status;
|
|
141
187
|
if (['SUCCESS', 'FAIL', 'CANCELED', 'ERROR'].includes(status))
|
|
142
188
|
break;
|
|
@@ -145,12 +191,13 @@ async function main() {
|
|
|
145
191
|
console.log(`${status === 'SUCCESS' ? '✅' : '❌'} ${svcName} run#${runId}: ${status}`);
|
|
146
192
|
if (status !== 'SUCCESS')
|
|
147
193
|
process.exit(1);
|
|
148
|
-
// 4. SAE ImageUrl 校验(空返回重试守卫)
|
|
149
|
-
|
|
194
|
+
// 4. SAE ImageUrl 校验(空返回重试守卫)。prod 是 digest 寻址,校验含 @sha256 即部署成功。
|
|
195
|
+
const appId = envName === 'prod' ? svc.prodSaeAppId : svc.saeAppId;
|
|
196
|
+
if (appId) {
|
|
150
197
|
let img = '';
|
|
151
198
|
for (let i = 0; i < 3; i++) {
|
|
152
199
|
const raw = sh('aliyun', ['sae', '--profile', PROFILE, '--region', 'cn-beijing',
|
|
153
|
-
'DescribeApplicationConfig', '--AppId',
|
|
200
|
+
'DescribeApplicationConfig', '--AppId', appId]);
|
|
154
201
|
try {
|
|
155
202
|
img = JSON.parse(raw)?.Data?.ImageUrl || '';
|
|
156
203
|
break;
|
|
@@ -163,11 +210,19 @@ async function main() {
|
|
|
163
210
|
console.log('⚠️ SAE 配置查询连续空返回,跳过 ImageUrl 校验(流水线本身已 SUCCESS)');
|
|
164
211
|
return;
|
|
165
212
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
213
|
+
if (envName === 'prod') {
|
|
214
|
+
const ok = img.includes('@sha256:');
|
|
215
|
+
console.log(`${ok ? '✓' : '✗'} SAE ImageUrl(prod 应为 digest 寻址): ${img.split('/').pop()}`);
|
|
216
|
+
if (!ok)
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const tag = img.split(':').pop() || '';
|
|
221
|
+
const ok = ghSha.startsWith(tag);
|
|
222
|
+
console.log(`${ok ? '✓' : '✗'} SAE ImageUrl tag=${tag} vs 目标 ${ghSha.slice(0, 10)}`);
|
|
223
|
+
if (!ok)
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
171
226
|
}
|
|
172
227
|
}
|
|
173
228
|
main().catch(e => { console.error('✗', e?.message || e); process.exit(1); });
|