@optima-chat/dev-skills 0.12.0 → 0.13.1
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/.claude/commands/query-db.md +13 -11
- package/.claude/skills/cn-deploy/SKILL.md +14 -1
- package/.claude/skills/query-db/SKILL.md +36 -6
- package/.codex/skills/cn-deploy/SKILL.md +14 -1
- package/.codex/skills/query-db/SKILL.md +12 -1
- package/AGENTS.md +4 -4
- package/bin/helpers/cn-deploy.ts +77 -38
- package/bin/helpers/query-db.ts +115 -15
- package/dist/bin/helpers/cn-deploy.js +93 -38
- package/dist/bin/helpers/query-db.js +103 -14
- package/package.json +5 -3
|
@@ -200,13 +200,14 @@ print(f\"COMMERCE_DB_PASSWORD={secrets['COMMERCE_DB_PASSWORD']}\")
|
|
|
200
200
|
" > /tmp/stage_db_config.sh && source /tmp/stage_db_config.sh
|
|
201
201
|
|
|
202
202
|
# 4. 建立 SSH 隧道到 Shared EC2,通过隧道访问 Stage RDS
|
|
203
|
-
|
|
203
|
+
# ⚠️ 先确认本地端口空闲(ss -ltn | grep 25432)——端口被本机 Docker PG 占用时,凭证会发给错误的库、报「密码错误」
|
|
204
|
+
ssh -i ~/.ssh/optima-ec2-key -f -N -L 25432:optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com:5432 ec2-user@3.0.210.113
|
|
204
205
|
|
|
205
|
-
# 5. 通过本地端口
|
|
206
|
-
PGPASSWORD="${COMMERCE_DB_PASSWORD}" psql -h localhost -p
|
|
206
|
+
# 5. 通过本地端口 25432 连接到 RDS
|
|
207
|
+
PGPASSWORD="${COMMERCE_DB_PASSWORD}" psql -h localhost -p 25432 -U "${COMMERCE_DB_USER}" -d optima_commerce -c "SELECT COUNT(*) FROM products"
|
|
207
208
|
|
|
208
209
|
# 6. 关闭 SSH 隧道(可选)
|
|
209
|
-
pkill -f "ssh.*
|
|
210
|
+
pkill -f "ssh.*25432:${DATABASE_HOST}:5432"
|
|
210
211
|
```
|
|
211
212
|
|
|
212
213
|
**完整示例(五个服务)**:
|
|
@@ -283,7 +284,7 @@ pkill -f "ssh.*15432:${DATABASE_HOST}:5432"
|
|
|
283
284
|
- billing、ads-backend、amazon-backend、browser-backend、shopify-backend、optima-generation、optima-sentinel 的凭证存在各自服务路径的 `DATABASE_URL` 中
|
|
284
285
|
- Stage RDS: `optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com`
|
|
285
286
|
- Shared EC2 IP: `3.0.210.113`
|
|
286
|
-
- SSH 隧道: 本地端口 `
|
|
287
|
+
- SSH 隧道: 本地端口 `25432` → Shared EC2 → Stage RDS `5432`
|
|
287
288
|
- Stage 和 Prod 有独立的 RDS 实例
|
|
288
289
|
- ⚠️ session-gateway 数据库名: Stage 用 `optima_shell`, Prod 用 `optima_ai_shell`
|
|
289
290
|
|
|
@@ -323,13 +324,14 @@ print(f\"COMMERCE_DB_PASSWORD={secrets['COMMERCE_DB_PASSWORD']}\")
|
|
|
323
324
|
" > /tmp/prod_db_config.sh && source /tmp/prod_db_config.sh
|
|
324
325
|
|
|
325
326
|
# 4. 建立 SSH 隧道到 Shared EC2,通过隧道访问 Prod RDS
|
|
326
|
-
|
|
327
|
+
# ⚠️ 先确认本地端口空闲(ss -ltn | grep 25433)——端口被本机 Docker PG 占用时,凭证会发给错误的库、报「密码错误」
|
|
328
|
+
ssh -i ~/.ssh/optima-ec2-key -f -N -L 25433:optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com:5432 ec2-user@3.0.210.113
|
|
327
329
|
|
|
328
|
-
# 5. 通过本地端口
|
|
329
|
-
PGPASSWORD="${COMMERCE_DB_PASSWORD}" psql -h localhost -p
|
|
330
|
+
# 5. 通过本地端口 25433 连接到 RDS
|
|
331
|
+
PGPASSWORD="${COMMERCE_DB_PASSWORD}" psql -h localhost -p 25433 -U "${COMMERCE_DB_USER}" -d optima_commerce -c "SELECT COUNT(*) FROM products"
|
|
330
332
|
|
|
331
333
|
# 6. 关闭 SSH 隧道(可选)
|
|
332
|
-
pkill -f "ssh.*
|
|
334
|
+
pkill -f "ssh.*25433:${DATABASE_HOST}:5432"
|
|
333
335
|
```
|
|
334
336
|
|
|
335
337
|
**完整示例(五个服务)**:
|
|
@@ -394,8 +396,8 @@ pkill -f "ssh.*15433:${DATABASE_HOST}:5432"
|
|
|
394
396
|
- billing、browser-backend、optima-generation 的凭证存在各自服务路径的 DATABASE_URL 中
|
|
395
397
|
- Prod RDS: `optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com`
|
|
396
398
|
- Shared EC2 IP: `3.0.210.113`
|
|
397
|
-
- SSH 隧道: 本地端口 `
|
|
398
|
-
- Stage 用端口 `
|
|
399
|
+
- SSH 隧道: 本地端口 `25433` → Shared EC2 → Prod RDS `5432`
|
|
400
|
+
- Stage 用端口 `25432`,Prod 用端口 `25433`
|
|
399
401
|
- Stage 和 Prod 有独立的 RDS 实例
|
|
400
402
|
- ⚠️ session-gateway 数据库名: Stage 用 `optima_shell`, Prod 用 `optima_ai_shell`
|
|
401
403
|
|
|
@@ -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 表。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "query-db"
|
|
3
|
-
description: "当用户请求查询数据库、执行SQL、查看数据、统计数据、检查数据库、查询表、数据库查询时,使用此技能。支持 CI、Stage、Prod
|
|
3
|
+
description: "当用户请求查询数据库、执行SQL、查看数据、统计数据、检查数据库、查询表、数据库查询时,使用此技能。支持 CI、Stage、Prod、cn-prod、cn-stage(阿里云)五个环境的 commerce-backend、user-auth、agentic-chat、bi-backend、session-gateway、gateway-core、ads-backend、amazon-backend、shopify-backend、optima-sentinel 等服务的数据库查询。优先使用 optima-query-db CLI 工具。"
|
|
4
4
|
allowed-tools: ["Bash", "SlashCommand"]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -16,6 +16,8 @@ allowed-tools: ["Bash", "SlashCommand"]
|
|
|
16
16
|
optima-query-db <service> "<sql>" [environment]
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
**环境怎么传**:environment 取 `ci`(默认)/ `stage` / `prod` / `cn-prod` / `cn-stage`,作为**第 3 个位置参数**(`optima-query-db gateway-core "SELECT 1" cn-stage`)或 **`--env` 旗标**(`--env cn-stage`,短形式 `-e`、`--env=cn-stage` 也认,与 optima-logs 一致)。除 `--env`/`-e` 外没有其他旗标;未知旗标、多余参数、未知环境名、纯注释 SQL 都会硬报错并打印 usage,绝不静默空输出(背景见 issue #60)。
|
|
20
|
+
|
|
19
21
|
**为什么使用 CLI 工具**:
|
|
20
22
|
- ✅ 统一实现,避免重复代码
|
|
21
23
|
- ✅ 自动处理所有环境差异
|
|
@@ -60,6 +62,12 @@ optima-query-db commerce-backend "SELECT COUNT(*) FROM orders" stage
|
|
|
60
62
|
|
|
61
63
|
# Prod 环境
|
|
62
64
|
optima-query-db commerce-backend "SELECT status, COUNT(*) FROM orders GROUP BY status" prod
|
|
65
|
+
|
|
66
|
+
# cn-stage 环境(阿里云预发;也可写成 --env cn-stage)
|
|
67
|
+
optima-query-db gateway-core "SELECT COUNT(*) FROM conversations" cn-stage
|
|
68
|
+
|
|
69
|
+
# cn-prod 环境(阿里云生产)
|
|
70
|
+
optima-query-db billing "SELECT COUNT(*) FROM credit_ledger" cn-prod
|
|
63
71
|
```
|
|
64
72
|
|
|
65
73
|
### 使用 Slash 命令(备用)
|
|
@@ -184,6 +192,26 @@ optima-query-db commerce-backend "SELECT status, COUNT(*) FROM orders GROUP BY s
|
|
|
184
192
|
- 通过 SSH 隧道访问 RDS
|
|
185
193
|
- ⚠️ 谨慎使用
|
|
186
194
|
|
|
195
|
+
### cn-stage / cn-prod 环境(阿里云)
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
optima-query-db gateway-core "SELECT COUNT(*) FROM conversations" cn-stage
|
|
199
|
+
optima-query-db gateway-core "SELECT key, value FROM app_configs" cn-prod
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**特点**:
|
|
203
|
+
- 阿里云侧独立部署:cn-stage(预发)与 cn-prod(生产)是**两个独立 RDS 实例**(共用 prod VPC 与 buildbox 跳板)
|
|
204
|
+
- 独立 Infisical 实例(secrets-cn.optima.chat,单实例双环境:cn-prod→`prod`、cn-stage→`staging`),与 AWS 侧 Infisical 完全无关
|
|
205
|
+
- 经 buildbox ECS 跳板自动建 SSH 隧道连内网 RDS
|
|
206
|
+
- ⚠️ cn-prod 是生产真实用户数据,谨慎使用
|
|
207
|
+
|
|
208
|
+
**前置环境变量**(缺失时命令会报错并提示,详见 https://github.com/Optima-Chat/optima-dev-skills/issues/21 ):
|
|
209
|
+
|
|
210
|
+
| 变量 | 用途 | 来源 |
|
|
211
|
+
|------|------|------|
|
|
212
|
+
| `INFISICAL_CN_EMAIL` + `INFISICAL_CN_PASSWORD` | 登录 cn Infisical 取数据库凭证 | 1Password「Infisical cn-prod admin (secrets-cn.optima.chat)」 |
|
|
213
|
+
| `OPTIMA_CN_BUILDBOX_PASSWORD` | 经 buildbox 跳板建 RDS 隧道(本机已有健康隧道时可省) | 1Password「Aliyun cn-prod buildbox ECS (root)」 |
|
|
214
|
+
|
|
187
215
|
## 🔧 技术架构
|
|
188
216
|
|
|
189
217
|
### Infisical 配置(v0.7.0+)
|
|
@@ -217,12 +245,14 @@ optima-query-db commerce-backend "SELECT status, COUNT(*) FROM orders GROUP BY s
|
|
|
217
245
|
|
|
218
246
|
### RDS 连接
|
|
219
247
|
|
|
220
|
-
| 环境 | RDS Host |
|
|
221
|
-
|
|
222
|
-
| Stage | `optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com` |
|
|
223
|
-
| Prod | `optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com` |
|
|
248
|
+
| 环境 | RDS Host | 跳板 |
|
|
249
|
+
|------|----------|------|
|
|
250
|
+
| Stage | `optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com` | Shared EC2 `3.0.210.113` |
|
|
251
|
+
| Prod | `optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com` | Shared EC2 `3.0.210.113` |
|
|
252
|
+
| cn-stage | `pgm-2zem1u9zdh06boim.pg.rds.aliyuncs.com` | buildbox ECS `47.94.105.163` |
|
|
253
|
+
| cn-prod | `pgm-2zexwx9eso9e4yla.pg.rds.aliyuncs.com` | buildbox ECS `47.94.105.163` |
|
|
224
254
|
|
|
225
|
-
|
|
255
|
+
**本地端口**:动态分配(自 25432 起扫描空闲端口,记录在 `~/.cache/optima-dev-skills/tunnel-ports.json`,复用前校验隧道健康);旧文档的固定 15432/15433 已废弃——固定端口会撞本机 Docker PG。
|
|
226
256
|
|
|
227
257
|
## 🔗 相关命令
|
|
228
258
|
|
|
@@ -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 表。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "query-db"
|
|
3
|
-
description: "Use when the user asks to query Optima databases, inspect rows, validate data, or run SQL in CI, Stage, or
|
|
3
|
+
description: "Use when the user asks to query Optima databases, inspect rows, validate data, or run SQL in CI, Stage, Prod, cn-prod, or cn-stage."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Query Databases
|
|
@@ -15,6 +15,17 @@ Always prefer:
|
|
|
15
15
|
optima-query-db <service> "<sql>" [environment]
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Environments
|
|
19
|
+
|
|
20
|
+
`ci` (default), `stage`, `prod`, `cn-prod` (Aliyun production), `cn-stage` (Aliyun staging).
|
|
21
|
+
|
|
22
|
+
Pass the environment as the 3rd positional argument (`optima-query-db gateway-core "SELECT 1" cn-stage`) or via the `--env` flag (`--env cn-stage`, consistent with optima-logs). Any other flag, extra positional argument, unknown environment name, or comment-only SQL is rejected with a hard error instead of a silent empty result (see issue #60).
|
|
23
|
+
|
|
24
|
+
For `cn-prod` / `cn-stage`, two extra env vars are required (the CLI prints an actionable error when missing; see https://github.com/Optima-Chat/optima-dev-skills/issues/21 ):
|
|
25
|
+
|
|
26
|
+
- `INFISICAL_CN_EMAIL` + `INFISICAL_CN_PASSWORD` — cn Infisical login (1Password "Infisical cn-prod admin (secrets-cn.optima.chat)")
|
|
27
|
+
- `OPTIMA_CN_BUILDBOX_PASSWORD` — buildbox ECS root password for the RDS tunnel (1Password "Aliyun cn-prod buildbox ECS (root)"); optional when a healthy tunnel already exists
|
|
28
|
+
|
|
18
29
|
## Services
|
|
19
30
|
|
|
20
31
|
- `commerce-backend`
|
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Optima Dev Skills For Codex
|
|
2
2
|
|
|
3
|
-
This repository provides shared development skills and CLI helpers for Optima engineers across `ci`, `stage`, and `prod
|
|
3
|
+
This repository provides shared development skills and CLI helpers for Optima engineers across `ci`, `stage`, `prod`, `cn-stage`, and `cn-prod` (Aliyun).
|
|
4
4
|
|
|
5
5
|
## Primary Entry Points
|
|
6
6
|
|
|
@@ -51,9 +51,9 @@ Some flows also require:
|
|
|
51
51
|
|
|
52
52
|
## Safety Rules
|
|
53
53
|
|
|
54
|
-
- Default to `ci` or `stage` when possible
|
|
55
|
-
- Treat `prod` as read-only unless the user explicitly asks for a write action.
|
|
56
|
-
- For `prod`, prefer limited queries and targeted operational commands.
|
|
54
|
+
- Default to `ci` or `stage` when possible; on the Aliyun side, default to `cn-stage`.
|
|
55
|
+
- Treat `prod` and `cn-prod` as read-only unless the user explicitly asks for a write action — `cn-prod` holds real production user data.
|
|
56
|
+
- For `prod` / `cn-prod`, prefer limited queries and targeted operational commands.
|
|
57
57
|
- Do not expose secrets unless the user explicitly asks to inspect them.
|
|
58
58
|
|
|
59
59
|
## Platform Notes
|
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
|
|
package/bin/helpers/query-db.ts
CHANGED
|
@@ -150,20 +150,111 @@ function queryDatabase(host: string, port: number, user: string, password: strin
|
|
|
150
150
|
return result;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
const USAGE = `Usage: optima-query-db <service> "<sql>" [environment]
|
|
154
|
+
or: optima-query-db <service> "<sql>" --env <environment>
|
|
155
|
+
|
|
156
|
+
Services: commerce-backend, user-auth, agentic-chat, bi-backend, session-gateway, gateway-core, optima-logistics, billing, ads-backend, amazon-backend, browser-backend, shopify-backend, optima-generation, optima-sentinel
|
|
157
|
+
Environments: ci (default), stage, prod, cn-prod (阿里云生产), cn-stage (阿里云预发)
|
|
158
|
+
|
|
159
|
+
Example: optima-query-db user-auth "SELECT COUNT(*) FROM users" prod`;
|
|
160
|
+
|
|
161
|
+
/** 参数用法错误(入口打印 message + usage 后退非零,区别于运行期错误)。 */
|
|
162
|
+
export class QueryDbUsageError extends Error {}
|
|
163
|
+
|
|
164
|
+
const VALID_ENVS = ['ci', 'stage', 'prod', 'cn-prod', 'cn-stage'];
|
|
165
|
+
// 'cn' 是历史别名(isCnEnv 认作 cn-prod),继续放行但不在 usage 里宣传。
|
|
166
|
+
const ENV_ALIASES = ['cn'];
|
|
167
|
+
|
|
168
|
+
// 单 token 旗标形态,含 --flag=value / 下划线(--env=cn-stage、--dry_run)。
|
|
169
|
+
// 带空白的 SQL 文本不会匹配;整段是注释的 SQL 由下方空语句检查兜住。
|
|
170
|
+
const FLAG_RE = /^--?[A-Za-z][-A-Za-z0-9_]*(=.*)?$/;
|
|
171
|
+
|
|
172
|
+
// SQL 去掉块注释、行注释、分号与空白后是否不剩任何语句(psql 对纯注释静默 no-op + exit 0)。
|
|
173
|
+
// 已知残留:PostgreSQL 块注释可嵌套(/* a /* b …两层闭合… ),本检查非嵌套感知会放行——
|
|
174
|
+
// 现实中几乎不出现,真出现也只是退回旧的空输出行为,不误伤合法 SQL。
|
|
175
|
+
function isEffectivelyEmptySql(sql: string): boolean {
|
|
176
|
+
const stripped = sql
|
|
177
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
178
|
+
.split('\n').map((l) => l.replace(/--.*/, '')).join('\n')
|
|
179
|
+
.replace(/[;\s]+/g, '');
|
|
180
|
+
return stripped.length === 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 解析 `<service> <sql> [environment]`,environment 也可经 --env/-e 旗标给
|
|
185
|
+
* (对齐 optima-logs 惯例)。历史 footgun(#60):`--env cn-stage` 曾被吞成
|
|
186
|
+
* sql='--env'(SQL 注释 = no-op)+ 真 SQL 静默丢弃,空输出 + exit 0 被误判成
|
|
187
|
+
* 「环境读不到」。故未知旗标、多余参数、未知环境、纯注释 SQL 一律硬报错。
|
|
188
|
+
*/
|
|
189
|
+
export function parseQueryDbArgs(
|
|
190
|
+
args: string[],
|
|
191
|
+
): { service: string; sql: string; environment: string } {
|
|
192
|
+
const positionals: string[] = [];
|
|
193
|
+
let envFromFlag: string | undefined;
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < args.length; i++) {
|
|
196
|
+
const arg = args[i];
|
|
197
|
+
if (arg === '--env' || arg === '-e' || arg.startsWith('--env=') || arg.startsWith('-e=')) {
|
|
198
|
+
let value: string | undefined;
|
|
199
|
+
if (arg.includes('=')) {
|
|
200
|
+
value = arg.slice(arg.indexOf('=') + 1);
|
|
201
|
+
} else {
|
|
202
|
+
value = args[i + 1];
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
if (!value || FLAG_RE.test(value)) {
|
|
206
|
+
throw new QueryDbUsageError('--env 需要环境名,如 --env cn-stage');
|
|
207
|
+
}
|
|
208
|
+
if (envFromFlag !== undefined) {
|
|
209
|
+
throw new QueryDbUsageError('--env 重复指定');
|
|
210
|
+
}
|
|
211
|
+
envFromFlag = value;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (FLAG_RE.test(arg)) {
|
|
215
|
+
throw new QueryDbUsageError(`未知旗标 ${arg}(仅支持 --env <环境>,其余都是位置参数)`);
|
|
216
|
+
}
|
|
217
|
+
positionals.push(arg);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (positionals.length < 2) {
|
|
221
|
+
throw new QueryDbUsageError('缺少参数:需要 <service> <sql>');
|
|
222
|
+
}
|
|
223
|
+
const maxPositionals = envFromFlag !== undefined ? 2 : 3;
|
|
224
|
+
if (positionals.length > maxPositionals) {
|
|
225
|
+
throw new QueryDbUsageError(
|
|
226
|
+
envFromFlag !== undefined && positionals.length === 3
|
|
227
|
+
? `environment 同时以 --env 和位置参数给出:${envFromFlag} vs ${positionals[2]}`
|
|
228
|
+
: `多余参数已拒绝(绝不静默丢弃):${positionals.slice(maxPositionals).join(' ')}。SQL 含空格时记得整体加引号`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const [service, sql] = positionals;
|
|
233
|
+
const environment = envFromFlag ?? positionals[2] ?? 'ci';
|
|
234
|
+
|
|
235
|
+
if (!VALID_ENVS.includes(environment) && !ENV_ALIASES.includes(environment)) {
|
|
236
|
+
throw new QueryDbUsageError(`未知环境 ${environment}(可选 ${VALID_ENVS.join(' | ')})`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (isEffectivelyEmptySql(sql)) {
|
|
240
|
+
throw new QueryDbUsageError(
|
|
241
|
+
'SQL 为空或全是注释/分号(psql 会静默 no-op)——检查参数顺序:<service> <sql> [environment]',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { service, sql, environment };
|
|
246
|
+
}
|
|
247
|
+
|
|
153
248
|
async function main() {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
console.
|
|
159
|
-
|
|
160
|
-
console.error('Environments: ci (default), stage, prod, cn-prod (阿里云生产), cn-stage (阿里云预发)');
|
|
161
|
-
console.error('');
|
|
162
|
-
console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
|
|
163
|
-
process.exit(1);
|
|
249
|
+
const argv = process.argv.slice(2);
|
|
250
|
+
// help 只认第 1 个参数位:尾部混入的 -h 走未知旗标报错(exit 1),
|
|
251
|
+
// 不给「打 usage 后 exit 0 但没跑 SQL」的静默通道。
|
|
252
|
+
if (argv[0] === '--help' || argv[0] === '-h') {
|
|
253
|
+
console.log(USAGE);
|
|
254
|
+
return;
|
|
164
255
|
}
|
|
165
256
|
|
|
166
|
-
const
|
|
257
|
+
const { service, sql, environment } = parseQueryDbArgs(argv);
|
|
167
258
|
|
|
168
259
|
if (!SERVICE_DB_MAP[service as keyof typeof SERVICE_DB_MAP]) {
|
|
169
260
|
console.error(`Unknown service: ${service}`);
|
|
@@ -273,7 +364,16 @@ async function main() {
|
|
|
273
364
|
}
|
|
274
365
|
}
|
|
275
366
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
367
|
+
// Only run the CLI flow when invoked directly — being require()'d (e.g. by the
|
|
368
|
+
// unit tests for parseQueryDbArgs) must not trigger main().
|
|
369
|
+
if (require.main === module) {
|
|
370
|
+
main().catch(error => {
|
|
371
|
+
if (error instanceof QueryDbUsageError) {
|
|
372
|
+
console.error(`❌ ${error.message}\n`);
|
|
373
|
+
console.error(USAGE);
|
|
374
|
+
} else {
|
|
375
|
+
console.error('\n❌ Error:', error.message);
|
|
376
|
+
}
|
|
377
|
+
process.exit(1);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
@@ -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); });
|
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.QueryDbUsageError = void 0;
|
|
38
|
+
exports.parseQueryDbArgs = parseQueryDbArgs;
|
|
37
39
|
const child_process_1 = require("child_process");
|
|
38
40
|
const fs = __importStar(require("fs"));
|
|
39
41
|
const db_utils_1 = require("./db-utils");
|
|
@@ -164,18 +166,95 @@ function queryDatabase(host, port, user, password, database, sql) {
|
|
|
164
166
|
});
|
|
165
167
|
return result;
|
|
166
168
|
}
|
|
169
|
+
const USAGE = `Usage: optima-query-db <service> "<sql>" [environment]
|
|
170
|
+
or: optima-query-db <service> "<sql>" --env <environment>
|
|
171
|
+
|
|
172
|
+
Services: commerce-backend, user-auth, agentic-chat, bi-backend, session-gateway, gateway-core, optima-logistics, billing, ads-backend, amazon-backend, browser-backend, shopify-backend, optima-generation, optima-sentinel
|
|
173
|
+
Environments: ci (default), stage, prod, cn-prod (阿里云生产), cn-stage (阿里云预发)
|
|
174
|
+
|
|
175
|
+
Example: optima-query-db user-auth "SELECT COUNT(*) FROM users" prod`;
|
|
176
|
+
/** 参数用法错误(入口打印 message + usage 后退非零,区别于运行期错误)。 */
|
|
177
|
+
class QueryDbUsageError extends Error {
|
|
178
|
+
}
|
|
179
|
+
exports.QueryDbUsageError = QueryDbUsageError;
|
|
180
|
+
const VALID_ENVS = ['ci', 'stage', 'prod', 'cn-prod', 'cn-stage'];
|
|
181
|
+
// 'cn' 是历史别名(isCnEnv 认作 cn-prod),继续放行但不在 usage 里宣传。
|
|
182
|
+
const ENV_ALIASES = ['cn'];
|
|
183
|
+
// 单 token 旗标形态,含 --flag=value / 下划线(--env=cn-stage、--dry_run)。
|
|
184
|
+
// 带空白的 SQL 文本不会匹配;整段是注释的 SQL 由下方空语句检查兜住。
|
|
185
|
+
const FLAG_RE = /^--?[A-Za-z][-A-Za-z0-9_]*(=.*)?$/;
|
|
186
|
+
// SQL 去掉块注释、行注释、分号与空白后是否不剩任何语句(psql 对纯注释静默 no-op + exit 0)。
|
|
187
|
+
// 已知残留:PostgreSQL 块注释可嵌套(/* a /* b …两层闭合… ),本检查非嵌套感知会放行——
|
|
188
|
+
// 现实中几乎不出现,真出现也只是退回旧的空输出行为,不误伤合法 SQL。
|
|
189
|
+
function isEffectivelyEmptySql(sql) {
|
|
190
|
+
const stripped = sql
|
|
191
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
192
|
+
.split('\n').map((l) => l.replace(/--.*/, '')).join('\n')
|
|
193
|
+
.replace(/[;\s]+/g, '');
|
|
194
|
+
return stripped.length === 0;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 解析 `<service> <sql> [environment]`,environment 也可经 --env/-e 旗标给
|
|
198
|
+
* (对齐 optima-logs 惯例)。历史 footgun(#60):`--env cn-stage` 曾被吞成
|
|
199
|
+
* sql='--env'(SQL 注释 = no-op)+ 真 SQL 静默丢弃,空输出 + exit 0 被误判成
|
|
200
|
+
* 「环境读不到」。故未知旗标、多余参数、未知环境、纯注释 SQL 一律硬报错。
|
|
201
|
+
*/
|
|
202
|
+
function parseQueryDbArgs(args) {
|
|
203
|
+
const positionals = [];
|
|
204
|
+
let envFromFlag;
|
|
205
|
+
for (let i = 0; i < args.length; i++) {
|
|
206
|
+
const arg = args[i];
|
|
207
|
+
if (arg === '--env' || arg === '-e' || arg.startsWith('--env=') || arg.startsWith('-e=')) {
|
|
208
|
+
let value;
|
|
209
|
+
if (arg.includes('=')) {
|
|
210
|
+
value = arg.slice(arg.indexOf('=') + 1);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
value = args[i + 1];
|
|
214
|
+
i++;
|
|
215
|
+
}
|
|
216
|
+
if (!value || FLAG_RE.test(value)) {
|
|
217
|
+
throw new QueryDbUsageError('--env 需要环境名,如 --env cn-stage');
|
|
218
|
+
}
|
|
219
|
+
if (envFromFlag !== undefined) {
|
|
220
|
+
throw new QueryDbUsageError('--env 重复指定');
|
|
221
|
+
}
|
|
222
|
+
envFromFlag = value;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (FLAG_RE.test(arg)) {
|
|
226
|
+
throw new QueryDbUsageError(`未知旗标 ${arg}(仅支持 --env <环境>,其余都是位置参数)`);
|
|
227
|
+
}
|
|
228
|
+
positionals.push(arg);
|
|
229
|
+
}
|
|
230
|
+
if (positionals.length < 2) {
|
|
231
|
+
throw new QueryDbUsageError('缺少参数:需要 <service> <sql>');
|
|
232
|
+
}
|
|
233
|
+
const maxPositionals = envFromFlag !== undefined ? 2 : 3;
|
|
234
|
+
if (positionals.length > maxPositionals) {
|
|
235
|
+
throw new QueryDbUsageError(envFromFlag !== undefined && positionals.length === 3
|
|
236
|
+
? `environment 同时以 --env 和位置参数给出:${envFromFlag} vs ${positionals[2]}`
|
|
237
|
+
: `多余参数已拒绝(绝不静默丢弃):${positionals.slice(maxPositionals).join(' ')}。SQL 含空格时记得整体加引号`);
|
|
238
|
+
}
|
|
239
|
+
const [service, sql] = positionals;
|
|
240
|
+
const environment = envFromFlag ?? positionals[2] ?? 'ci';
|
|
241
|
+
if (!VALID_ENVS.includes(environment) && !ENV_ALIASES.includes(environment)) {
|
|
242
|
+
throw new QueryDbUsageError(`未知环境 ${environment}(可选 ${VALID_ENVS.join(' | ')})`);
|
|
243
|
+
}
|
|
244
|
+
if (isEffectivelyEmptySql(sql)) {
|
|
245
|
+
throw new QueryDbUsageError('SQL 为空或全是注释/分号(psql 会静默 no-op)——检查参数顺序:<service> <sql> [environment]');
|
|
246
|
+
}
|
|
247
|
+
return { service, sql, environment };
|
|
248
|
+
}
|
|
167
249
|
async function main() {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.
|
|
173
|
-
|
|
174
|
-
console.error('');
|
|
175
|
-
console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
|
|
176
|
-
process.exit(1);
|
|
250
|
+
const argv = process.argv.slice(2);
|
|
251
|
+
// help 只认第 1 个参数位:尾部混入的 -h 走未知旗标报错(exit 1),
|
|
252
|
+
// 不给「打 usage 后 exit 0 但没跑 SQL」的静默通道。
|
|
253
|
+
if (argv[0] === '--help' || argv[0] === '-h') {
|
|
254
|
+
console.log(USAGE);
|
|
255
|
+
return;
|
|
177
256
|
}
|
|
178
|
-
const
|
|
257
|
+
const { service, sql, environment } = parseQueryDbArgs(argv);
|
|
179
258
|
if (!SERVICE_DB_MAP[service]) {
|
|
180
259
|
console.error(`Unknown service: ${service}`);
|
|
181
260
|
console.error('Available services:', Object.keys(SERVICE_DB_MAP).join(', '));
|
|
@@ -267,7 +346,17 @@ async function main() {
|
|
|
267
346
|
console.log('\n' + result);
|
|
268
347
|
}
|
|
269
348
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
349
|
+
// Only run the CLI flow when invoked directly — being require()'d (e.g. by the
|
|
350
|
+
// unit tests for parseQueryDbArgs) must not trigger main().
|
|
351
|
+
if (require.main === module) {
|
|
352
|
+
main().catch(error => {
|
|
353
|
+
if (error instanceof QueryDbUsageError) {
|
|
354
|
+
console.error(`❌ ${error.message}\n`);
|
|
355
|
+
console.error(USAGE);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
console.error('\n❌ Error:', error.message);
|
|
359
|
+
}
|
|
360
|
+
process.exit(1);
|
|
361
|
+
});
|
|
362
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optima-chat/dev-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"postinstall": "node scripts/install.js",
|
|
25
25
|
"build": "tsc",
|
|
26
|
-
"prepare": "npm run build"
|
|
26
|
+
"prepare": "npm run build",
|
|
27
|
+
"pretest": "npm run build",
|
|
28
|
+
"test": "node --test tests/*.test.js"
|
|
27
29
|
},
|
|
28
30
|
"keywords": [
|
|
29
31
|
"claude-code",
|
|
@@ -63,4 +65,4 @@
|
|
|
63
65
|
"ts-node": "^10.9.2",
|
|
64
66
|
"typescript": "^5.9.3"
|
|
65
67
|
}
|
|
66
|
-
}
|
|
68
|
+
}
|