docs: 新增部署文档模板与位置约定 (#24)
新增 Deployment-Template 模板页及镜像,固化 nginx 反代 + supervisor 托管的 Python 服务部署做法;明确部署文档在有无常驻服务两种情况下的 落点,并在新项目初始化加入部署页判定。模板文件纳入 REQUIRED_FILES, 部署实例页不进入 CORE_DOCUMENT_REQUIREMENTS 强制检查。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -194,6 +194,7 @@ MVP 内所有单元任务通过后才能做 MVP 集成验收;MVP 通过后才
|
||||
|
||||
- 每个单元任务必须在工单中选择“无长期文档影响并说明原因”或列出需要更新的 Wiki 页面。
|
||||
- 启动、测试、部署、排错命令,模块入口、目录职责、主要调用路径,配置、API、数据结构、状态、业务规则、安全边界、日志位置发生变化时,必须更新对应 Wiki。
|
||||
- 部署命令变化时,有常驻服务的项目更新自己的 `Deployment-and-Operations` 页面(由 [部署文档模板](docs/templates/deployment.md) 复制建立);没有常驻服务的项目记录为无部署文档影响,不创建空的部署页。
|
||||
- 普通内部重构只有在入口、行为、配置和验证方式均未改变时,才可以记录为不影响长期文档。
|
||||
- 必需核心页面及结构以 `python dev_scripts/harness.py check --strict` 和 [新项目文档初始化](docs/07-new-project-documentation-setup.md) 为准;稳定文档与任务归档的分工见 [开发工作流](docs/01-workflow.md)。
|
||||
|
||||
|
||||
@@ -189,6 +189,7 @@ REQUIRED_FILES = (
|
||||
"README.md",
|
||||
"docs/00-project-profile.md",
|
||||
"docs/01-workflow.md",
|
||||
"docs/templates/deployment.md",
|
||||
"docs/templates/task-archive.md",
|
||||
*CORE_DOCUMENT_REQUIREMENTS,
|
||||
"wiki-docs.json",
|
||||
|
||||
+4
-2
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Development-Workflow
|
||||
wiki_url: https://git.ilapage.cn/OPC/dev_harness/wiki/Development-Workflow.-
|
||||
wiki_revision: d6afa1cba5cd7e04e589182bb7c010ef97f26cd1
|
||||
synchronized_at: 2026-08-18T13:34:40Z
|
||||
wiki_revision: 98acbf90ebad7a515f1a805defe6d963e5153e48
|
||||
synchronized_at: 2026-08-19T01:51:50Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 开发工作流
|
||||
@@ -206,6 +206,8 @@ python dev_scripts/harness.py export --all # 全量:读取全部线上任务
|
||||
- 业务规则、安全边界或权限变化;
|
||||
- 日志位置、错误定位或常见处理方式变化。
|
||||
|
||||
部署命令的落点:有常驻服务的项目更新自己的 `Deployment-and-Operations` 页面(由[部署文档模板](Deployment-Template.-)复制建立);没有常驻服务的项目在工单记录“无部署文档影响”及原因,不要创建空的部署页。
|
||||
|
||||
普通内部重构如果入口、行为、配置和验证方式均未改变,可以记录“不影响长期文档”及原因。
|
||||
|
||||
## 需求记录与流转
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: New-Project-Documentation-Setup
|
||||
wiki_url: https://git.ilapage.cn/OPC/dev_harness/wiki/New-Project-Documentation-Setup.-
|
||||
wiki_revision: e186d7119620c2e5eadfb47311538dd8cbeb4db9
|
||||
synchronized_at: 2026-08-18T13:34:57Z
|
||||
wiki_revision: 581e2e04edbe7b34d62482e45aa0e4548960d93d
|
||||
synchronized_at: 2026-08-19T01:52:06Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 新项目文档初始化
|
||||
@@ -166,6 +166,8 @@ Agent 只读检查:
|
||||
|
||||
Home 给出建议阅读顺序;每个命令必须有预期结果;代码地图必须指出入口和测试位置。具体岗位文档仅按第 7 步确认的受众创建。
|
||||
|
||||
部署页按需创建,不属于必需核心页面:项目负责人确认存在需要部署的常驻服务时,复制[部署文档模板](Deployment-Template.-)在本项目 Wiki 建立 `Deployment-and-Operations` 页面,并在本项目 `wiki-docs.json` 增加映射(建议镜像到 `docs/10-deployment-and-operations.md`);确认没有常驻服务时,在初始化工单记录原因,不创建该页面。
|
||||
|
||||
### 9. 人工确认
|
||||
|
||||
项目负责人至少确认:
|
||||
@@ -174,6 +176,7 @@ Home 给出建议阅读顺序;每个命令必须有预期结果;代码地图
|
||||
- 关键业务规则和状态;
|
||||
- 权限、安全和数据边界;
|
||||
- 真实运行、测试和部署命令;
|
||||
- 本项目是否有需要部署的常驻服务;
|
||||
- 哪些修改属于高风险;
|
||||
- 交付对象、文档可见范围和外部信息边界。
|
||||
|
||||
|
||||
Vendored
+282
@@ -0,0 +1,282 @@
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Deployment-Template
|
||||
wiki_url: https://git.ilapage.cn/OPC/dev_harness/wiki/Deployment-Template.-
|
||||
wiki_revision: 948321c2f3e4fd7a2923f2764fa8c81c18099a56
|
||||
synchronized_at: 2026-08-19T01:52:18Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 部署文档模板
|
||||
|
||||
> 使用说明:本页是 DevHarness 模板,不描述任何真实服务。有常驻服务的项目复制本页,在自己的 Gitea Wiki 创建 `Deployment-and-Operations` 页面,并在本项目 `wiki-docs.json` 增加映射(建议镜像到 `docs/10-deployment-and-operations.md`)。填写时删除全部说明性文字,不得保留“待填写”后直接交付。没有常驻服务的项目不要创建部署页,在初始化工单记录原因即可。
|
||||
>
|
||||
> 本模板面向项目内部维护者。面向客户或外部运维岗位的部署说明属于交付文档,使用[岗位文档模板](Audience-Document-Template.-)。
|
||||
>
|
||||
> 标准栈为 nginx 反向代理 + supervisor 进程托管,示例按 Python 服务(gunicorn / uvicorn)编写。实际技术栈不同时替换命令,但保留章节结构和“每条命令写明预期结果”的要求。
|
||||
|
||||
## 本页用途
|
||||
|
||||
让维护者能够在一台干净的服务器上完成首次部署、日常运维、健康检查和回滚。所有命令默认以具备 sudo 权限的账号在服务器上执行,示例以 Linux 为主。
|
||||
|
||||
## 安全边界
|
||||
|
||||
- 本页只写配置项的**名称和来源**,不写任何真实密码、令牌、私钥、证书内容、生产数据库地址或个人数据。
|
||||
- 需要凭据的步骤写明“从哪里取”,例如运维密码库条目名或环境变量名。
|
||||
- 涉及删除数据、数据库迁移和不可逆操作的步骤必须给出醒目警告、影响范围和回退条件。
|
||||
|
||||
## 服务概览
|
||||
|
||||
| 项目 | 内容 |
|
||||
|---|---|
|
||||
| 服务名(supervisor program) | `<myapp>` |
|
||||
| 代码部署目录 | `/srv/<myapp>` |
|
||||
| 运行账号 | `<myapp>` |
|
||||
| 运行时 | Python `<3.x>` |
|
||||
| 应用服务器 | gunicorn / uvicorn |
|
||||
| 本地监听地址 | `127.0.0.1:<8000>` |
|
||||
| 进程数 | `<n>` |
|
||||
| 对外域名与路径 | `https://<example.com>/` |
|
||||
| 依赖的外部服务 | 数据库 / 缓存 / 对象存储 / 无 |
|
||||
| 日志目录 | `/var/log/<myapp>/` |
|
||||
|
||||
服务只监听 `127.0.0.1`,不直接对外暴露端口;所有外部访问经 nginx 转发。
|
||||
|
||||
## 环境要求
|
||||
|
||||
| 组件 | 版本要求 | 检查命令 | 预期结果 |
|
||||
|---|---|---|---|
|
||||
| 操作系统 | `<Ubuntu 22.04>` | `cat /etc/os-release` | 输出与要求一致 |
|
||||
| Python | `<3.11+>` | `python3 --version` | 输出版本号且不低于要求 |
|
||||
| nginx | `<1.18+>` | `nginx -v` | 输出版本号 |
|
||||
| supervisor | `<4.2+>` | `supervisord --version` | 输出版本号 |
|
||||
|
||||
未安装时:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y nginx supervisor python3-venv
|
||||
```
|
||||
|
||||
**预期结果**:`systemctl status nginx` 与 `systemctl status supervisor` 均为 `active (running)`。
|
||||
|
||||
## 首次部署
|
||||
|
||||
### 1. 创建运行账号与目录
|
||||
|
||||
```bash
|
||||
sudo useradd --system --home /srv/<myapp> --shell /usr/sbin/nologin <myapp>
|
||||
sudo mkdir -p /srv/<myapp> /var/log/<myapp>
|
||||
sudo chown -R <myapp>:<myapp> /srv/<myapp> /var/log/<myapp>
|
||||
```
|
||||
|
||||
**预期结果**:`id <myapp>` 输出该账号;两个目录存在且属主为 `<myapp>`。
|
||||
|
||||
服务账号使用 `nologin`,不允许直接登录。
|
||||
|
||||
### 2. 取得代码
|
||||
|
||||
```bash
|
||||
sudo -u <myapp> git clone <仓库地址> /srv/<myapp>/app
|
||||
cd /srv/<myapp>/app && sudo -u <myapp> git rev-parse HEAD
|
||||
```
|
||||
|
||||
**预期结果**:输出本次部署的完整提交哈希,记录到部署记录中。
|
||||
|
||||
### 3. 安装依赖
|
||||
|
||||
```bash
|
||||
sudo -u <myapp> python3 -m venv /srv/<myapp>/venv
|
||||
sudo -u <myapp> /srv/<myapp>/venv/bin/pip install -r /srv/<myapp>/app/requirements.txt
|
||||
```
|
||||
|
||||
**预期结果**:pip 以 `Successfully installed ...` 结束,无 ERROR。
|
||||
|
||||
### 4. 落位配置文件
|
||||
|
||||
```bash
|
||||
sudo install -o <myapp> -g <myapp> -m 600 /dev/null /srv/<myapp>/app.env
|
||||
sudo -u <myapp> vi /srv/<myapp>/app.env
|
||||
```
|
||||
|
||||
**预期结果**:`ls -l /srv/<myapp>/app.env` 显示权限 `-rw-------` 且属主为 `<myapp>`。
|
||||
|
||||
配置项清单见下方“配置与凭据来源”。配置文件不进入 Git。
|
||||
|
||||
### 5. 数据库初始化或迁移
|
||||
|
||||
<!-- 无数据库时删除本节。 -->
|
||||
|
||||
> **注意**:迁移可能不可逆。执行前必须先备份,并确认回退方式。
|
||||
|
||||
```bash
|
||||
sudo -u <myapp> /srv/<myapp>/venv/bin/python -m <myapp>.manage migrate
|
||||
```
|
||||
|
||||
**预期结果**:输出全部迁移已应用,无失败项。
|
||||
|
||||
## supervisor 配置
|
||||
|
||||
写入 `/etc/supervisor/conf.d/<myapp>.conf`:
|
||||
|
||||
```ini
|
||||
[program:<myapp>]
|
||||
command=/srv/<myapp>/venv/bin/gunicorn <myapp>.wsgi:application --workers <n> --bind 127.0.0.1:<8000> --timeout 60
|
||||
directory=/srv/<myapp>/app
|
||||
user=<myapp>
|
||||
environment=PATH="/srv/<myapp>/venv/bin",APP_ENV_FILE="/srv/<myapp>/app.env"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=5
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stopwaitsecs=30
|
||||
stdout_logfile=/var/log/<myapp>/stdout.log
|
||||
stderr_logfile=/var/log/<myapp>/stderr.log
|
||||
stdout_logfile_maxbytes=50MB
|
||||
stdout_logfile_backups=5
|
||||
```
|
||||
|
||||
> 异步框架使用 uvicorn 时把 `command` 换成:
|
||||
> `/srv/<myapp>/venv/bin/uvicorn <myapp>.asgi:app --host 127.0.0.1 --port <8000> --workers <n>`
|
||||
|
||||
不要在 `environment` 里写明文密码或令牌;敏感值放在 `app.env`,由应用读取。
|
||||
|
||||
加载配置并启动:
|
||||
|
||||
```bash
|
||||
sudo supervisorctl reread
|
||||
sudo supervisorctl update
|
||||
sudo supervisorctl start <myapp>
|
||||
sudo supervisorctl status <myapp>
|
||||
```
|
||||
|
||||
**预期结果**:`reread` 输出 `<myapp>: available`;`status` 显示 `RUNNING` 且 uptime 持续增长。出现 `BACKOFF` 或 `FATAL` 时查看 `stderr.log`。
|
||||
|
||||
## nginx 配置
|
||||
|
||||
写入 `/etc/nginx/sites-available/<myapp>.conf` 并软链到 `sites-enabled`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name <example.com>;
|
||||
|
||||
access_log /var/log/nginx/<myapp>.access.log;
|
||||
error_log /var/log/nginx/<myapp>.error.log;
|
||||
|
||||
client_max_body_size <20m>;
|
||||
|
||||
location /static/ {
|
||||
alias /srv/<myapp>/app/static/;
|
||||
expires 7d;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:<8000>;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_read_timeout <60s>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- WebSocket 需求存在时,在对应 location 增加 Upgrade 与 Connection 头;无此需求时删除本注释。 -->
|
||||
|
||||
启用并生效:
|
||||
|
||||
```bash
|
||||
sudo ln -sf /etc/nginx/sites-available/<myapp>.conf /etc/nginx/sites-enabled/<myapp>.conf
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
**预期结果**:`nginx -t` 输出 `syntax is ok` 与 `test is successful`;reload 无输出且 `systemctl status nginx` 仍为 `active (running)`。
|
||||
|
||||
`nginx -t` 未通过时不要 reload,先修正配置。
|
||||
|
||||
<!-- 需要 HTTPS 时在此记录证书来源、签发方式和续期检查命令;证书内容和私钥不得写入本页。 -->
|
||||
|
||||
## 配置与凭据来源
|
||||
|
||||
| 配置项 | 用途 | 来源 | 是否敏感 |
|
||||
|---|---|---|---|
|
||||
| `APP_ENV_FILE` | 指向配置文件路径 | supervisor 配置 | 否 |
|
||||
| `<DATABASE_URL>` | 数据库连接 | `/srv/<myapp>/app.env`,值取自运维密码库条目 `<条目名>` | 是 |
|
||||
| `<SECRET_KEY>` | 会话与签名 | 同上 | 是 |
|
||||
| `<LOG_LEVEL>` | 日志级别 | `/srv/<myapp>/app.env` | 否 |
|
||||
|
||||
敏感值只记录取用位置,不在本页、工单、日志和提交中出现真实内容。
|
||||
|
||||
## 日常运维
|
||||
|
||||
| 操作 | 命令 | 预期结果 |
|
||||
|---|---|---|
|
||||
| 查看状态 | `sudo supervisorctl status <myapp>` | `RUNNING`,uptime 持续增长 |
|
||||
| 重启服务 | `sudo supervisorctl restart <myapp>` | 输出 `stopped` 后 `started` |
|
||||
| 停止服务 | `sudo supervisorctl stop <myapp>` | 输出 `stopped` |
|
||||
| 实时日志 | `sudo supervisorctl tail -f <myapp> stderr` | 持续输出应用日志 |
|
||||
| 应用日志 | `sudo tail -n 200 /var/log/<myapp>/stderr.log` | 输出最近日志 |
|
||||
| 接入层日志 | `sudo tail -n 200 /var/log/nginx/<myapp>.error.log` | 输出 nginx 错误 |
|
||||
| 重载 nginx | `sudo nginx -t && sudo systemctl reload nginx` | 测试通过后无中断生效 |
|
||||
|
||||
修改 supervisor 配置后必须 `reread` + `update`,只 `restart` 不会加载新配置。
|
||||
|
||||
## 健康检查
|
||||
|
||||
每次部署、重启和回滚后必须全部执行:
|
||||
|
||||
```bash
|
||||
sudo supervisorctl status <myapp>
|
||||
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:<8000><健康检查路径>
|
||||
curl -sS -o /dev/null -w "%{http_code}\n" https://<example.com><健康检查路径>
|
||||
sudo tail -n 50 /var/log/<myapp>/stderr.log
|
||||
```
|
||||
|
||||
**预期结果**:状态为 `RUNNING`;两个 `curl` 均返回 `200`;日志无新增异常堆栈。
|
||||
|
||||
任何一项不符合时不视为部署成功,按“升级与回滚”处理。
|
||||
|
||||
## 升级与回滚
|
||||
|
||||
### 升级
|
||||
|
||||
```bash
|
||||
cd /srv/<myapp>/app
|
||||
sudo -u <myapp> git rev-parse HEAD # 记录当前提交,回滚需要
|
||||
sudo -u <myapp> git fetch --all
|
||||
sudo -u <myapp> git checkout <目标提交或标签>
|
||||
sudo -u <myapp> /srv/<myapp>/venv/bin/pip install -r requirements.txt
|
||||
sudo -u <myapp> /srv/<myapp>/venv/bin/python -m <myapp>.manage migrate # 无数据库时删除
|
||||
sudo supervisorctl restart <myapp>
|
||||
```
|
||||
|
||||
**预期结果**:restart 后 `status` 为 `RUNNING`,随后健康检查全部通过。
|
||||
|
||||
升级前必须记录当前提交哈希;涉及数据库迁移时必须先备份。
|
||||
|
||||
### 回滚
|
||||
|
||||
```bash
|
||||
cd /srv/<myapp>/app
|
||||
sudo -u <myapp> git checkout <升级前记录的提交>
|
||||
sudo -u <myapp> /srv/<myapp>/venv/bin/pip install -r requirements.txt
|
||||
sudo supervisorctl restart <myapp>
|
||||
```
|
||||
|
||||
**预期结果**:健康检查全部通过。
|
||||
|
||||
> **注意**:已执行的数据库迁移通常不能通过切回代码撤销。存在迁移时必须先确认迁移是否向后兼容;不兼容时按备份恢复流程处理,并停止自行操作、联系负责人。
|
||||
|
||||
### 备份与恢复
|
||||
|
||||
<!-- 记录备份对象、频率、保存位置、保留期和恢复步骤;无持久化数据时说明原因。 -->
|
||||
|
||||
## 已知限制
|
||||
|
||||
<!-- 记录本环境无法验证的部分,例如未做过真实回滚演练、未验证高并发表现、灰度或多机部署尚未支持。不得留空,无限制时写“无”。 -->
|
||||
|
||||
- <!-- 填写 -->
|
||||
@@ -3,17 +3,20 @@ from __future__ import annotations
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "dev_scripts"))
|
||||
|
||||
import harness # noqa: E402
|
||||
from harness import ( # noqa: E402
|
||||
CORE_DOCUMENT_REQUIREMENTS,
|
||||
CORE_PAGE_PATHS,
|
||||
REQUIRED_FILES,
|
||||
check_claude_code_entry,
|
||||
check_required_files,
|
||||
check_core_documents,
|
||||
check_agent_efficiency_rules,
|
||||
check_repository_readme,
|
||||
@@ -110,6 +113,25 @@ class CoreDocumentTests(unittest.TestCase):
|
||||
self.assertIn("### 3. 识别子项目与交付单元", required)
|
||||
self.assertIn("#### 需求总览启用条件", required)
|
||||
|
||||
def test_deployment_template_is_required_and_mapped(self) -> None:
|
||||
path = "docs/templates/deployment.md"
|
||||
self.assertIn(path, REQUIRED_FILES)
|
||||
config = load_config()
|
||||
mappings = {mapping.page: mapping.path for mapping in config.mappings}
|
||||
self.assertEqual(mappings.get("Deployment-Template"), path)
|
||||
# 部署页按项目需要创建,不强制每个项目产出实例页。
|
||||
self.assertNotIn(path, CORE_DOCUMENT_REQUIREMENTS)
|
||||
|
||||
def test_missing_deployment_template_is_reported(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
with unittest.mock.patch.object(harness, "ROOT", root):
|
||||
errors: list[str] = []
|
||||
check_required_files(errors)
|
||||
self.assertTrue(
|
||||
any("docs/templates/deployment.md" in error for error in errors)
|
||||
)
|
||||
|
||||
def test_missing_or_wrong_core_mapping_is_reported(self) -> None:
|
||||
errors = core_mapping_errors({"Home": "docs/wrong.md"})
|
||||
self.assertTrue(any("Home -> docs/README.md" in error for error in errors))
|
||||
|
||||
@@ -56,6 +56,10 @@
|
||||
"page": "Audience-Document-Template",
|
||||
"path": "docs/delivery/audience-document-template.md"
|
||||
},
|
||||
{
|
||||
"page": "Deployment-Template",
|
||||
"path": "docs/templates/deployment.md"
|
||||
},
|
||||
{
|
||||
"page": "Task-Archive-Template",
|
||||
"path": "docs/templates/task-archive.md"
|
||||
|
||||
Reference in New Issue
Block a user