@wdyy/skills 0.1.9 → 0.1.11

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  {
9
9
  "name": "wdyy-deployment-standard",
10
- "description": "基于标准五段根 .env,为无 CI/CD 的多项目 Linux 服务器生成确定性的前后端双 Docker 发布配置、本地 deploy.sh build 完整打包、人工上传及按独立端口执行蓝绿启动、替换、重启、停止、删除和回滚。Use when 规划、实现、审查或执行 `/xxzx/home/项目目录` 下的手工生产部署时。",
10
+ "description": "基于标准五段根 .env,为无 CI/CD 项目生成确定性的前后端双 Docker 发布配置、本地 deploy.sh build 完整打包、人工上传及按独立端口执行蓝绿启动、替换、重启、停止、删除和回滚;部署根由脚本位置确定且不处理数据库迁移。Use when 规划、实现、审查或执行手工生产部署时。",
11
11
  "files": ["SKILL.md", "agents/openai.yaml", "reference/linux-deployment-rules.md", "scripts/generate-deployment-files.mjs", "scripts/generate-deployment-files.test.mjs", "scripts/validate-deployment-package.mjs", "scripts/validate-deployment-package.test.mjs", "templates/Dockerfile.template", "templates/frontend.Dockerfile.template", "templates/frontend-container.conf.template", "templates/env.example.template", "templates/deploy.sh.template", "templates/docker-compose.blue-green.yml", "templates/dockerignore.template", "templates/nginx-upstream.template.conf"]
12
12
  },
13
13
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wdyy-deployment-standard
3
- description: 基于标准五段根 .env,为无 CI/CD 的多项目 Linux 服务器生成确定性的前后端双 Docker 发布配置、本地 deploy.sh build 完整打包、人工上传及按独立端口执行蓝绿启动、替换、重启、停止、删除和回滚。Use when 规划、实现、审查或执行 `/xxzx/home/项目目录` 下的手工生产部署时。
3
+ description: 基于标准五段根 .env,为无 CI/CD 项目生成确定性的前后端双 Docker 发布配置、本地 deploy.sh build 完整打包、人工上传及按独立端口执行蓝绿启动、替换、重启、停止、删除和回滚;部署根由脚本位置确定且不处理数据库迁移。Use when 规划、实现、审查或执行手工生产部署时。
4
4
  ---
5
5
 
6
6
  # 企业部署规范
@@ -19,15 +19,15 @@ description: 基于标准五段根 .env,为无 CI/CD 的多项目 Linux 服务
19
19
 
20
20
  ## 固定契约
21
21
 
22
- - 项目名只允许小写字母、数字和下划线,并与服务器目录 `/xxzx/home/<项目名称>` 对应。
22
+ - 项目名只允许小写字母、数字和下划线;服务器部署根由上传后的 `deploy.sh` 物理路径确定。
23
23
  - 两个镜像必须精确命名为 `<项目名称>_frontend:<YYYYMMDD-NNN>` 和 `<项目名称>_backend:<YYYYMMDD-NNN>`。
24
24
  - Compose 必须包含 frontend-blue、backend-blue、frontend-green、backend-green;同一颜色的前后端作为一个切流单元。
25
25
  - 宿主机 Nginx 只监听根 `.env` 的 `PROJECT_HTTP_PORT`,`/api/` 代理活动后端,其他请求代理活动前端。
26
26
  - 所有容器只把项目根 `./logs` 挂载到 `/app/logs`;日志文件直接位于 `logs/`,不得创建服务、颜色或日期子目录。
27
27
  - 根 `.env` 原样复制到 `deploy/.env` 并人工上传,但必须被 `.dockerignore` 排除,且不得进入版本目录或任一镜像。
28
- - `FRONTEND_BASE_IMAGE` 与 `BACKEND_BASE_IMAGE` 使用非空 Docker 镜像引用;可直接使用本地带标签镜像。相同标签在不同时间重新构建时可能对应不同基础镜像内容,但已导出的项目成品镜像仍按版本确定部署。
28
+ - `.env.example` 是环境主模板,内置 `FRONTEND_BASE_IMAGE=nginx:stable` 与 `BACKEND_BASE_IMAGE=node:24-alpine3.24`;`.env` 可在生成后显式覆盖。相同标签在不同时间重新构建时可能对应不同基础镜像内容,但已导出的项目成品镜像仍按版本确定部署。
29
29
  - 环境文件固定按“前端、后端、数据库、API、部署”排列;部署生成器保留前四段和已有值,只扩充部署段。
30
- - `DATABASE_MIGRATION_MODE=manual` 时只打包迁移与执行脚本;`none` 时不要求数据库文件。任何部署命令都不得自动访问数据库。
30
+ - 发布链路不接受数据库迁移参数,也不打包、校验或执行迁移文件。
31
31
  - 相同输入生成的 deploy.sh、Compose、Nginx 配置必须字节一致;版本指针、镜像归档和 manifest 可随发布变化。
32
32
 
33
33
  ## 工作流
@@ -35,18 +35,16 @@ description: 基于标准五段根 .env,为无 CI/CD 的多项目 Linux 服务
35
35
  1. 使用 `pnpm dev` 完成开发测试。
36
36
  2. 执行不带额外参数的 `./deploy.sh build`;从 `AGENTS.md` 唯一“发布记录”表生成 `YYYYMMDD-NNN`。
37
37
  3. 依次通过测试、lint、类型检查、构建、双镜像语义检查、包校验和配置确定性检查;成功后才替换 `deploy/` 并写发布记录。
38
- 4. 人工把 `deploy/` 全部内容上传到 `.env` 声明的 `<SERVER_PROJECTS_ROOT>/<PROJECT_NAME>`;生产值不得通过命令行传入。
39
- 5. 数据库模式为 manual 时,由工程师单独执行版本包中的迁移脚本并确认结果。
40
- 6. 首次部署执行 `./deploy.sh start`;后续新版本执行 `./deploy.sh replace`。
41
- 7. 通过 `restart`、`stop`、`status`、`rollback <version>` 管理当前项目;`remove` 只删除本项目容器、网络及两个精确镜像仓库。
38
+ 4. 人工把 `deploy/` 全部内容上传到目标部署目录;生产值不得通过命令行传入。
39
+ 5. 首次部署执行 `./deploy.sh start`;后续新版本执行 `./deploy.sh replace`。
40
+ 6. 通过 `restart`、`stop`、`status`、`rollback <version>` 管理当前项目;`remove` 只删除本项目容器、网络及两个精确镜像仓库。
42
41
 
43
- ## Nginx 多项目隔离
42
+ ## Nginx 当前项目边界
44
43
 
45
- - 运维只在服务器初次接入时让共享 Nginx include `/xxzx/home/*/nginx/site.conf`;部署脚本不得编辑该全局配置。
46
44
  - 每个项目只写自身 `nginx/site.conf` 和 `nginx/active-upstreams.conf`。
47
45
  - upstream 名称由严格项目名确定,不接受自由文本配置。
48
- - 在共享文件锁内检查其他项目配置和运行中容器的实际发布端口、安装候选配置、运行进程级 `nginx -t` 和 reload;失败时恢复本项目旧文件。
49
- - 其他项目配置错误可以阻止本次 reload,但不得强制跳过检查或改写其他项目文件。
46
+ - 安装候选配置后运行 `nginx -t` 和 reload;失败时恢复本项目旧文件。
47
+ - 不扫描、校验或修改其他项目配置、端口或 Nginx include。
50
48
 
51
49
  ## 禁止事项
52
50
 
@@ -66,7 +64,7 @@ description: 基于标准五段根 .env,为无 CI/CD 的多项目 Linux 服务
66
64
  - [ ] `.env` 与 `.env.example` 的五段、变量顺序一致,部署段键相同且无重复 URL/端口配置。
