@wdyy/skills 0.1.5 → 0.1.6
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/.well-known/skills/index.json +3 -3
- package/.well-known/skills/wdyy-deployment-standard/SKILL.md +34 -21
- package/.well-known/skills/wdyy-deployment-standard/agents/openai.yaml +2 -2
- package/.well-known/skills/wdyy-deployment-standard/reference/linux-deployment-rules.md +129 -7
- package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.mjs +159 -11
- package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.test.mjs +576 -38
- package/.well-known/skills/wdyy-deployment-standard/templates/Dockerfile.template +6 -1
- package/.well-known/skills/wdyy-deployment-standard/templates/deploy.sh.template +588 -77
- package/.well-known/skills/wdyy-deployment-standard/templates/docker-compose.blue-green.yml +10 -8
- package/.well-known/skills/wdyy-deployment-standard/templates/dockerignore.template +15 -0
- package/.well-known/skills/wdyy-deployment-standard/templates/nginx-upstream.template.conf +5 -4
- package/.well-known/skills/wdyy-logging-standard/SKILL.md +8 -7
- package/.well-known/skills/wdyy-logging-standard/agents/openai.yaml +2 -2
- package/.well-known/skills/wdyy-logging-standard/reference/logging-rules.md +4 -3
- package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.test.mjs +50 -1
- package/.well-known/skills/wdyy-logging-standard/templates/logger.template.ts +7 -0
- package/package.json +1 -1
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
"name": "wdyy-deployment-standard",
|
|
10
|
-
"description": "为无 CI/CD 的 Linux
|
|
11
|
-
"files": ["SKILL.md", "agents/openai.yaml", "reference/linux-deployment-rules.md", "scripts/validate-deployment-package.mjs", "scripts/validate-deployment-package.test.mjs", "templates/Dockerfile.template", "templates/deploy.sh.template", "templates/docker-compose.blue-green.yml", "templates/nginx-upstream.template.conf"]
|
|
10
|
+
"description": "为无 CI/CD 的 Linux 生产环境生成 pnpm 开发流程、本地 deploy.sh build 一键打包、AGENTS.md 自动发布编号、deploy 目录人工上传及 deploy.sh start 蓝绿部署与回滚规范。Use when 规划、实现、审查或执行手工生产部署时。",
|
|
11
|
+
"files": ["SKILL.md", "agents/openai.yaml", "reference/linux-deployment-rules.md", "scripts/validate-deployment-package.mjs", "scripts/validate-deployment-package.test.mjs", "templates/Dockerfile.template", "templates/deploy.sh.template", "templates/docker-compose.blue-green.yml", "templates/dockerignore.template", "templates/nginx-upstream.template.conf"]
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"name": "wdyy-api-standard",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"name": "wdyy-logging-standard",
|
|
25
|
-
"description": "为 NestJS 和 Vue
|
|
25
|
+
"description": "为 NestJS 和 Vue 项目实现仅写入项目根目录 ./logs 的结构化 JSON 日志、前端异常上报、traceId 传递、完整原始入参与 2MB 文件轮转。Use when 编写日志、错误处理、接口日志或审查生产可观测性时。",
|
|
26
26
|
"files": ["SKILL.md", "agents/openai.yaml", "reference/logging-rules.md", "scripts/validate-log-entry.mjs", "scripts/validate-log-entry.test.mjs", "templates/frontend-error-report.template.ts", "templates/logger.template.ts"]
|
|
27
27
|
}
|
|
28
28
|
]
|
|
@@ -1,52 +1,60 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wdyy-deployment-standard
|
|
3
|
-
description: 为无 CI/CD 的 Linux
|
|
3
|
+
description: 为无 CI/CD 的 Linux 生产环境生成 pnpm 开发流程、本地 deploy.sh build 一键打包、AGENTS.md 自动发布编号、deploy 目录人工上传及 deploy.sh start 蓝绿部署与回滚规范。Use when 规划、实现、审查或执行手工生产部署时。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 企业部署规范
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## When to Use
|
|
13
|
-
|
|
14
|
-
- 规划、实现、审查或发布生产部署时。
|
|
10
|
+
将项目开发与生产发布分离为 `pnpm dev`、本地一键打包、人工全目录上传和服务器一键启动,同时保留版本化前端、后端蓝绿切流、健康检查和回滚。
|
|
15
11
|
|
|
16
12
|
## 协作边界
|
|
17
13
|
|
|
18
|
-
|
|
14
|
+
生成目标项目根目录 `deploy.sh` 及其实际依赖的部署配置。工程师负责人工上传 `deploy/` 下全部文件并在服务器手动执行脚本;不得在脚本中上传、拉取代码、安装依赖或替代 GO/NO-GO 决策。
|
|
19
15
|
|
|
20
16
|
## 输入与输出
|
|
21
17
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- 读取 [Linux 部署规则](reference/linux-deployment-rules.md)
|
|
18
|
+
- 输入:目标项目真实的 pnpm scripts、前后端构建路径、Dockerfile、版本接口、健康检查、数据库迁移、Nginx 权限及服务器 `.env.production`。
|
|
19
|
+
- 输出:项目根目录 `deploy.sh`、`.dockerignore`、后端 Dockerfile、蓝绿 Compose、版本化前端 Nginx 配置,以及 `./deploy/<YYYYMMDD-NNN>/` 完整发布包。
|
|
20
|
+
- 读取 [Linux 部署规则](reference/linux-deployment-rules.md),按目标项目实际结构使用 `templates/`,并运行 `scripts/validate-deployment-package.mjs`。
|
|
25
21
|
|
|
26
22
|
## 执行步骤
|
|
27
23
|
|
|
28
|
-
1.
|
|
29
|
-
2.
|
|
30
|
-
3.
|
|
31
|
-
4.
|
|
32
|
-
5.
|
|
33
|
-
6.
|
|
34
|
-
7.
|
|
35
|
-
8.
|
|
24
|
+
1. 开发测试阶段只使用项目根目录 `pnpm dev`,不得由部署脚本管理开发服务或数据库。
|
|
25
|
+
2. 生成无版本参数的 `./deploy.sh build`。从目标项目 `AGENTS.md` 的固定发布记录表读取当天最大序号,自动生成 `YYYYMMDD-NNN`。
|
|
26
|
+
3. 在本地依次通过测试、lint、类型检查、`pnpm build`、前端版本路径、镜像和发布包校验;任一步失败不得写发布记录或暴露半成品。
|
|
27
|
+
4. 重新生成只含本次版本的 `deploy/`:根部放置 `deploy.sh`、`release.env`,版本目录包含前端包、后端镜像、完整迁移、迁移执行器、Compose、Nginx 配置和校验清单。
|
|
28
|
+
5. 全部校验成功后,向 `AGENTS.md` 的“发布记录”表写入版本号与构建时间。
|
|
29
|
+
6. 工程师将 `deploy/` 下全部内容人工上传到服务器指定目录;该目录即部署根目录。
|
|
30
|
+
7. 在服务器执行无版本参数的 `./deploy.sh start`。脚本从 `release.env` 读取版本,并在任何变更前校验文件、校验清单和服务器 `.env.production`。
|
|
31
|
+
8. 使用原生 `psql` 执行向前兼容迁移,在非活动颜色启动后端;健康检查和版本接口通过后切换 Nginx upstream,再原子切换前端 `current`。
|
|
32
|
+
9. 保留旧颜色和前端 release 作为上一版本;提供 `stop`、`restart`、`status` 和 `rollback <version>`。
|
|
33
|
+
10. blue 与 green 共同将服务器部署根目录 `./logs` 挂载到 `/app/logs`,不得设置 `LOG_DIR`、`HOST_LOG_DIR` 或创建实例日志子目录。
|
|
36
34
|
|
|
37
35
|
## 禁止事项
|
|
38
36
|
|
|
37
|
+
- 不得接受人工输入 build 版本号或 `start <version>` 旧语法。
|
|
38
|
+
- 不得以扫描 `deploy/` 目录代替 `AGENTS.md` 发布账本。
|
|
39
|
+
- 不得把 `.env`、`.env.production`、凭据或其他生产秘密打入 `deploy/`。
|
|
40
|
+
- 不得在缺少已验证 `.dockerignore` 的情况下用项目根目录构建后端镜像。
|
|
41
|
+
- 不得在脚本、Compose、Dockerfile 或 Nginx 中硬编码 URL、端口或 IP;全部由 `.env` 或服务器 `.env.production` 明确提供。
|
|
39
42
|
- 不得直接覆盖正在服务的前端 `dist`。
|
|
40
43
|
- 不得先停止旧后端容器再启动新容器。
|
|
41
44
|
- 不得以 PM2 或 systemd 作为后端主部署方案。
|
|
42
45
|
- 不得在无健康检查、版本标识或可演练回滚的情况下发布。
|
|
43
|
-
- 不得在 `deploy.sh` 中执行 SCP
|
|
46
|
+
- 不得在 `deploy.sh` 中执行 SCP、拉取代码、访问制品仓库或静默安装依赖。
|
|
44
47
|
- 不得把“保留旧 release 目录”等同于旧资源可访问;资源 URL 必须包含版本。
|
|
48
|
+
- 不得将日志挂载到项目根目录 `./logs` 之外,或为 blue、green 配置不同日志目录。
|
|
45
49
|
|
|
46
50
|
## Red Flags
|
|
47
51
|
|
|
52
|
+
- `build` 成功前修改 `AGENTS.md`,或失败后仍留下新的可上传目录。
|
|
53
|
+
- 发布包依赖目标项目工作树中未上传的迁移执行器、配置或脚本。
|
|
54
|
+
- `start` 依赖固定 `/srv/<project>`、调用者当前目录或人工版本参数。
|
|
48
55
|
- 发布脚本在新版本通过健康检查前修改 upstream。
|
|
49
56
|
- 前端目录被直接覆盖,或发布计划包含破坏性迁移。
|
|
57
|
+
- Compose 包含 `LOG_DIR`、`HOST_LOG_DIR`、实例日志子目录或非 `../../logs:/app/logs` 的日志挂载。
|
|
50
58
|
|
|
51
59
|
## Verification
|
|
52
60
|
|
|
@@ -54,8 +62,11 @@ description: 为无 CI/CD 的 Linux 生产环境生成本地发布门禁、人
|
|
|
54
62
|
- [ ] upstream 切换后新请求命中新版本,旧资源在保留窗口不返回 404。
|
|
55
63
|
- [ ] 回滚脚本在演练环境恢复旧版本。
|
|
56
64
|
- [ ] 数据库变更的兼容性已审查。
|
|
57
|
-
- [ ] `
|
|
58
|
-
- [ ]
|
|
65
|
+
- [ ] `build` 无版本参数,发布编号来自格式正确且唯一的 `AGENTS.md` 发布记录。
|
|
66
|
+
- [ ] `deploy/` 只含本次完整发布包且不含生产秘密。
|
|
67
|
+
- [ ] `start` 无版本参数,通过脚本物理位置识别服务器部署根目录。
|
|
68
|
+
- [ ] `deploy.sh` 不包含上传、拉取代码或安装依赖行为。
|
|
69
|
+
- [ ] blue 与 green 都将项目根目录 `./logs` 挂载到 `/app/logs`,且不存在其他日志目录。
|
|
59
70
|
|
|
60
71
|
任一步骤失败时保持或恢复旧 upstream,保留失败容器日志并报告失败环节;不得强制切流或删除旧版本掩盖问题。
|
|
61
72
|
|
|
@@ -63,6 +74,8 @@ description: 为无 CI/CD 的 Linux 生产环境生成本地发布门禁、人
|
|
|
63
74
|
|
|
64
75
|
| 合理化说法 | 事实 |
|
|
65
76
|
|---|---|
|
|
77
|
+
| “版本号手工输入更快” | 手工参数可能与上传包不一致,版本必须来自发布账本和 `release.env`。 |
|
|
78
|
+
| “迁移脚本服务器上已经有” | 发布包必须自包含,不能依赖未上传的项目工作树。 |
|
|
66
79
|
| “停几秒没关系” | 发布规范要求后端无先停后启的中断窗口。 |
|
|
67
80
|
| “覆盖 dist 最简单” | 旧页面会引用旧 hash 文件,覆盖会造成资源 404。 |
|
|
68
81
|
| “小字段改动无需兼容” | 滚动与回滚期间新旧版本会并存。 |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Enterprise Deployment Standard"
|
|
3
|
-
short_description: "
|
|
4
|
-
default_prompt: "Use $wdyy-deployment-standard to
|
|
3
|
+
short_description: "Build and start complete manual deployment packages"
|
|
4
|
+
default_prompt: "Use $wdyy-deployment-standard to build a versioned deploy directory and prepare its manual server start."
|
|
@@ -1,9 +1,131 @@
|
|
|
1
1
|
# Linux 部署规则
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
## 目标项目文件
|
|
4
|
+
|
|
5
|
+
按目标项目真实结构生成并维护以下受版本控制文件:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
deploy.sh
|
|
9
|
+
.dockerignore
|
|
10
|
+
src/backend/Dockerfile
|
|
11
|
+
scripts/apply-migrations.sh
|
|
12
|
+
scripts/deployment/docker-compose.blue-green.yml
|
|
13
|
+
scripts/deployment/nginx-site.conf
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- `deploy.sh` 同时提供本地 `build` 和服务器 `start`、`stop`、`restart`、`status`、`rollback`。
|
|
17
|
+
- Dockerfile 必须接收同一发布版本,使镜像标签和后端版本接口返回 `YYYYMMDD-NNN`。
|
|
18
|
+
- `.dockerignore` 必须排除 `.env`、`.env.*`、密钥、凭据、`deploy/`、`logs/`、`.git/`、`node_modules/`、`venv/` 和 `.pnpm-store/`;仅允许明确反向包含 `.env.example`。
|
|
19
|
+
- Compose 与 Nginx 源文件不得位于生成目录 `deploy/`,否则下一次 build 会删除部署源配置。
|
|
20
|
+
- `scripts/apply-migrations.sh` 必须来自数据库 Skill 的已确认迁移契约,且随每次发布复制到版本目录。
|
|
21
|
+
- 根据目标项目的 pnpm workspace 和构建输出调整模板变量;不得保留不存在的示例路径。
|
|
22
|
+
- 在项目 `.env` 提供 `PROJECT_NAME` 与 `BACKEND_CONTAINER_PORT` 供本地 build 使用;不得在脚本或 Dockerfile 中写默认端口。
|
|
23
|
+
|
|
24
|
+
## 开发与本地打包
|
|
25
|
+
|
|
26
|
+
1. 开发测试阶段执行 `pnpm dev`,不得由部署脚本启动或管理数据库。
|
|
27
|
+
2. 开发测试完成后,只执行无额外参数的 `./deploy.sh build`。
|
|
28
|
+
3. build 依次执行目标项目已确认的测试、lint、类型检查、`pnpm build`、版本化前端校验、Docker 镜像构建与检查、完整发布包校验。
|
|
29
|
+
4. 前端构建使用 `/releases/<version>/` 资源基路径,后端镜像标签和版本接口使用同一个 `<version>`。
|
|
30
|
+
5. 任一步失败必须返回非零状态并报告具体环节,不得跳过、降级或形成可上传结论。
|
|
31
|
+
|
|
32
|
+
## AGENTS.md 发布账本
|
|
33
|
+
|
|
34
|
+
`AGENTS.md` 是发布序号的唯一来源。发布记录使用且只使用以下两列:
|
|
35
|
+
|
|
36
|
+
```markdown
|
|
37
|
+
## 发布记录
|
|
38
|
+
|
|
39
|
+
| 版本号 | 构建时间 |
|
|
40
|
+
|---|---|
|
|
41
|
+
| 20260730-001 | 2026-07-30 09:30:00 +08:00 |
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
build 必须:
|
|
45
|
+
|
|
46
|
+
1. 要求项目根目录存在可读写的 `AGENTS.md`。
|
|
47
|
+
2. 要求最多存在一个标题完全等于 `## 发布记录` 的章节。
|
|
48
|
+
3. 严格验证表头、分隔行以及所有 `YYYYMMDD-NNN` 和构建时间记录;版本不得重复,序号必须为 `001` 至 `999`。
|
|
49
|
+
4. 读取本地当前日期,取当天最大序号加一;当天无记录时使用 `001`。
|
|
50
|
+
5. 在全部发布门禁和包校验成功后才原子写入版本号与构建时间。
|
|
51
|
+
6. 章节不存在时,在首次成功 build 后追加固定章节和表格。
|
|
52
|
+
7. 账本损坏、章节重复、版本重复或当天已到 `999` 时明确失败。
|
|
53
|
+
|
|
54
|
+
不得接受人工 build 版本参数,不得扫描 `deploy/` 推导序号。构建人员必须先同步版本控制中的发布记录;本规范不提供跨未同步工作区的中央编号服务。
|
|
55
|
+
|
|
56
|
+
## 本地 deploy 目录
|
|
57
|
+
|
|
58
|
+
每次 build 先在项目根目录外显路径之外生成临时包,全部校验通过后重新生成只含本次版本的 `deploy/`:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
deploy/
|
|
62
|
+
├── deploy.sh
|
|
63
|
+
├── release.env
|
|
64
|
+
└── YYYYMMDD-NNN/
|
|
65
|
+
├── frontend.tar.gz
|
|
66
|
+
├── backend-image.tar
|
|
67
|
+
├── manifest.sha256
|
|
68
|
+
├── database/migrations/
|
|
69
|
+
├── scripts/apply-migrations.sh
|
|
70
|
+
├── docker/docker-compose.blue-green.yml
|
|
71
|
+
└── nginx/site.conf
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `release.env` 只允许声明 `RELEASE_VERSION=YYYYMMDD-NNN`。
|
|
75
|
+
- `manifest.sha256` 覆盖版本目录中除清单本身外的全部文件。
|
|
76
|
+
- `deploy/` 只包含本次版本,不承担历史账本职责。
|
|
77
|
+
- `.env`、`.env.production`、访问令牌、私钥、证书、密码和其他生产秘密不得进入 `deploy/`。
|
|
78
|
+
- build 必须在 Docker 构建前验证项目根目录 `.dockerignore` 的固定秘密与生成目录排除项,防止秘密进入 `backend-image.tar`。
|
|
79
|
+
- 任一步失败必须清理本次临时输出,不得修改发布记录或留下半成品。
|
|
80
|
+
|
|
81
|
+
## 人工上传
|
|
82
|
+
|
|
83
|
+
工程师将本地 `deploy/` 下全部内容人工上传到服务器指定目录。该目录本身就是部署根目录,例如本地 `deploy/deploy.sh` 上传后对应 `<deploy-root>/deploy.sh`,不得在服务器额外嵌套一层固定 `deploy/`。
|
|
84
|
+
|
|
85
|
+
`deploy.sh` 不得执行 SCP、rsync、Git 拉取、制品仓库下载或依赖安装。生产 `.env.production` 由服务器单独保管,不随上传包传输。
|
|
86
|
+
|
|
87
|
+
服务器 `.env.production` 必须明确提供 `PROJECT_NAME`、`DATABASE_URL`、`BACKEND_BIND_IP`、`BACKEND_LISTEN_HOST`、`BACKEND_CONTAINER_PORT`、`BACKEND_CONTAINER_HEALTH_URL`、`BLUE_PORT`、`GREEN_PORT`、`BLUE_HEALTH_URL`、`GREEN_HEALTH_URL`、`BLUE_VERSION_URL`、`GREEN_VERSION_URL`、`BLUE_UPSTREAM`、`GREEN_UPSTREAM` 和 `BACKEND_PROXY_URL`。`BACKEND_PROXY_URL` 必须是仅包含合法 Nginx upstream 名称的 `http://<name>` 或 `https://<name>`。URL、端口和 IP 不得在脚本、Compose、Dockerfile 或 Nginx 模板中设置默认值。
|
|
88
|
+
|
|
89
|
+
## 服务器目录与 start
|
|
90
|
+
|
|
91
|
+
上传后服务器目录形态为:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
<deploy-root>/
|
|
95
|
+
├── deploy.sh
|
|
96
|
+
├── release.env
|
|
97
|
+
├── YYYYMMDD-NNN/
|
|
98
|
+
├── .env.production
|
|
99
|
+
├── logs/
|
|
100
|
+
├── releases/
|
|
101
|
+
├── state/
|
|
102
|
+
├── current -> releases/<active-version>
|
|
103
|
+
└── nginx/
|
|
104
|
+
├── site.conf
|
|
105
|
+
└── backend-active.conf
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
1. 在部署根目录执行无额外参数的 `./deploy.sh start`。
|
|
109
|
+
2. 脚本通过自身绝对路径确定部署根目录,不依赖调用者当前目录,不使用固定 `/srv/<project>`。
|
|
110
|
+
3. start 严格读取 `release.env`,校验版本格式、版本目录、必需文件和全部 SHA-256;传入版本参数必须失败。
|
|
111
|
+
4. `.env.production` 缺失或不可读时,在加载镜像、迁移和切流前失败。
|
|
112
|
+
5. 安全解压前端到不可变 `releases/<version>`;确认 `index.html` 与版本化资源路径后才允许切换 `current`。
|
|
113
|
+
6. 加载精确标签的后端镜像,使用版本包内的迁移执行器执行尚未执行的向前兼容迁移。
|
|
114
|
+
7. 在非活动颜色启动新容器;健康检查和版本接口返回目标版本后才切换 Nginx upstream,再原子切换前端 `current`。
|
|
115
|
+
8. 保留旧颜色和旧前端 release 到观测窗口结束,默认保留最近五个版本。
|
|
116
|
+
9. `rollback <version>` 只允许切回已保留、健康且数据库兼容的上一版本。
|
|
117
|
+
|
|
118
|
+
## 路径与日志
|
|
119
|
+
|
|
120
|
+
- 版本 Compose 位于 `<deploy-root>/<version>/docker`。
|
|
121
|
+
- blue 与 green 共同使用 `../../logs:/app/logs`,使宿主机部署根目录 `./logs` 对应容器 `/app/logs`。
|
|
122
|
+
- 不得设置 `LOG_DIR`、`HOST_LOG_DIR`,不得创建服务、实例、日期、颜色或其他日志子目录。
|
|
123
|
+
- Nginx 包内模板使用明确占位符;start 按脚本物理路径渲染到 `<deploy-root>/nginx/site.conf`。
|
|
124
|
+
- Nginx 主配置必须由工程师预先包含或链接该 site 文件,并授予脚本执行 `nginx -t` 与 reload 的权限。
|
|
125
|
+
|
|
126
|
+
## 失败与回滚
|
|
127
|
+
|
|
128
|
+
- 包校验、生产配置、解压、镜像、迁移、容器启动、健康检查、版本检查、Nginx 校验或 reload 任一步失败时,保持当前 upstream 和前端 `current`。
|
|
129
|
+
- Nginx 切换失败时恢复原 upstream 内容,再验证并 reload;必须明确报告失败。
|
|
130
|
+
- 不得强制切流、先停止旧容器、删除旧版本、静默安装工具或吞掉错误。
|
|
131
|
+
- `stop`、`restart`、`status` 和 `rollback` 必须从持久 active/previous 状态解析对应版本目录,不得依赖上传者重复输入当前版本。
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
3
5
|
|
|
4
|
-
const [deployPath, nginxPath, composePath] = process.argv.slice(2);
|
|
5
|
-
if (!deployPath || !nginxPath || !composePath) {
|
|
6
|
-
throw new Error('Pass deploy.sh,
|
|
6
|
+
const [deployPath, nginxPath, composePath, packageRoot] = process.argv.slice(2);
|
|
7
|
+
if (!deployPath || !nginxPath || !composePath || !packageRoot) {
|
|
8
|
+
throw new Error('Pass deploy.sh, Nginx config, compose file and generated deploy directory paths');
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
const [deploy, nginx, compose] = await Promise.all([
|
|
@@ -13,24 +15,51 @@ const [deploy, nginx, compose] = await Promise.all([
|
|
|
13
15
|
]);
|
|
14
16
|
|
|
15
17
|
const deployRequirements = [
|
|
18
|
+
'build)',
|
|
16
19
|
'start)',
|
|
17
20
|
'stop)',
|
|
18
21
|
'restart)',
|
|
19
22
|
'status)',
|
|
20
23
|
'rollback)',
|
|
21
|
-
'
|
|
24
|
+
'AGENTS.md',
|
|
25
|
+
'## 发布记录',
|
|
26
|
+
'| 版本号 | 构建时间 |',
|
|
27
|
+
'release.env',
|
|
28
|
+
'RELEASE_VERSION',
|
|
29
|
+
'SCRIPT_DIR',
|
|
30
|
+
'manifest.sha256',
|
|
31
|
+
'pnpm test',
|
|
32
|
+
'pnpm lint',
|
|
33
|
+
'pnpm typecheck',
|
|
34
|
+
'pnpm build',
|
|
35
|
+
'docker build',
|
|
36
|
+
'docker save',
|
|
37
|
+
'.dockerignore',
|
|
38
|
+
'database/migrations',
|
|
39
|
+
'scripts/apply-migrations.sh',
|
|
22
40
|
'HEALTH_URL',
|
|
23
41
|
'VERSION_URL',
|
|
24
|
-
'
|
|
42
|
+
'BLUE_HEALTH_URL',
|
|
43
|
+
'GREEN_HEALTH_URL',
|
|
44
|
+
'BLUE_VERSION_URL',
|
|
45
|
+
'GREEN_VERSION_URL',
|
|
46
|
+
'BLUE_UPSTREAM',
|
|
47
|
+
'GREEN_UPSTREAM',
|
|
48
|
+
'BACKEND_PROXY_URL',
|
|
25
49
|
];
|
|
26
|
-
const nginxRequirements = [
|
|
50
|
+
const nginxRequirements = [
|
|
51
|
+
'__DEPLOY_ROOT__',
|
|
52
|
+
'__BACKEND_PROXY_URL__',
|
|
53
|
+
'location /releases/',
|
|
54
|
+
'/current',
|
|
55
|
+
];
|
|
56
|
+
const projectRootLogMount = '../../logs:/app/logs';
|
|
27
57
|
const composeRequirements = [
|
|
28
58
|
'backend-blue:',
|
|
29
59
|
'backend-green:',
|
|
30
60
|
'INSTANCE_ID',
|
|
31
61
|
'blue',
|
|
32
62
|
'green',
|
|
33
|
-
'LOG_DIR',
|
|
34
63
|
];
|
|
35
64
|
|
|
36
65
|
const missing = [
|
|
@@ -41,8 +70,127 @@ const missing = [
|
|
|
41
70
|
if (missing.length) {
|
|
42
71
|
throw new Error(`Missing deployment requirements: ${missing.join(', ')}`);
|
|
43
72
|
}
|
|
44
|
-
|
|
45
|
-
|
|
73
|
+
|
|
74
|
+
if (!/start\)\s*\n\s*\[\[ "\$#" -eq 1 \]\] \|\| usage/.test(deploy)) {
|
|
75
|
+
throw new Error('start must reject version arguments and read release.env');
|
|
76
|
+
}
|
|
77
|
+
if (/start\)[\s\S]{0,160}version="\$2"/.test(deploy) || deploy.includes('INCOMING_DIR')) {
|
|
78
|
+
throw new Error('Legacy start <version> and incoming/<version> layouts are forbidden');
|
|
79
|
+
}
|
|
80
|
+
if (/(?:^|[\s"'])\/srv\//m.test(`${deploy}\n${nginx}\n${compose}`)) {
|
|
81
|
+
throw new Error('Deployment root must come from deploy.sh physical location, not a fixed /srv path');
|
|
82
|
+
}
|
|
83
|
+
if (/(?:127\.0\.0\.1|\b3001\b|\b3002\b)/.test(`${deploy}\n${compose}`)) {
|
|
84
|
+
throw new Error('Deployment URLs, ports and IP addresses must come from environment files');
|
|
85
|
+
}
|
|
86
|
+
if (/proxy_pass\s+https?:\/\//.test(nginx)) {
|
|
87
|
+
throw new Error('Nginx proxy URL must come from BACKEND_PROXY_URL in .env.production');
|
|
88
|
+
}
|
|
89
|
+
if (/\b(?:scp|rsync)\b/.test(deploy)) {
|
|
90
|
+
throw new Error('deploy.sh must not upload artifacts; upload is a manual engineer action');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const forbiddenLogConfiguration = /\b(?:LOG_DIR|HOST_LOG_DIR)\b|backend\/(?:blue|green)|\/var\/log\/application/;
|
|
94
|
+
if (forbiddenLogConfiguration.test(compose)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
'Logs must use the shared project-root ../../logs:/app/logs mount without configurable or per-instance directories',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const logTargetMounts = compose.match(/^[ \t]*-[ \t]*[^#\n]+:\/app\/logs[ \t]*$/gm) ?? [];
|
|
100
|
+
if (
|
|
101
|
+
logTargetMounts.length !== 2
|
|
102
|
+
|| logTargetMounts.some((mount) => !mount.includes(projectRootLogMount))
|
|
103
|
+
) {
|
|
104
|
+
throw new Error('Blue and green must both mount project-root ../../logs directly to /app/logs');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const releaseEnvPath = join(packageRoot, 'release.env');
|
|
108
|
+
const releaseEnv = await readFile(releaseEnvPath, 'utf8');
|
|
109
|
+
const releaseMatch = releaseEnv.match(/^RELEASE_VERSION=(\d{8}-\d{3})\n$/);
|
|
110
|
+
if (!releaseMatch) {
|
|
111
|
+
throw new Error('release.env must contain exactly one RELEASE_VERSION=YYYYMMDD-NNN line');
|
|
112
|
+
}
|
|
113
|
+
const version = releaseMatch[1];
|
|
114
|
+
const rootEntries = (await readdir(packageRoot)).sort();
|
|
115
|
+
const expectedRootEntries = [version, 'deploy.sh', 'release.env'].sort();
|
|
116
|
+
if (JSON.stringify(rootEntries) !== JSON.stringify(expectedRootEntries)) {
|
|
117
|
+
throw new Error(`Generated deploy directory must contain only deploy.sh, release.env and ${version}`);
|
|
118
|
+
}
|
|
119
|
+
if (resolve(deployPath) !== resolve(packageRoot, 'deploy.sh')) {
|
|
120
|
+
throw new Error('Validator deploy.sh path must be the generated deploy directory root script');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const versionRoot = join(packageRoot, version);
|
|
124
|
+
const requiredPaths = [
|
|
125
|
+
'frontend.tar.gz',
|
|
126
|
+
'backend-image.tar',
|
|
127
|
+
'manifest.sha256',
|
|
128
|
+
'database/migrations',
|
|
129
|
+
'scripts/apply-migrations.sh',
|
|
130
|
+
'docker/docker-compose.blue-green.yml',
|
|
131
|
+
'nginx/site.conf',
|
|
132
|
+
];
|
|
133
|
+
for (const requiredPath of requiredPaths) {
|
|
134
|
+
try {
|
|
135
|
+
await stat(join(versionRoot, requiredPath));
|
|
136
|
+
} catch {
|
|
137
|
+
throw new Error(`Missing generated release package path: ${requiredPath}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const runner = await stat(join(versionRoot, 'scripts/apply-migrations.sh'));
|
|
142
|
+
if (!runner.isFile() || (runner.mode & 0o111) === 0) {
|
|
143
|
+
throw new Error('Generated migration runner must be an executable file');
|
|
144
|
+
}
|
|
145
|
+
const migrationEntries = await readdir(join(versionRoot, 'database/migrations'));
|
|
146
|
+
if (!migrationEntries.some((entry) => /^V\d{3}__[a-z0-9]+(?:_[a-z0-9]+)*\.sql$/.test(entry))) {
|
|
147
|
+
throw new Error('Generated release package must include versioned database migrations');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function listFiles(root, directory = root) {
|
|
151
|
+
const result = [];
|
|
152
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
153
|
+
const path = join(directory, entry.name);
|
|
154
|
+
if (entry.isSymbolicLink()) {
|
|
155
|
+
throw new Error(`Generated release package must not contain symbolic links: ${relative(root, path)}`);
|
|
156
|
+
}
|
|
157
|
+
if (entry.isDirectory()) result.push(...await listFiles(root, path));
|
|
158
|
+
else if (entry.isFile()) result.push(relative(root, path).split(sep).join('/'));
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const versionFiles = (await listFiles(versionRoot)).sort();
|
|
164
|
+
for (const path of versionFiles) {
|
|
165
|
+
const name = basename(path);
|
|
166
|
+
if (
|
|
167
|
+
name === '.env'
|
|
168
|
+
|| name.startsWith('.env.')
|
|
169
|
+
|| /\.(?:pem|key)$/.test(name)
|
|
170
|
+
|| /^(?:id_rsa|id_ed25519|credentials(?:\..*)?)$/.test(name)
|
|
171
|
+
) {
|
|
172
|
+
throw new Error(`Production secret file must not be packaged: ${path}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const manifestPath = join(versionRoot, 'manifest.sha256');
|
|
177
|
+
const manifest = await readFile(manifestPath, 'utf8');
|
|
178
|
+
const entries = new Map();
|
|
179
|
+
for (const line of manifest.trimEnd().split('\n')) {
|
|
180
|
+
const match = line.match(/^([0-9a-f]{64}) ([^/].*)$/);
|
|
181
|
+
if (!match || match[2].split('/').includes('..')) {
|
|
182
|
+
throw new Error(`Invalid manifest entry: ${line}`);
|
|
183
|
+
}
|
|
184
|
+
if (entries.has(match[2])) throw new Error(`Duplicate manifest entry: ${match[2]}`);
|
|
185
|
+
entries.set(match[2], match[1]);
|
|
186
|
+
}
|
|
187
|
+
const expectedManifestFiles = versionFiles.filter((path) => path !== 'manifest.sha256');
|
|
188
|
+
if (JSON.stringify([...entries.keys()].sort()) !== JSON.stringify(expectedManifestFiles)) {
|
|
189
|
+
throw new Error('manifest.sha256 must cover every generated release file exactly once');
|
|
190
|
+
}
|
|
191
|
+
for (const [path, expected] of entries) {
|
|
192
|
+
const actual = createHash('sha256').update(await readFile(join(versionRoot, path))).digest('hex');
|
|
193
|
+
if (actual !== expected) throw new Error(`Checksum mismatch: ${path}`);
|
|
46
194
|
}
|
|
47
195
|
|
|
48
|
-
process.stdout.write(
|
|
196
|
+
process.stdout.write(`valid deployment package ${version}\n`);
|