67
65
  - [ ] 外部项目端口唯一;Nginx 一次同时切换前后端,失败恢复旧流量和状态。
68
66
  - [ ] start、replace、restart、stop、remove、status、rollback 行为与资源范围均通过测试。
69
- - [ ] manual/none 两种数据库模式均通过测试,部署脚本不存在数据库执行路径。
67
+ - [ ] 不存在数据库迁移参数、迁移包内容或数据库执行路径。
70
68
  - [ ] 连续两次构建的配置文件一致,失败不写发布记录、不暴露半成品。
71
69
  - [ ] `deploy/` 含根 `.env`,但版本目录、Docker 上下文和镜像不含秘密文件。
72
70
  - [ ] 替换或删除容器后,项目根 `logs/` 内容仍保留。
@@ -33,7 +33,6 @@ DB_PASSWORD=
33
33
  DB_NAME=
34
34
  DB_SCHEMA=
35
35
  PROJECT_NAME=example_project
36
- SERVER_PROJECTS_ROOT=/xxzx/home
37
36
  PROJECT_HTTP_PORT=9099
38
37
  DOCKER_BIND_IP=127.0.0.1
39
38
  FRONTEND_BLUE_PORT=19091
@@ -42,7 +41,6 @@ BACKEND_BLUE_PORT=19093
42
41
  BACKEND_GREEN_PORT=19094
43
42
  FRONTEND_BASE_IMAGE=nginx:stable
44
43
  BACKEND_BASE_IMAGE=node:24-alpine3.24
45
- DATABASE_MIGRATION_MODE=none
46
44
  ```
47
45
 
48
46
  `FRONTEND_URL`、`FRONTEND_PORT`、`BACKEND_URL`、`BACKEND_PORT` 是应用监听的唯一来源;容器、蓝绿和项目入口的健康/版本 URL 必须由对应主机、端口与固定路径确定性构造。不得增加 `FRONTEND_CONTAINER_PORT`、`BACKEND_CONTAINER_PORT`、`BACKEND_LISTEN_HOST`、`*_HEALTH_URL` 或 `*_VERSION_URL`。通配监听地址 `0.0.0.0` 仅在主动本机探测时转换为 `127.0.0.1`。
@@ -51,7 +49,7 @@ DATABASE_MIGRATION_MODE=none
51
49
 
52
50
  `.env` 权限必须为 `0600` 或只读的 `0400`。部署脚本只把明确的部署键载入自身环境;数据库密码、JWT 等其他应用键保留在文件中供 Compose 注入容器,不得改变脚本的 `PATH`、Shell 选项或命令行为。
53
51
 
54
- 可选路径键为 `FRONTEND_DOCKERFILE`、`BACKEND_DOCKERFILE`、`MIGRATIONS_SOURCE`、`MIGRATION_RUNNER_SOURCE`、`DEPLOY_CONFIG_DIR`;未设置时只能使用与目标项目真实结构一致且已验证的标准路径。
52
+ 可选路径键为 `FRONTEND_DOCKERFILE`、`BACKEND_DOCKERFILE`、`DEPLOY_CONFIG_DIR`;未设置时只能使用与目标项目真实结构一致且已验证的标准路径。
55
53
 
56
54
  ## Docker 与日志
57
55
 
@@ -88,16 +86,13 @@ deploy/
88
86
  ├── manifest.sha256
89
87
  ├── docker/docker-compose.blue-green.yml
90
88
  ├── nginx/site.conf
91
- └── database/ # 仅 manual 模式
92
- ├── migrations/
93
- └── apply-migrations.sh
94
89
  ```
95
90
 
96
91
  `release.env` 只包含当前版本。manifest 覆盖版本目录内除自身外的全部普通文件,禁止符号链接、硬链接秘密、绝对路径和 `..`。根 `.env` 可上传,但版本目录与镜像不得包含它。
97
92
 
98
93
  ## 服务器部署和 Nginx
99
94
 
100
- 上传目标必须是 `${SERVER_PROJECTS_ROOT}/${PROJECT_NAME}`,生产值为 `/xxzx/home/<项目名称>`。脚本通过自身物理路径验证该关系,不依赖调用者当前目录。
95
+ 上传目标由工程师确定;脚本通过自身物理路径确定部署根,不依赖调用者当前目录。
101
96
 
102
97
  共享 Nginx 主配置由运维预先 include `/xxzx/home/*/nginx/site.conf`。项目脚本不得写 `/etc/nginx` 或其他项目,只维护:
103
98
 
@@ -122,10 +117,7 @@ site 监听 `PROJECT_HTTP_PORT`,并带项目名、外部端口及四个 Docker
122
117
 
123
118
  ## 数据库
124
119
 
125
- - `DATABASE_MIGRATION_MODE=none`:版本包不要求数据库目录。
126
- - `DATABASE_MIGRATION_MODE=manual`:必须打包完整迁移目录和可执行脚本。
127
- - deploy.sh 的任何服务器命令都不得调用迁移脚本、psql 或数据库客户端。
128
- - build 完成和服务器 start/replace 必须明确提示 manual 操作尚需工程师单独执行,不得声称已迁移。
120
+ 部署 Skill 不接受数据库迁移参数,也不打包、校验、提示或执行迁移文件。
129
121
 
130
122
  ## 失败边界
131
123
 
@@ -22,9 +22,9 @@ const baseKeys = new Map([
22
22
  ['数据库', ['DB_URL', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME', 'DB_SCHEMA']],
23
23
  ]);
24
24
  const deploymentKeys = [
25
- 'PROJECT_NAME', 'SERVER_PROJECTS_ROOT', 'PROJECT_HTTP_PORT', 'DOCKER_BIND_IP',
25
+ 'PROJECT_NAME', 'PROJECT_HTTP_PORT', 'DOCKER_BIND_IP',
26
26
  'FRONTEND_BLUE_PORT', 'FRONTEND_GREEN_PORT', 'BACKEND_BLUE_PORT', 'BACKEND_GREEN_PORT',
27
- 'FRONTEND_BASE_IMAGE', 'BACKEND_BASE_IMAGE', 'DATABASE_MIGRATION_MODE',
27
+ 'FRONTEND_BASE_IMAGE', 'BACKEND_BASE_IMAGE',
28
28
  ];
29
29
 
30
30
  function parseEnvironmentStructure(text, relativeTarget, requireExampleDefaults, exampleValues) {
@@ -73,8 +73,14 @@ function parseEnvironmentStructure(text, relativeTarget, requireExampleDefaults,
73
73
  if (actualDeploymentKeys.length !== 0 && actualDeploymentKeys.join('|') !== deploymentKeys.join('|')) {
74
74
  throw new Error(`${relativeTarget} # 部署 keys must be empty or exactly: ${deploymentKeys.join(', ')}`);
75
75
  }
76
- if (requireExampleDefaults && sections.get('部署').some(({ value }) => value !== '')) {
77
- throw new Error(`${relativeTarget} deployment example values must be empty`);
76
+ if (requireExampleDefaults && sections.get('部署').some(({ key, value }) => !['FRONTEND_BASE_IMAGE', 'BACKEND_BASE_IMAGE'].includes(key) && value !== '')) {
77
+ throw new Error(`${relativeTarget} deployment example values must be empty except base-image defaults`);
78
+ }
79
+ if (requireExampleDefaults) {
80
+ for (const key of ['FRONTEND_BASE_IMAGE', 'BACKEND_BASE_IMAGE']) {
81
+ const entry = sections.get('部署').find((item) => item.key === key);
82
+ if (entry && entry.value !== exampleValues.get(key)) throw new Error(`${relativeTarget} must keep the standard default for ${key}`);
83
+ }
78
84
  }
79
85
  return {
80
86
  extended: actualDeploymentKeys.length === deploymentKeys.length,
@@ -83,7 +89,7 @@ function parseEnvironmentStructure(text, relativeTarget, requireExampleDefaults,
83
89
  }
84
90
 
85
91
  function extendDeploymentSection(text) {
86
- return `${text.trimEnd()}\n${deploymentKeys.map((key) => `${key}=`).join('\n')}\n`;
92
+ return `${text.trimEnd()}\n${deploymentKeys.map((key) => `${key}=${['FRONTEND_BASE_IMAGE', 'BACKEND_BASE_IMAGE'].includes(key) ? exampleValues.get(key) : ''}`).join('\n')}\n`;
87
93
  }
88
94
 
89
95
  const args = process.argv.slice(2);
@@ -38,7 +38,9 @@ test('确定性生成全部标准文件并通过 check', async () => {
38
38
  const environment = await readFile(join(root, '.env'), 'utf8');
39
39
  const example = await readFile(join(root, '.env.example'), 'utf8');
40
40
  assert.match(environment, /^# 前端\nFRONTEND_URL=0\.0\.0\.0\nFRONTEND_PORT=5173$/m);
41
- assert.match(environment, /^# 部署\nPROJECT_NAME=\nSERVER_PROJECTS_ROOT=/m);
41
+ assert.match(environment, /^# 部署\nPROJECT_NAME=\nPROJECT_HTTP_PORT=/m);
42
+ assert.match(environment, /^FRONTEND_BASE_IMAGE=nginx:stable$/m);
43
+ assert.match(environment, /^BACKEND_BASE_IMAGE=node:24-alpine3\.24$/m);
42
44
  assert.equal(example, environment);
43
45
  if (process.platform !== 'win32') assert.equal((await stat(join(root, '.env'))).mode & 0o777, 0o600);
44
46
  });
@@ -61,11 +61,8 @@ const requiredDeploySemantics = [
61
61
  /docker save -o "\$version_root\/backend-image\.tar"/,
62
62
  /load_dotenv "\$DEPLOY_ROOT\/\.env"/,
63
63
  /endpoint_url "\$DOCKER_BIND_IP"/,
64
- /server deploy root must be exactly/,
65
- /assert_project_ports_isolated/,
66
64
  /flock -x 9/,
67
65
  /Nginx reload failed; old project configuration restored/,
68
- /database migration was not executed/,
69
66
  /docker ps -aq --filter "label=com\.docker\.compose\.project=\$PROJECT_NAME"/,
70
67
  /\$1 == frontend \|\| \$1 == backend/,
71
68
  ];
@@ -95,11 +92,11 @@ for (const key of ['FRONTEND_PORT', 'BACKEND_PORT', 'FRONTEND_URL', 'BACKEND_URL
95
92
  if (!compose.includes(`\${${key}}`)) throw new Error(`Compose must use ${key}`);
96
93
  }
97
94
 
98
- for (const placeholder of ['__PROJECT_NAME__', '__PROJECT_HTTP_PORT__', '__DOCKER_HOST_PORTS__', '__DEPLOY_ROOT__', '__FRONTEND_UPSTREAM_NAME__', '__BACKEND_UPSTREAM_NAME__']) {
95
+ for (const placeholder of ['__PROJECT_NAME__', '__PROJECT_HTTP_PORT__', '__DEPLOY_ROOT__', '__FRONTEND_UPSTREAM_NAME__', '__BACKEND_UPSTREAM_NAME__']) {
99
96
  if (!nginx.includes(placeholder)) throw new Error(`Nginx template is missing ${placeholder}`);
100
97
  }
101
- if (!/^# wdyy-project: __PROJECT_NAME__$/m.test(nginx) || !/^# wdyy-port: __PROJECT_HTTP_PORT__$/m.test(nginx) || !/^# wdyy-docker-ports: __DOCKER_HOST_PORTS__$/m.test(nginx)) {
102
- throw new Error('Nginx template must expose deterministic project and all port markers');
98
+ if (!/^# wdyy-project: __PROJECT_NAME__$/m.test(nginx) || !/^# wdyy-port: __PROJECT_HTTP_PORT__$/m.test(nginx)) {
99
+ throw new Error('Nginx template must expose deterministic project and port markers');
103
100
  }
104
101
  if (/listen\s+\d+/.test(nginx) || /proxy_pass\s+https?:\/\/(?!__)/.test(nginx)) throw new Error('Nginx URL or port is hardcoded');
105
102
 
@@ -114,23 +111,20 @@ const sectionNames = [...rootEnvText.matchAll(/^# (前端|后端|数据库|API|
114
111
  if (sectionNames.join('|') !== '前端|后端|数据库|API|部署') throw new Error('Root .env must use the five standard sections in order');
115
112
  const requiredEnv = [
116
113
  'FRONTEND_URL', 'FRONTEND_PORT', 'BACKEND_URL', 'BACKEND_PORT', 'DB_PORT',
117
- 'PROJECT_NAME', 'SERVER_PROJECTS_ROOT', 'PROJECT_HTTP_PORT', 'DOCKER_BIND_IP',
114
+ 'PROJECT_NAME', 'PROJECT_HTTP_PORT', 'DOCKER_BIND_IP',
118
115
  'FRONTEND_BLUE_PORT', 'FRONTEND_GREEN_PORT', 'BACKEND_BLUE_PORT', 'BACKEND_GREEN_PORT',
119
- 'FRONTEND_BASE_IMAGE', 'BACKEND_BASE_IMAGE', 'DATABASE_MIGRATION_MODE',
116
+ 'FRONTEND_BASE_IMAGE', 'BACKEND_BASE_IMAGE',
120
117
  ];
121
118
  for (const key of requiredEnv) if (!env.get(key)) throw new Error(`Root .env is missing ${key}`);
122
119
  for (const key of env.keys()) {
123
- if (['FRONTEND_CONTAINER_PORT', 'BACKEND_CONTAINER_PORT', 'BACKEND_LISTEN_HOST'].includes(key) || /_(?:HEALTH|VERSION)_URL$/.test(key)) {
120
+ if (['FRONTEND_CONTAINER_PORT', 'BACKEND_CONTAINER_PORT', 'BACKEND_LISTEN_HOST', 'DATABASE_MIGRATION_MODE', 'MIGRATIONS_SOURCE', 'MIGRATION_RUNNER_SOURCE'].includes(key) || /_(?:HEALTH|VERSION)_URL$/.test(key)) {
124
121
  throw new Error(`Root .env contains legacy duplicate configuration: ${key}`);
125
122
  }
126
123
  }
127
124
  if (!/^[a-z][a-z0-9_]*$/.test(env.get('PROJECT_NAME'))) throw new Error('Invalid PROJECT_NAME');
128
- if (!/^\/.+[^/]$/.test(env.get('SERVER_PROJECTS_ROOT'))) throw new Error('Invalid SERVER_PROJECTS_ROOT');
129
125
  for (const key of ['FRONTEND_URL', 'BACKEND_URL', 'DOCKER_BIND_IP']) {
130
126
  if (!/^[A-Za-z0-9:.%-]+$/.test(env.get(key))) throw new Error(`${key} contains unsafe characters`);
131
127
  }
132
- const migrationMode = env.get('DATABASE_MIGRATION_MODE');
133
- if (!['manual', 'none'].includes(migrationMode)) throw new Error('DATABASE_MIGRATION_MODE must be manual or none');
134
128
  if (env.has('LOG_DIR') || env.has('HOST_LOG_DIR')) throw new Error('Root .env must not configure log directories');
135
129
  const allPortKeys = ['FRONTEND_PORT', 'BACKEND_PORT', 'DB_PORT', 'PROJECT_HTTP_PORT', 'FRONTEND_BLUE_PORT', 'FRONTEND_GREEN_PORT', 'BACKEND_BLUE_PORT', 'BACKEND_GREEN_PORT'];
136
130
  for (const key of allPortKeys) {
@@ -159,14 +153,7 @@ for (const path of requiredPaths) {
159
153
  const info = await stat(join(versionRoot, path));
160
154
  if (!info.isFile()) throw new Error(`Required release path is not a file: ${path}`);
161
155
  }
162
- if (migrationMode === 'manual') {
163
- const runner = await stat(join(versionRoot, 'database/apply-migrations.sh'));
164
- if (!runner.isFile() || (runner.mode & 0o111) === 0) throw new Error('Manual migration runner must be executable');
165
- const migrations = await readdir(join(versionRoot, 'database/migrations'));
166
- if (!migrations.some((item) => /^V\d{3}__[a-z0-9]+(?:_[a-z0-9]+)*\.sql$/.test(item))) throw new Error('Manual migration files are missing');
167
- } else {
168
- await assertMissing(join(versionRoot, 'database'), 'none mode must not package database files');
169
- }
156
+ await assertMissing(join(versionRoot, 'database'), 'Release package must not contain database migration files');
170
157
 
171
158
  const versionFiles = (await listFiles(versionRoot)).sort();
172
159
  for (const path of versionFiles) {
@@ -32,7 +32,7 @@ async function temporary(prefix) {
32
32
  return path;
33
33
  }
34
34
 
35
- function envText(serverProjectsRoot, mode = 'none', extra = '') {
35
+ function envText(extra = '') {
36
36
  return `# 前端
37
37
  FRONTEND_URL=0.0.0.0
38
38
  FRONTEND_PORT=5173
@@ -53,7 +53,6 @@ DB_SCHEMA=
53
53
 
54
54
  # 部署
55
55
  PROJECT_NAME=fixture
56
- SERVER_PROJECTS_ROOT=${serverProjectsRoot}
57
56
  PROJECT_HTTP_PORT=9099
58
57
  DOCKER_BIND_IP=127.0.0.1
59
58
  FRONTEND_BLUE_PORT=19091
@@ -62,11 +61,10 @@ BACKEND_BLUE_PORT=19093
62
61
  BACKEND_GREEN_PORT=19094
63
62
  FRONTEND_BASE_IMAGE=nginx:stable
64
63
  BACKEND_BASE_IMAGE=node:24-alpine3.24
65
- DATABASE_MIGRATION_MODE=${mode}
66
64
  ${extra}`;
67
65
  }
68
66
 
69
- async function createFixture({ mode = 'none', agents = '# Target project\n', extraEnv = '' } = {}) {
67
+ async function createFixture({ agents = '# Target project\n', extraEnv = '' } = {}) {
70
68
  const root = await temporary('wdyy-deploy-');
71
69
  const fakeBin = join(root, 'fake-bin');
72
70
  const serverProjectsRoot = join(root, 'servers');
@@ -87,19 +85,12 @@ async function createFixture({ mode = 'none', agents = '# Target project\n', ext
87
85
  copyFile(frontendNginxTemplate, join(root, 'scripts/deployment/frontend-container.conf.template')),
88
86
  copyFile(dockerignoreTemplate, join(root, '.dockerignore')),
89
87
  writeFile(join(root, 'AGENTS.md'), agents),
90
- writeFile(join(root, '.env'), envText(serverProjectsRoot, mode, extraEnv)),
88
+ writeFile(join(root, '.env'), envText(extraEnv)),
91
89
  writeFile(commandLog, ''),
92
90
  ]);
93
91
  await chmod(join(root, 'deploy.sh'), 0o755);
94
92
  await chmod(join(root, '.env'), 0o600);
95
93
 
96
- if (mode === 'manual') {
97
- await mkdir(join(root, 'database/migrations'), { recursive: true });
98
- await writeFile(join(root, 'database/migrations/V001__baseline.sql'), 'SELECT 1;\n');
99
- await writeFile(join(root, 'scripts/apply-migrations.sh'), '#!/usr/bin/env bash\nset -euo pipefail\n');
100
- await chmod(join(root, 'scripts/apply-migrations.sh'), 0o755);
101
- }
102
-
103
94
  await writeExecutable(join(fakeBin, 'pnpm'), `#!/usr/bin/env bash
104
95
  set -euo pipefail
105
96
  printf 'pnpm %s\n' "$*" >> "$COMMAND_LOG"
@@ -175,7 +166,7 @@ if [[ "\${FAIL_STATE_INSTALL:-0}" == 1 && "$target" == */state/active.env ]]; th
175
166
  exec /usr/bin/install "$@"
176
167
  `);
177
168
 
178
- return { root, fakeBin, serverProjectsRoot, commandLog, mode };
169
+ return { root, fakeBin, serverProjectsRoot, commandLog };
179
170
  }
180
171
 
181
172
  async function writeExecutable(path, content) {
@@ -251,19 +242,12 @@ test('build 生成根 .env、双镜像和四服务完整包', async () => {
251
242
  assert.doesNotMatch(await readFile(join(fixture.root, `deploy/${version}/docker/docker-compose.blue-green.yml`), 'utf8'), /FRONTEND_CONTAINER_PORT|BACKEND_CONTAINER_PORT|_HEALTH_URL|_VERSION_URL/);
252
243
  });
253
244
 
254
- test('none 模式不伪造数据库文件,manual 模式只打包人工脚本', async () => {
255
- const none = await createFixture();
256
- assert.equal(runBuild(none).status, 0);
257
- const noneVersion = await releaseVersion(none.root);
258
- await assert.rejects(stat(join(none.root, `deploy/${noneVersion}/database`)));
259
-
260
- const manual = await createFixture({ mode: 'manual' });
261
- const built = runBuild(manual);
262
- assert.equal(built.status, 0, built.stderr);
263
- assert.match(built.stdout, /migration was not executed/);
264
- const manualVersion = await releaseVersion(manual.root);
265
- assert.ok((await stat(join(manual.root, `deploy/${manualVersion}/database/apply-migrations.sh`))).mode & 0o111);
266
- assert.doesNotMatch(await readFile(join(manual.root, 'deploy/deploy.sh'), 'utf8'), /\bpsql\b/);
245
+ test('发布包不包含数据库迁移文件或迁移参数', async () => {
246
+ const fixture = await createFixture();
247
+ assert.equal(runBuild(fixture).status, 0);
248
+ const version = await releaseVersion(fixture.root);
249
+ await assert.rejects(stat(join(fixture.root, `deploy/${version}/database`)));
250
+ assert.doesNotMatch(await readFile(join(fixture.root, 'deploy/deploy.sh'), 'utf8'), /apply-migrations\.sh|database\/migrations|\bpsql\b/);
267
251
  });
268
252
 
269
253
  test('连续构建的 deploy.sh、Compose 和 Nginx 配置字节一致', async () => {
@@ -334,6 +318,10 @@ test('build 接受带标签基础镜像并拒绝重复键和额外版本参数',
334
318
  const legacyRejected = runBuild(legacy);
335
319
  assert.notEqual(legacyRejected.status, 0);
336
320
  assert.match(legacyRejected.stderr, /legacy duplicate environment key/);
321
+ const migration = await createFixture({ extraEnv: 'DATABASE_MIGRATION_MODE=none\n' });
322
+ const migrationRejected = runBuild(migration);
323
+ assert.notEqual(migrationRejected.status, 0);
324
+ assert.match(migrationRejected.stderr, /legacy duplicate environment key/);
337
325
  const reverseInclude = await createFixture();
338
326
  await writeFile(join(reverseInclude.root, '.dockerignore'), `${await readFile(join(reverseInclude.root, '.dockerignore'), 'utf8')}!.env\n`);
339
327
  const reverseRejected = runBuild(reverseInclude);
@@ -446,68 +434,6 @@ test('remove 只删除本项目容器、网络和两个镜像仓库并保留日
446
434
  await access(join(serverRoot, 'release.env'));
447
435
  });
448
436
 
449
- test('端口冲突在加载镜像或启动容器前失败', async () => {
450
- const fixture = await createFixture();
451
- assert.equal(runBuild(fixture).status, 0);
452
- const version = await releaseVersion(fixture.root);
453
- const serverRoot = await installOnServer(fixture);
454
- await mkdir(join(fixture.serverProjectsRoot, 'other/nginx'), { recursive: true });
455
- await writeFile(join(fixture.serverProjectsRoot, 'other/nginx/site.conf'), '# wdyy-project: other\n# wdyy-port: 9099\nserver { listen 9099; }\n');
456
- await writeFile(fixture.commandLog, '');
457
- const result = runServer(fixture, serverRoot, 'start', { EXPECTED_VERSION: version });
458
- assert.notEqual(result.status, 0);
459
- assert.match(result.stderr, /already used by another project/);
460
- const log = await readFile(fixture.commandLog, 'utf8');
461
- assert.doesNotMatch(log, /docker load|docker compose/);
462
- });
463
-
464
- test('展开的全局 Nginx 中存在非标准项目端口时也拒绝部署', async () => {
465
- const fixture = await createFixture();
466
- assert.equal(runBuild(fixture).status, 0);
467
- const version = await releaseVersion(fixture.root);
468
- const serverRoot = await installOnServer(fixture);
469
- await writeFile(fixture.commandLog, '');
470
- const result = runServer(fixture, serverRoot, 'start', {
471
- EXPECTED_VERSION: version,
472
- EXTRA_NGINX_CONFIG: 'server {\n listen 9099;\n}',
473
- });
474
- assert.notEqual(result.status, 0);
475
- assert.match(result.stderr, /expanded Nginx configuration/);
476
- assert.doesNotMatch(await readFile(fixture.commandLog, 'utf8'), /docker load|docker compose/);
477
- });
478
-
479
- test('其他项目预留任一 Docker blue-green 宿主机端口时在启动前失败', async () => {
480
- const fixture = await createFixture();
481
- assert.equal(runBuild(fixture).status, 0);
482
- const version = await releaseVersion(fixture.root);
483
- const serverRoot = await installOnServer(fixture);
484
- await mkdir(join(fixture.serverProjectsRoot, 'other/nginx'), { recursive: true });
485
- await writeFile(
486
- join(fixture.serverProjectsRoot, 'other/nginx/site.conf'),
487
- '# wdyy-project: other\n# wdyy-port: 9100\n# wdyy-docker-ports: 19091,29092,29093,29094\nserver { listen 9100; }\n',
488
- );
489
- await writeFile(fixture.commandLog, '');
490
- const result = runServer(fixture, serverRoot, 'start', { EXPECTED_VERSION: version });
491
- assert.notEqual(result.status, 0);
492
- assert.match(result.stderr, /Docker host port 19091/);
493
- assert.doesNotMatch(await readFile(fixture.commandLog, 'utf8'), /docker load|docker compose/);
494
- });
495
-
496
- test('其他运行中容器发布项目 Docker 端口时在镜像加载前失败', async () => {
497
- const fixture = await createFixture();
498
- assert.equal(runBuild(fixture).status, 0);
499
- const version = await releaseVersion(fixture.root);
500
- const serverRoot = await installOnServer(fixture);
501
- await writeFile(fixture.commandLog, '');
502
- const result = runServer(fixture, serverRoot, 'start', {
503
- EXPECTED_VERSION: version,
504
- DOCKER_RUNNING_PORTS: 'other_project|0.0.0.0:19093->3000/tcp, [::]:19093->3000/tcp',
505
- });
506
- assert.notEqual(result.status, 0);
507
- assert.match(result.stderr, /Docker host port 19093 is already published by another container/);
508
- assert.doesNotMatch(await readFile(fixture.commandLog, 'utf8'), /docker load|docker compose/);
509
- });
510
-
511
437
  test('replace 的 Nginx reload 失败时恢复旧 upstream 和状态', async () => {
512
438
  const fixture = await createFixture();
513
439
  assert.equal(runBuild(fixture).status, 0);
@@ -555,14 +481,12 @@ test('状态提交失败时恢复旧流量和旧状态', async () => {
555
481
  assert.equal(await readFile(join(serverRoot, 'state/active.env'), 'utf8'), oldState);
556
482
  });
557
483
 
558
- test('服务器命令拒绝额外参数和错误部署根', async () => {
484
+ test('服务器命令拒绝额外参数且部署根由脚本位置确定', async () => {
559
485
  const fixture = await createFixture();
560
486
  assert.equal(runBuild(fixture).status, 0);
561
487
  const serverRoot = await installOnServer(fixture);
562
488
  assert.equal(runServer(fixture, serverRoot, 'start', {}, 'unexpected').status, 2);
563
- const wrongRoot = join(fixture.root, 'wrong');
564
- await cp(join(fixture.root, 'deploy'), wrongRoot, { recursive: true });
565
- const wrong = runServer(fixture, wrongRoot, 'status');
566
- assert.notEqual(wrong.status, 0);
567
- assert.match(wrong.stderr, /server deploy root must be exactly/);
489
+ const alternateRoot = join(fixture.root, 'alternate');
490
+ await cp(join(fixture.root, 'deploy'), alternateRoot, { recursive: true });
491
+ assert.equal(runServer(fixture, alternateRoot, 'status').status, 0);
568
492
  });
@@ -31,22 +31,22 @@ assert_port() {
31
31
  }
32
32
 
33
33
  reset_configuration() {
34
- unset PROJECT_NAME SERVER_PROJECTS_ROOT PROJECT_HTTP_PORT DOCKER_BIND_IP
34
+ unset PROJECT_NAME PROJECT_HTTP_PORT DOCKER_BIND_IP
35
35
  unset FRONTEND_URL FRONTEND_PORT BACKEND_URL BACKEND_PORT
36
36
  unset FRONTEND_BLUE_PORT FRONTEND_GREEN_PORT BACKEND_BLUE_PORT BACKEND_GREEN_PORT
37
- unset FRONTEND_BASE_IMAGE BACKEND_BASE_IMAGE DATABASE_MIGRATION_MODE
37
+ unset FRONTEND_BASE_IMAGE BACKEND_BASE_IMAGE
38
38
  unset FRONTEND_DOCKERFILE BACKEND_DOCKERFILE DOCKERIGNORE_FILE
39
- unset MIGRATIONS_SOURCE MIGRATION_RUNNER_SOURCE DEPLOY_CONFIG_DIR
39
+ unset DEPLOY_CONFIG_DIR
40
40
  unset COMPOSE_SOURCE NGINX_SOURCE
41
41
  }
42
42
 
43
43
  is_deployment_key() {
44
44
  case "$1" in
45
45
  FRONTEND_URL|FRONTEND_PORT|BACKEND_URL|BACKEND_PORT|\
46
- PROJECT_NAME|SERVER_PROJECTS_ROOT|PROJECT_HTTP_PORT|DOCKER_BIND_IP|\
46
+ PROJECT_NAME|PROJECT_HTTP_PORT|DOCKER_BIND_IP|\
47
47
  FRONTEND_BLUE_PORT|FRONTEND_GREEN_PORT|BACKEND_BLUE_PORT|BACKEND_GREEN_PORT|\
48
- FRONTEND_BASE_IMAGE|BACKEND_BASE_IMAGE|DATABASE_MIGRATION_MODE|\
49
- FRONTEND_DOCKERFILE|BACKEND_DOCKERFILE|DOCKERIGNORE_FILE|MIGRATIONS_SOURCE|MIGRATION_RUNNER_SOURCE|\
48
+ FRONTEND_BASE_IMAGE|BACKEND_BASE_IMAGE|\
49
+ FRONTEND_DOCKERFILE|BACKEND_DOCKERFILE|DOCKERIGNORE_FILE|\
50
50
  DEPLOY_CONFIG_DIR|COMPOSE_SOURCE|NGINX_SOURCE)
51
51
  return 0 ;;
52
52
  *) return 1 ;;
@@ -55,7 +55,7 @@ is_deployment_key() {
55
55
 
56
56
  is_legacy_configuration_key() {
57
57
  case "$1" in
58
- FRONTEND_CONTAINER_PORT|BACKEND_CONTAINER_PORT|BACKEND_LISTEN_HOST|*_HEALTH_URL|*_VERSION_URL)
58
+ FRONTEND_CONTAINER_PORT|BACKEND_CONTAINER_PORT|BACKEND_LISTEN_HOST|DATABASE_MIGRATION_MODE|MIGRATIONS_SOURCE|MIGRATION_RUNNER_SOURCE|*_HEALTH_URL|*_VERSION_URL)
59
59
  return 0 ;;
60
60
  *) return 1 ;;
61
61
  esac
@@ -109,17 +109,15 @@ validate_configuration() {
109
109
  local name port seen=" "
110
110
  local -a required=(
111
111
  FRONTEND_URL FRONTEND_PORT BACKEND_URL BACKEND_PORT
112
- PROJECT_NAME SERVER_PROJECTS_ROOT PROJECT_HTTP_PORT DOCKER_BIND_IP
112
+ PROJECT_NAME PROJECT_HTTP_PORT DOCKER_BIND_IP
113
113
  FRONTEND_BLUE_PORT FRONTEND_GREEN_PORT BACKEND_BLUE_PORT BACKEND_GREEN_PORT
114
- FRONTEND_BASE_IMAGE BACKEND_BASE_IMAGE DATABASE_MIGRATION_MODE
114
+ FRONTEND_BASE_IMAGE BACKEND_BASE_IMAGE
115
115
  )
116
116
  for name in "${required[@]}"; do
117
117
  [[ -n "${!name:-}" ]] || fail "$name is required in root .env"
118
118
  done
119
119
  [[ "$PROJECT_NAME" =~ ^[a-z][a-z0-9_]*$ ]] ||
120
120
  fail "PROJECT_NAME must match ^[a-z][a-z0-9_]*$"
121
- [[ "$SERVER_PROJECTS_ROOT" = /* && "$SERVER_PROJECTS_ROOT" != */ ]] ||
122
- fail "SERVER_PROJECTS_ROOT must be an absolute path without a trailing slash"
123
121
  [[ "$DOCKER_BIND_IP" =~ ^[A-Za-z0-9:.%-]+$ ]] || fail "DOCKER_BIND_IP contains unsafe characters"
124
122
  [[ "$FRONTEND_URL" =~ ^[A-Za-z0-9:.%-]+$ ]] || fail "FRONTEND_URL contains unsafe characters"
125
123
  [[ "$BACKEND_URL" =~ ^[A-Za-z0-9:.%-]+$ ]] || fail "BACKEND_URL contains unsafe characters"
@@ -132,8 +130,6 @@ validate_configuration() {
132
130
  [[ "$seen" != *" $port "* ]] || fail "host ports must be unique; duplicate value: $port"
133
131
  seen+="$port "
134
132
  done
135
- [[ "$DATABASE_MIGRATION_MODE" == manual || "$DATABASE_MIGRATION_MODE" == none ]] ||
136
- fail "DATABASE_MIGRATION_MODE must be manual or none"
137
133
  }
138
134
 
139
135
  probe_host() {
@@ -329,7 +325,6 @@ validate_dockerignore() {
329
325
 
330
326
  validate_local_package() {
331
327
  local output_root="$1" version="$2" version_root="$1/$2"
332
- local expected_count
333
328
  [[ -x "$output_root/deploy.sh" ]] || fail "generated deploy.sh is missing or not executable"
334
329
  cmp -s "$SCRIPT_PATH" "$output_root/deploy.sh" || fail "generated deploy.sh differs from its source"
335
330
  cmp -s "$SCRIPT_DIR/.env" "$output_root/.env" || fail "generated root .env differs from project .env"
@@ -343,14 +338,8 @@ validate_local_package() {
343
338
  grep -Fq 'frontend-green:' "$version_root/docker/docker-compose.blue-green.yml" || fail "missing frontend-green"
344
339
  grep -Fq 'backend-green:' "$version_root/docker/docker-compose.blue-green.yml" || fail "missing backend-green"
345
340
  grep -Fq '__PROJECT_HTTP_PORT__' "$version_root/nginx/site.conf" || fail "missing project port placeholder"
346
- if [[ "$DATABASE_MIGRATION_MODE" == manual ]]; then
347
- [[ -d "$version_root/database/migrations" && -x "$version_root/database/apply-migrations.sh" ]] || fail "manual database package is incomplete"
348
- else
349
- [[ ! -e "$version_root/database" ]] || fail "none database mode must not package database files"
350
- fi
351
- expected_count=4
352
- [[ "$DATABASE_MIGRATION_MODE" == manual ]] && expected_count=6
353
- [[ "$(find "$version_root" -type f ! -name manifest.sha256 | wc -l | tr -d ' ')" -ge "$expected_count" ]] || fail "release package is incomplete"
341
+ [[ ! -e "$version_root/database" ]] || fail "release package must not contain database migration files"
342
+ [[ "$(find "$version_root" -type f ! -name manifest.sha256 | wc -l | tr -d ' ')" -ge 4 ]] || fail "release package is incomplete"
354
343
  reject_version_secrets "$version_root"
355
344
  verify_manifest "$version_root"
356
345
  }
@@ -368,7 +357,7 @@ build_release() {
368
357
  local project_root="$SCRIPT_DIR" agents_file="$SCRIPT_DIR/AGENTS.md"
369
358
  local today version raw_time build_time frontend_image backend_image
370
359
  local frontend_dockerfile backend_dockerfile dockerignore_file deploy_config_dir compose_source nginx_source
371
- local migrations_source migration_runner_source staging_parent output_root version_root agents_prepared old_deploy
360
+ local staging_parent output_root version_root agents_prepared old_deploy
372
361
 
373
362
  acquire_build_lock
374
363
  staging_parent=
@@ -385,18 +374,12 @@ build_release() {
385
374
  deploy_config_dir="$(resolve_project_path "$project_root" "${DEPLOY_CONFIG_DIR:-scripts/deployment}")"
386
375
  compose_source="$(resolve_project_path "$project_root" "${COMPOSE_SOURCE:-${deploy_config_dir#"$project_root"/}/docker-compose.blue-green.yml}")"
387
376
  nginx_source="$(resolve_project_path "$project_root" "${NGINX_SOURCE:-${deploy_config_dir#"$project_root"/}/nginx-site.conf}")"
388
- migrations_source="$(resolve_project_path "$project_root" "${MIGRATIONS_SOURCE:-database/migrations}")"
389
- migration_runner_source="$(resolve_project_path "$project_root" "${MIGRATION_RUNNER_SOURCE:-scripts/apply-migrations.sh}")"
390
377
 
391
378
  for command in node pnpm docker tar install find sort cmp awk sed grep uniq; do require_command "$command"; done
392
379
  select_checksum_tool
393
380
  [[ -f "$frontend_dockerfile" && -f "$backend_dockerfile" ]] || fail "frontend and backend Dockerfiles are required"
394
381
  [[ -f "$compose_source" && -f "$nginx_source" ]] || fail "deployment Compose and Nginx sources are required"
395
382
  validate_dockerignore "$dockerignore_file"
396
- if [[ "$DATABASE_MIGRATION_MODE" == manual ]]; then
397
- [[ -d "$migrations_source" ]] || fail "manual migration directory is missing: $migrations_source"
398
- [[ -x "$migration_runner_source" ]] || fail "manual migration runner is missing or not executable"
399
- fi
400
383
 
401
384
  today="$(date +%Y%m%d)"
402
385
  version="$(release_ledger next "$agents_file" "$today")"
@@ -432,11 +415,6 @@ build_release() {
432
415
  install -m 0644 "$nginx_source" "$version_root/nginx/site.conf"
433
416
  cmp -s "$compose_source" "$version_root/docker/docker-compose.blue-green.yml" || fail "generated Compose is not deterministic"
434
417
  cmp -s "$nginx_source" "$version_root/nginx/site.conf" || fail "generated Nginx config is not deterministic"
435
- if [[ "$DATABASE_MIGRATION_MODE" == manual ]]; then
436
- mkdir -p "$version_root/database"
437
- cp -R "$migrations_source" "$version_root/database/migrations"
438
- install -m 0755 "$migration_runner_source" "$version_root/database/apply-migrations.sh"
439
- fi
440
418
  create_manifest "$version_root"
441
419
  validate_local_package "$output_root" "$version"
442
420
 
@@ -463,7 +441,6 @@ build_release() {
463
441
  release_build_lock; BUILD_LOCK=
464
442
  trap - EXIT
465
443
  echo "release package created: deploy/$version"
466
- [[ "$DATABASE_MIGRATION_MODE" == manual ]] && echo "database migration was not executed; run the packaged script manually after approval"
467
444
  return 0
468
445
  }
469
446
 
@@ -477,13 +454,9 @@ read_release_pointer() {
477
454
  }
478
455
 
479
456
  initialize_server_context() {
480
- local expected_root
481
457
  DEPLOY_ROOT="$SCRIPT_DIR"
482
458
  load_dotenv "$DEPLOY_ROOT/.env"
483
459
  validate_configuration
484
- [[ -d "$SERVER_PROJECTS_ROOT" ]] || fail "SERVER_PROJECTS_ROOT does not exist: $SERVER_PROJECTS_ROOT"
485
- expected_root="$(cd "$SERVER_PROJECTS_ROOT" >/dev/null 2>&1 && pwd -P)/$PROJECT_NAME"
486
- [[ "$DEPLOY_ROOT" == "$expected_root" ]] || fail "server deploy root must be exactly $expected_root; got $DEPLOY_ROOT"
487
460
  STATE_DIR="$DEPLOY_ROOT/state"
488
461
  ACTIVE_STATE="$STATE_DIR/active.env"
489
462
  PREVIOUS_STATE="$STATE_DIR/previous.env"
@@ -543,11 +516,7 @@ validate_uploaded_package() {
543
516
  local package_root="$1"
544
517
  [[ -f "$package_root/frontend-image.tar" && -f "$package_root/backend-image.tar" ]] || fail "dual image archives are missing"
545
518
  [[ -f "$package_root/docker/docker-compose.blue-green.yml" && -f "$package_root/nginx/site.conf" ]] || fail "release configuration is incomplete"
546
- if [[ "$DATABASE_MIGRATION_MODE" == manual ]]; then
547
- [[ -d "$package_root/database/migrations" && -x "$package_root/database/apply-migrations.sh" ]] || fail "manual database package is incomplete"
548
- else
549
- [[ ! -e "$package_root/database" ]] || fail "none database mode must not contain database files"
550
- fi
519
+ [[ ! -e "$package_root/database" ]] || fail "release package must not contain database migration files"
551
520
  reject_version_secrets "$package_root"
552
521
  verify_manifest "$package_root"
553
522
  tar -tf "$package_root/frontend-image.tar" >/dev/null || fail "invalid frontend image archive"
@@ -608,12 +577,11 @@ verify_pair() {
608
577
 
609
578
  render_site_candidate() {
610
579
  local source="$1" output="$2" escaped_root
611
- for placeholder in __PROJECT_NAME__ __PROJECT_HTTP_PORT__ __DOCKER_HOST_PORTS__ __DEPLOY_ROOT__ __FRONTEND_UPSTREAM_NAME__ __BACKEND_UPSTREAM_NAME__; do
580
+ for placeholder in __PROJECT_NAME__ __PROJECT_HTTP_PORT__ __DEPLOY_ROOT__ __FRONTEND_UPSTREAM_NAME__ __BACKEND_UPSTREAM_NAME__; do
612
581
  grep -Fq "$placeholder" "$source" || fail "Nginx template is missing $placeholder"
613
582
  done
614
583
  escaped_root="${DEPLOY_ROOT//\\/\\\\}"; escaped_root="${escaped_root//&/\\&}"; escaped_root="${escaped_root//|/\\|}"
615
584
  sed -e "s|__PROJECT_NAME__|$PROJECT_NAME|g" -e "s|__PROJECT_HTTP_PORT__|$PROJECT_HTTP_PORT|g" \
616
- -e "s|__DOCKER_HOST_PORTS__|$FRONTEND_BLUE_PORT,$FRONTEND_GREEN_PORT,$BACKEND_BLUE_PORT,$BACKEND_GREEN_PORT|g" \
617
585
  -e "s|__DEPLOY_ROOT__|$escaped_root|g" -e "s|__FRONTEND_UPSTREAM_NAME__|$FRONTEND_UPSTREAM_NAME|g" \
618
586
  -e "s|__BACKEND_UPSTREAM_NAME__|$BACKEND_UPSTREAM_NAME|g" "$source" > "$output"
619
587
  }
@@ -626,56 +594,6 @@ write_upstream_candidate() {
626
594
  "$BACKEND_UPSTREAM_NAME" "$DOCKER_BIND_IP" "$backend_port" > "$output"
627
595
  }
628
596
 
629
- assert_project_ports_isolated() {
630
- local site site_physical expanded total_listeners own_listener marker port own_upstream_listener
631
- local docker_ports compose_project published_ports
632
- shopt -s nullglob
633
- for site in "$SERVER_PROJECTS_ROOT"/*/nginx/site.conf; do
634
- site_physical="$(cd "$(dirname "$site")" >/dev/null 2>&1 && pwd -P)/$(basename "$site")"
635
- [[ "$site_physical" == "$NGINX_SITE_FILE" ]] && continue
636
- if grep -Eq "^# wdyy-port: ${PROJECT_HTTP_PORT}$|^[[:space:]]*listen[[:space:]]+${PROJECT_HTTP_PORT}[[:space:]]*;" "$site"; then
637
- shopt -u nullglob
638
- fail "PROJECT_HTTP_PORT is already used by another project: $site"
639
- fi
640
- marker="$(sed -n 's/^# wdyy-docker-ports: //p' "$site")"
641
- [[ "$marker" =~ ^[0-9]+,[0-9]+,[0-9]+,[0-9]+$ ]] || {
642
- shopt -u nullglob
643
- fail "cannot prove Docker host port isolation for project site: $site"
644
- }
645
- marker=" ${marker//,/ } "
646
- for port in $FRONTEND_BLUE_PORT $FRONTEND_GREEN_PORT $BACKEND_BLUE_PORT $BACKEND_GREEN_PORT; do
647
- [[ "$marker" != *" $port "* ]] || {
648
- shopt -u nullglob
649
- fail "Docker host port $port is already reserved by another project: $site"
650
- }
651
- done
652
- done
653
- shopt -u nullglob
654
- docker_ports="$(docker ps --format '{{.Label "com.docker.compose.project"}}|{{.Ports}}')" ||
655
- fail "unable to inspect running Docker port bindings"
656
- while IFS='|' read -r compose_project published_ports; do
657
- [[ -n "$published_ports" && "$compose_project" != "$PROJECT_NAME" ]] || continue
658
- for port in $FRONTEND_BLUE_PORT $FRONTEND_GREEN_PORT $BACKEND_BLUE_PORT $BACKEND_GREEN_PORT; do
659
- if grep -Eq "(^|[[:space:],])([^,[:space:]]*:)?${port}->" <<< "$published_ports"; then
660
- fail "Docker host port $port is already published by another container"
661
- fi
662
- done
663
- done <<< "$docker_ports"
664
- expanded="$(nginx -T 2>&1)" || fail "current global Nginx configuration is invalid"
665
- total_listeners="$(grep -Ec "^[[:space:]]*listen[[:space:]]+${PROJECT_HTTP_PORT}[[:space:]]*;" <<< "$expanded" || true)"
666
- if grep -Fq "# wdyy-project: $PROJECT_NAME" <<< "$expanded"; then own_listener=1; fi
667
- (( total_listeners <= own_listener )) || fail "PROJECT_HTTP_PORT is already used outside this project in expanded Nginx configuration"
668
- for port in $FRONTEND_BLUE_PORT $FRONTEND_GREEN_PORT $BACKEND_BLUE_PORT $BACKEND_GREEN_PORT; do
669
- total_listeners="$(grep -Ec "^[[:space:]]*server[[:space:]]+[^;[:space:]]+:${port}[[:space:]]*;|proxy_pass[[:space:]]+https?://[^;[:space:]]+:${port}([/;]|$)" <<< "$expanded" || true)"
670
- own_upstream_listener=0
671
- if grep -Eq "upstream[[:space:]]+(${FRONTEND_UPSTREAM_NAME}|${BACKEND_UPSTREAM_NAME})" <<< "$expanded" &&
672
- [[ -f "$UPSTREAM_FILE" ]] && grep -Eq ":${port}[[:space:]]*;" "$UPSTREAM_FILE"; then
673
- own_upstream_listener=1
674
- fi
675
- (( total_listeners <= own_upstream_listener )) || fail "Docker host port $port is already used outside this project in expanded Nginx configuration"
676
- done
677
- }
678
-
679
597
  restore_file() {
680
598
  local backup="$1" target="$2"
681
599
  if [[ -f "$backup" ]]; then cp -p "$backup" "$target"; else rm -f -- "$target"; fi
@@ -683,7 +601,7 @@ restore_file() {
683
601
 
684
602
  commit_traffic_and_state() {
685
603
  local site_candidate="$1" upstream_candidate="$2" active_candidate="$3" previous_candidate="${4:-}" expected_version="$5"
686
- local transaction expanded public_health public_version_url public_version rollback_failed=0
604
+ local transaction public_health public_version_url public_version rollback_failed=0
687
605
  mkdir -p "$NGINX_DIR" "$STATE_DIR"
688
606
  transaction="$(mktemp -d "$DEPLOY_ROOT/.nginx-switch.XXXXXX")"
689
607
  [[ -f "$NGINX_SITE_FILE" ]] && cp -p "$NGINX_SITE_FILE" "$transaction/site.old"
@@ -693,7 +611,6 @@ commit_traffic_and_state() {
693
611
 
694
612
  exec 9>/tmp/wdyy-nginx-deploy.lock
695
613
  flock -x 9
696
- assert_project_ports_isolated
697
614
  install -m 0644 "$site_candidate" "$NGINX_SITE_FILE"
698
615
  install -m 0644 "$upstream_candidate" "$UPSTREAM_FILE"
699
616
  if ! nginx -t; then
@@ -702,18 +619,6 @@ commit_traffic_and_state() {
702
619
  rm -rf -- "$transaction"
703
620
  fail "Nginx candidate validation failed; project configuration restored"
704
621
  fi
705
- expanded="$(nginx -T 2>&1)" || {
706
- restore_file "$transaction/site.old" "$NGINX_SITE_FILE"
707
- restore_file "$transaction/upstreams.old" "$UPSTREAM_FILE"
708
- rm -rf -- "$transaction"
709
- fail "unable to inspect expanded Nginx configuration"
710
- }
711
- grep -Fq "# wdyy-project: $PROJECT_NAME" <<< "$expanded" || {
712
- restore_file "$transaction/site.old" "$NGINX_SITE_FILE"
713
- restore_file "$transaction/upstreams.old" "$UPSTREAM_FILE"
714
- rm -rf -- "$transaction"
715
- fail "shared Nginx does not include this project's site.conf"
716
- }
717
622
  if ! nginx -s reload; then
718
623
  restore_file "$transaction/site.old" "$NGINX_SITE_FILE"
719
624
  restore_file "$transaction/upstreams.old" "$UPSTREAM_FILE"
@@ -767,7 +672,6 @@ deploy_release() {
767
672
  target_color=$([[ "$ACTIVE_COLOR" == blue ]] && printf green || printf blue)
768
673
  fi
769
674
  mkdir -p "$STATE_DIR" "$NGINX_DIR" "$DEPLOY_ROOT/logs"
770
- assert_project_ports_isolated
771
675
  load_release_images "$RELEASE_VERSION"
772
676
  up_pair "$target_color" "$RELEASE_VERSION"
773
677
  verify_pair "$target_color" "$RELEASE_VERSION"
@@ -786,7 +690,6 @@ deploy_release() {
786
690
  rm -rf -- "$work"
787
691
  trap - EXIT
788
692
  echo "active release: $RELEASE_VERSION ($target_color)"
789
- [[ "$DATABASE_MIGRATION_MODE" == manual ]] && echo "database migration was not executed; execute $package_root/database/apply-migrations.sh manually after approval"
790
693
  return 0
791
694
  }
792
695
 
@@ -18,7 +18,6 @@ DB_SCHEMA=
18
18
 
19
19
  # 部署
20
20
  PROJECT_NAME=
21
- SERVER_PROJECTS_ROOT=
22
21
  PROJECT_HTTP_PORT=
23
22
  DOCKER_BIND_IP=
24
23
  FRONTEND_BLUE_PORT=
@@ -27,4 +26,3 @@ BACKEND_BLUE_PORT=
27
26
  BACKEND_GREEN_PORT=
28
27
  FRONTEND_BASE_IMAGE=nginx:stable
29
28
  BACKEND_BASE_IMAGE=node:24-alpine3.24
30
- DATABASE_MIGRATION_MODE=
@@ -1,6 +1,5 @@
1
1
  # wdyy-project: __PROJECT_NAME__
2
2
  # wdyy-port: __PROJECT_HTTP_PORT__
3
- # wdyy-docker-ports: __DOCKER_HOST_PORTS__
4
3
  include "__DEPLOY_ROOT__/nginx/active-upstreams.conf";
5
4
 
6
5
  server {
package/README.md CHANGED
@@ -64,7 +64,7 @@ wdyy update-skill --all
64
64
 
65
65
  ### 使用发布脚本
66
66
 
67
- 推荐从项目根目录手动运行 `release_npm.sh`。脚本会检查工作区、组织权限、项目测试、lint 与打包内容,自动执行 `npm version <patch|minor|major>`、`npm publish --access public`,并核验 registry 版本。它不会执行登录、浏览器认证或处理认证凭据。
67
+ 推荐从项目根目录手动运行 `release_npm.sh`。脚本会检查工作区、组织权限、lint 与打包内容,自动执行 `npm version <patch|minor|major>`、`npm publish --access public`;发布成功后等待 5 秒,再一次查询刚发布的确切版本进行 registry 核验。它不会执行登录、浏览器认证或处理认证凭据。
68
68
 
69
69
  首次运行或登录失效时,先手工登录;脚本检测到未登录也会提示并退出:
70
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdyy/skills",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "企业内部开发 Skill 与项目初始化命令",
5
5
  "type": "module",
6
6
  "bin": {