Compare commits
3
Commits
cc2156b44d
...
20f5308116
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20f5308116 | ||
|
|
8af7ef3501 | ||
|
|
0131d21ea4 |
@@ -6,6 +6,13 @@ All notable changes to this repository should be documented in this file.
|
||||
The format loosely follows Keep a Changelog and can be adapted to the team's habits.
|
||||
本文档参考了 Keep a Changelog 的思路,也可以根据团队习惯调整。
|
||||
|
||||
## [0.2.2] - 2026-09-29
|
||||
|
||||
### Docs / 文档
|
||||
|
||||
- Roadmap P1 delivered: prod write side went live in the compose drill — minio-prod + api-prod storage/games env + templated nginx wildcard block (crearte `63bb96d` / crearte-deploy `4d53d58`), POSTGRES_PASSWORD default retired, TLS deferred by design with a real-server checklist in README.
|
||||
- 路线图 P1 交付:prod 写侧在本机演练栈完整启用——minio-prod、api-prod 存储/游玩域名环境、nginx 通配块模板化(crearte `63bb96d` / crearte-deploy `4d53d58`);POSTGRES_PASSWORD 默认值退役;TLS 按设计缓做,真机清单已入 README。
|
||||
|
||||
## [0.2.1] - 2026-09-29
|
||||
|
||||
### Fixed / 修复
|
||||
|
||||
+2
-1
@@ -16,7 +16,7 @@
|
||||
| # | 子项目 | 内容 | 涉及仓库 | 依赖 | 状态 |
|
||||
|---|--------|------|----------|------|------|
|
||||
| P0 | 已知缺陷清理 | ① CORS `Access-Control-Allow-Headers` 补 `If-None-Match`(解锁 e2e:stack Step5:revoke 410 + 降级)② 清理 compose 配置漂移(`BUNDLE_KEY_STORE` 环境变量、`bundle-keys-*` 卷——密钥自 server 0.6.0 起在 Postgres)③ `crearte-deploy` CHANGELOG 补账 | server, deploy | 无 | **完成**(server `a8ea755` / deploy `27044be`,2026-09-29 合并推送) |
|
||||
| P1 | prod 写侧可用 | prod 栈补对象存储(prod MinIO 或外部 S3)+ `api-prod` 配 `STORAGE_S3_*` / `GAMES_BASE_DOMAIN` / `CORS_ALLOWED_ORIGINS`;密码默认值、TLS 起步。现状:prod 无对象存储时写侧路由(上传/投稿/审核/预览)不注册,生产只读 | deploy(少量 server) | P0 | 待启动 |
|
||||
| P1 | prod 写侧可用 | prod 栈补对象存储(prod MinIO 或外部 S3)+ `api-prod` 配 `STORAGE_S3_*` / `GAMES_BASE_DOMAIN` / `CORS_ALLOWED_ORIGINS`;密码默认值、TLS 起步。现状:prod 无对象存储时写侧路由(上传/投稿/审核/预览)不注册,生产只读 | deploy(少量 server) | P0 | **完成**(crearte `63bb96d` / deploy `4d53d58`,2026-09-29 合并推送;TLS 与真域名留清单,见 spec §6) |
|
||||
| P2 | CI + 测试基线 | 后端 / 部署仓建 CI(gofmt/vet/test + `TEST_DATABASE_URL` 集成层 + e2e:stack 全链路);与前端已有 `validate.yml` 对齐。现状:后端/部署仓无 CI,Postgres 集成测试无 `TEST_DATABASE_URL` 时静默 skip | 三仓 | P0(e2e 依赖其修复) | 待启动 |
|
||||
| P3 | 备份 + 可观测 | `pg_dump` 定时备份 + MinIO 数据保护 + 恢复演练 runbook;结构化 / 请求日志 + `/metrics`;限流器单实例问题记录权衡 | server, deploy | P1(拓扑定型) | 待启动 |
|
||||
|
||||
@@ -51,3 +51,4 @@
|
||||
| 子项目 | spec | 实现计划 |
|
||||
|--------|------|----------|
|
||||
| P4 作者主页 | `docs/specs/2026-09-29-author-page-design.md` | `docs/plans/2026-09-29-author-page.md`(已执行,2026-09-29) |
|
||||
| P1 prod 写侧可用 | `docs/specs/2026-09-29-prod-write-side-design.md` | `docs/plans/2026-09-29-prod-write-side.md`(已执行,2026-09-29) |
|
||||
|
||||
@@ -0,0 +1,667 @@
|
||||
# P1 prod 写侧可用 实现计划
|
||||
|
||||
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
||||
|
||||
**目标:** 让 prod profile 在本机 compose 内具备完整写侧与游玩链路(上传/投稿/审核/virtual 子域运行时),并收掉 `POSTGRES_PASSWORD` 默认值。
|
||||
|
||||
**架构:** 方案 A(镜像 dev 直连法)——新增 `minio-prod`(主机 9001,浏览器跨源直连),`api-prod` 补 `STORAGE_S3_*`/`GAMES_BASE_DOMAIN`/`CORS_ALLOWED_ORIGINS`;`crearte` 的 nginx 配置改为 envsubst 模板,通配游戏域块参数化并补 `/api/` 同源反代;TLS 本轮不做。
|
||||
|
||||
**技术栈:** docker compose v5、nginx:1.27-alpine entrypoint envsubst 模板、Go CLI(容器内 `/app/crearte-server`)、curl+python3 冒烟链。
|
||||
|
||||
**依据:** `docs/specs/2026-09-29-prod-write-side-design.md`(已批准)。规格覆盖映射见文末自检。
|
||||
|
||||
---
|
||||
|
||||
## 环境事实(本机,worker 必读)
|
||||
|
||||
- 仓库布局:wrapper `crearte-monorepo/` 下平级 `crearte`、`crearte-server`、`crearte-deploy`(已 clone,全在 master 干净)。
|
||||
- 本机 `crearte-deploy/.env` **不存在**,`docker volume ls` **无任何 crearte 卷**(全新环境,无旧数据迁移负担;口令迁移警告仍要写 README,供其他主机用)。
|
||||
- 本机无 host node/go。前端测试一律容器内跑,镜像 `crearte:dev` 需先由 compose 构建(任务 2)。
|
||||
- `git@github.com` SSH 认证 = XingfenD(可推 crearte/crearte-deploy);wrapper remote = `git@git.yoresee.cc:XingfenD/crearte-monorepo.git`。各仓若报 `Author identity unknown`:`git config user.name XingfenD && git config user.email xingfen.fendy@outlook.com`(local,勿 --global)。
|
||||
- 主机端口 8080/9000/9001/5432/5434 当前均空闲。`*.localhost` 由 Chrome 内建解析、glibc 不保证——curl 一律加 `--resolve <sub>.localhost:8080:127.0.0.1`。
|
||||
- compose `:?` 守卫在 config 期对整个文件生效(不分 profile):任何 `--profile X config -q` 都要求 `.env` 五项 secret 齐(POSTGRES_PASSWORD/MINIO_ROOT_USER/MINIO_ROOT_PASSWORD/AUTH_TOKEN_SECRET/BUNDLE_KEK_k1)。
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 仓 | 文件 | 动作 | 职责 |
|
||||
|---|---|---|---|
|
||||
| crearte | `deploy/nginx.conf.template` | 新建(git mv 改名 + 编辑) | 两 server 块的 envsubst 模板:`_` 主站 + `${GAMES_SERVER_NAME}` 通配游戏域(含新 `/api/` 反代) |
|
||||
| crearte | `deploy/Dockerfile` | 修改 L15 | COPY 指向模板目录 |
|
||||
| crearte | `src/scripts/repo-yaml.test.ts` | 修改 describe 块 | 守卫测试同步读模板、新增 `/api/` 断言 |
|
||||
| crearte | `src/scripts/dev-game-runtime.ts` | 修改 L2 注释 | 引用路径改为 .template |
|
||||
| crearte | `docs/CHANGELOG.md` | 顶部加 0.15.0 | 变更账目 |
|
||||
| crearte-deploy | `docker-compose.yml` | 修改 | minio-prod、api-prod 写侧 env、web-prod args/env、口令去默认、新卷 |
|
||||
| crearte-deploy | `.env.example` | 重写(全文件) | secret 占位与新变量注释 |
|
||||
| crearte-deploy | `README.md` | 增 prod 小节 + 迁移警告 + 真机 TLS 清单 | 用法与数据 |
|
||||
| crearte-deploy | `docs/CHANGELOG.md` | 顶部加 0.5.0 | 变更账目 |
|
||||
| wrapper | `docs/ROADMAP.md`、`docs/CHANGELOG.md`、本计划索引 | 修改 | 状态与账目 |
|
||||
|
||||
提交顺序(spec §7):crearte → crearte-deploy → wrapper。
|
||||
|
||||
---
|
||||
|
||||
### 任务 1:crearte nginx 模板化(TDD)
|
||||
|
||||
**分支:** `cd crearte && git checkout -b feat/prod-nginx-wildcard-api`
|
||||
|
||||
- [ ] **步骤 1.1:改写守卫测试(先红)**
|
||||
|
||||
`src/scripts/repo-yaml.test.ts`:将整个 `describe('deploy/nginx.conf', ...)` 块替换为:
|
||||
|
||||
```ts
|
||||
describe('deploy/nginx.conf.template', () => {
|
||||
async function loadTemplate(): Promise<string> {
|
||||
return readFile(path.join(REPO_ROOT, 'deploy', 'nginx.conf.template'), 'utf8')
|
||||
}
|
||||
|
||||
it('通配游戏域 server block 暴露运行时三件套、/api/ 同源反代与 404 兜底', async () => {
|
||||
const wildcard = serverBlock(await loadTemplate(), '${GAMES_SERVER_NAME}')
|
||||
expect(wildcard).toMatch(/listen\s+80;/)
|
||||
expect(wildcard).toMatch(/root\s+\/usr\/share\/nginx\/html;/)
|
||||
|
||||
const exact = [...wildcard.matchAll(/location\s*=\s*(\S+)\s*\{([^}]*)\}/g)]
|
||||
expect(exact.map(([, location]) => location).sort()).toEqual(['/__bootstrap', '/agent.js', '/sw.js'])
|
||||
for (const [, location, body] of exact) {
|
||||
expect(body, `${location} 需要 no-store`).toContain('"no-store"')
|
||||
}
|
||||
|
||||
const api = wildcard.match(/location\s+\/api\/\s*\{([^}]*)\}/)
|
||||
expect(api, '通配块缺少 /api/ 反代(子域运行时取钥依赖同源代理)').toBeTruthy()
|
||||
expect(api?.[1]).toMatch(/proxy_pass\s+http:\/\/\$api_upstream;/)
|
||||
expect(api?.[1]).toMatch(/set\s+\$api_upstream\s+api:8080;/)
|
||||
|
||||
expect(wildcard).toMatch(/location\s*\/\s*\{[\s\S]*?return 404;\s*\}/)
|
||||
expect(wildcard).not.toMatch(/location\s+\/data\//)
|
||||
expect(wildcard).not.toMatch(/location\s+\/assets\//)
|
||||
})
|
||||
|
||||
it('主站为 /data/bundles/ 提供 CORS 且保留 /data/ 行为', async () => {
|
||||
const main = serverBlock(await loadTemplate(), '_')
|
||||
const bundles = main.match(/location\s+\/data\/bundles\/\s*\{([^}]*)\}/)
|
||||
expect(bundles, '主站缺少 /data/bundles/ location').toBeTruthy()
|
||||
expect(bundles?.[1]).toContain('Access-Control-Allow-Origin')
|
||||
expect(bundles?.[1]).toMatch(/try_files\s+\$uri\s+=404;/)
|
||||
expect(main).toMatch(/location\s+\/data\/\s*\{/)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
> `serverBlock()` 的 `server_name ${serverName};` 拼接传入字符串 `'${GAMES_SERVER_NAME}'` 后得到 `server_name ${GAMES_SERVER_NAME};`,与模板文本逐字匹配;JS 模板不会对已插值的 `$` 二次展开。
|
||||
|
||||
- [ ] **步骤 1.2:跑测试确认红**
|
||||
|
||||
```bash
|
||||
cd crearte && npx --yes vitest run src/scripts/repo-yaml.test.ts
|
||||
```
|
||||
|
||||
预期:FAIL(`ENOENT ... deploy/nginx.conf.template` 或 serverBlock 抛「缺少 server_name」)。**若本机 npx 因 node_modules 缺失失败,推迟到步骤 1.5 用容器跑,同样先确认红再继续。**
|
||||
|
||||
- [ ] **步骤 1.3:git mv 改名并编辑模板**
|
||||
|
||||
```bash
|
||||
cd crearte && git mv deploy/nginx.conf deploy/nginx.conf.template
|
||||
```
|
||||
|
||||
将文件整体重写为以下内容(头部注释为新增;通配块 `server_name` 参数化并新增 `/api/` location;其余与旧文件逐字一致):
|
||||
|
||||
```nginx
|
||||
# envsubst 模板:nginx 官方镜像 entrypoint(20-envsubst-on-templates.sh)按已定义环境变量
|
||||
# 渲染到 /etc/nginx/conf.d/default.conf。$uri/$host/$api_upstream 等 nginx 内建变量不是
|
||||
# 环境变量,原样保留。运行期唯一必须定义的变量:GAMES_SERVER_NAME(compose web-prod 注入)。
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 1024;
|
||||
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /data/bundles/ {
|
||||
add_header Cache-Control "no-cache";
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /data/ {
|
||||
add_header Cache-Control "no-cache";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
resolver 127.0.0.11 valid=10s;
|
||||
set $api_upstream api:8080;
|
||||
proxy_pass http://$api_upstream;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name ${GAMES_SERVER_NAME};
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location = /__bootstrap {
|
||||
add_header Cache-Control "no-store";
|
||||
try_files /bootstrap/index.html =404;
|
||||
}
|
||||
|
||||
location = /sw.js {
|
||||
add_header Cache-Control "no-store";
|
||||
try_files /sw.js =404;
|
||||
}
|
||||
|
||||
location = /agent.js {
|
||||
add_header Cache-Control "no-store";
|
||||
try_files /agent.js =404;
|
||||
}
|
||||
|
||||
# 子域游玩时 sw/agent 取 bundle-key 走同源反代(prod 由本块承担;dev 等价逻辑在 vite 插件)
|
||||
location /api/ {
|
||||
resolver 127.0.0.11 valid=10s;
|
||||
set $api_upstream api:8080;
|
||||
proxy_pass http://$api_upstream;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **步骤 1.4:改 Dockerfile 与过时注释**
|
||||
|
||||
`deploy/Dockerfile` L15:
|
||||
|
||||
```dockerfile
|
||||
# 旧:COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY deploy/nginx.conf.template /etc/nginx/templates/default.conf.template
|
||||
```
|
||||
|
||||
`src/scripts/dev-game-runtime.ts` L2 注释:`(deploy/nginx.conf)` → `(deploy/nginx.conf.template)`。
|
||||
|
||||
- [ ] **步骤 1.5:容器内跑全量单测确认绿(含类型)**
|
||||
|
||||
先建镜像与依赖卷(在 deploy 目录,context 会用到当前分支工作树):
|
||||
|
||||
```bash
|
||||
cd ../crearte-deploy
|
||||
docker compose --profile mock build mock # 产出 crearte:dev
|
||||
docker run --rm -v "$PWD/../crearte":/repo \
|
||||
-v crearte-deploy_mock_node_modules:/repo/src/node_modules \
|
||||
-w /repo/src crearte:dev npm test
|
||||
```
|
||||
|
||||
预期:全部 PASS(含 `repo-yaml.test.ts`;必须挂整个 crearte 仓,只挂 `src/` 会假失败——HANDOFF 既有事实)。再:
|
||||
|
||||
```bash
|
||||
docker run --rm -v "$PWD/../crearte":/repo \
|
||||
-v crearte-deploy_mock_node_modules:/repo/src/node_modules \
|
||||
-w /repo/src crearte:dev npm run typecheck
|
||||
```
|
||||
|
||||
预期:无错误。
|
||||
|
||||
- [ ] **步骤 1.6:CHANGELOG 0.15.0**
|
||||
|
||||
`crearte/docs/CHANGELOG.md` 顶部(`## [0.14.0]` 之前)插入(英中行相邻、条间空行):
|
||||
|
||||
```markdown
|
||||
## [0.15.0] - 2026-09-29
|
||||
|
||||
### Changed / 变更
|
||||
|
||||
- `deploy/nginx.conf` became `deploy/nginx.conf.template`, rendered at container start by the official nginx entrypoint's envsubst pass (`/etc/nginx/templates/`). The wildcard game server's `server_name` is now the `GAMES_SERVER_NAME` variable instead of the hardcoded `*.games.example.com`, and the block gained `location /api/` same-origin reverse proxy to `api:8080` — without it, prod play runtime on `<sub>.<domain>` couldn't reach bundle-key (dev only worked because vite proxies `/api` on every host). The three-piece runtime (`/__bootstrap`, `/sw.js`, `/agent.js`) and the 404 fallback are unchanged; the shape is pinned by `repo-yaml.test.ts` against the template.
|
||||
- `deploy/nginx.conf` 改为 `deploy/nginx.conf.template`,由 nginx 官方镜像 entrypoint 的 envsubst 机制在容器启动时渲染到 `/etc/nginx/templates/`。通配游戏域 server 的 `server_name` 从硬编码 `*.games.example.com` 改为 `GAMES_SERVER_NAME` 变量,并新增 `location /api/` 同源反代到 `api:8080`——此前 prod 子域运行时根本够不着 bundle-key(dev 能跑全靠 vite 在每个 Host 上顺带代理 `/api`)。运行时三件套(`/__bootstrap`、`/sw.js`、`/agent.js`)与 404 兜底骨架不变,形状由 `repo-yaml.test.ts` 对模板钉死。
|
||||
```
|
||||
|
||||
- [ ] **步骤 1.7:Commit**
|
||||
|
||||
```bash
|
||||
cd crearte
|
||||
git status --short # 应只有 deploy/、src/scripts/、docs/CHANGELOG.md
|
||||
git add -A && git commit -m "feat(deploy): template nginx conf, wildcard block gains same-origin /api proxy
|
||||
|
||||
- deploy/nginx.conf -> nginx.conf.template (GAMES_SERVER_NAME rendered by
|
||||
nginx entrypoint envsubst; prod wildcard host was hardcoded dead config)
|
||||
- wildcard game server adds location /api/ -> api:8080 so prod subdomain
|
||||
play runtime can fetch bundle-key same-origin (dev-parity with vite)
|
||||
- repo-yaml.test.ts guards the template shape incl. /api/ assertions
|
||||
- docs: CHANGELOG 0.15.0"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 2:crearte 合入 master 并推送
|
||||
|
||||
- [ ] **步骤 2.1:合并(--no-ff,禁 fast-forward)**
|
||||
|
||||
```bash
|
||||
cd crearte
|
||||
git checkout master && git pull --ff-only origin master
|
||||
git merge --no-ff feat/prod-nginx-wildcard-api -m "Merge branch 'feat/prod-nginx-wildcard-api' — prod play runtime via templated nginx wildcard /api proxy (P1)"
|
||||
```
|
||||
|
||||
预期:新增 merge commit,无冲突(master 自 f12cbf1 后无新提交;若 pull 带来新改动导致冲突,停下向用户报告,勿强合)。
|
||||
|
||||
- [ ] **步骤 2.2:推送 + 删分支**
|
||||
|
||||
```bash
|
||||
git push origin master
|
||||
git branch -d feat/prod-nginx-wildcard-api && git push origin --delete feat/prod-nginx-wildcard-api
|
||||
git log --oneline -2 # 记录 merge hash,供 wrapper ROADMAP 状态引用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 3:crearte-deploy compose 与 .env.example
|
||||
|
||||
**分支:** `cd crearte-deploy && git checkout -b feat/prod-write-side`
|
||||
|
||||
- [ ] **步骤 3.1:anchor 与 DATABASE_URL 去默认口令**
|
||||
|
||||
`docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
# L5(x-postgres-env 内)旧: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-crearte}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
|
||||
|
||||
# L54(api-dev)与 L126(api-prod)同型替换,旧: ...crearte:-crearte}@...
|
||||
DATABASE_URL: postgres://crearte:${POST…rd}
|
||||
|
||||
# L58/L59(minio-dev)显式化默认值(行为不变,为 :? 语义让路)
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO…min}
|
||||
```
|
||||
|
||||
> api-dev L54 一并改:anchor 共享,dev/prod 同受 `:?` 约束——README 迁移警告覆盖(任务 4)。
|
||||
|
||||
- [ ] **步骤 3.2:api-prod 补写侧 env**
|
||||
|
||||
`api-prod` 服务块整体替换为:
|
||||
|
||||
```yaml
|
||||
api-prod:
|
||||
profiles: ["prod"]
|
||||
build: *api-build
|
||||
image: crearte-server:prod
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db-prod:
|
||||
condition: service_healthy
|
||||
minio-prod:
|
||||
condition: service_started
|
||||
environment:
|
||||
<<: *api-env
|
||||
DATABASE_URL: postgres://crearte:${POST…rd}
|
||||
STORAGE_S3_ENDPOINT: http://minio-prod:9000
|
||||
STORAGE_S3_BUCKET: ${MINIO_BUCKET:-crearte}
|
||||
STORAGE_S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER:?set MINIO_ROOT_USER in .env}
|
||||
STORAGE_S3_SECRET_ACCESS_KEY: ${MINI…?:set MINIO_ROOT_PASSWORD in .env}
|
||||
STORAGE_S3_FORCE_PATH_STYLE: "true"
|
||||
# 浏览器可达的公开基址(PublicURL = base + "/" + key,需含 bucket 段)
|
||||
STORAGE_S3_PUBLIC_BASE_URL: http://localhost:${MINIO_PROD_PORT:-9001}/${MINIO_BUCKET:-crearte}
|
||||
# CORS 通配放行:host == base 或 *.base;http 仅对 *.localhost 开
|
||||
GAMES_BASE_DOMAIN: localhost
|
||||
CORS_ALLOWED_ORIGINS: ${HOST_ORIGIN:-http://localhost:8080}
|
||||
networks:
|
||||
default:
|
||||
aliases: [api]
|
||||
healthcheck:
|
||||
<<: *api-healthcheck
|
||||
```
|
||||
|
||||
- [ ] **步骤 3.3:新增 minio-prod 服务**
|
||||
|
||||
紧接 `api-prod` 之后(`db-debug` 之前)插入:
|
||||
|
||||
```yaml
|
||||
minio-prod:
|
||||
profiles: ["prod"]
|
||||
image: pgsty/minio:latest
|
||||
restart: unless-stopped
|
||||
command: ["server", "/data"]
|
||||
ports:
|
||||
- "${MINIO_PROD_PORT:-9001}:9000"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?set MINIO_ROOT_USER in .env}
|
||||
MINIO_ROOT_PASSWORD: ${MINI…?:set MINIO_ROOT_PASSWORD in .env}
|
||||
# 游玩/封面跨源直连(与 minio-dev 同策略)
|
||||
MINIO_API_CORS_ALLOW_ORIGIN: "*"
|
||||
volumes:
|
||||
- minio-data-prod:/data
|
||||
```
|
||||
|
||||
- [ ] **步骤 3.4:web-prod 补构建参数与模板变量**
|
||||
|
||||
`web-prod` 服务块整体替换为:
|
||||
|
||||
```yaml
|
||||
web-prod:
|
||||
profiles: ["prod"]
|
||||
build:
|
||||
context: ../crearte
|
||||
dockerfile: deploy/Dockerfile
|
||||
args:
|
||||
VITE_API_BASE_URL: /
|
||||
VITE_GAMES_BASE_DOMAIN: ${PUBLIC_GAMES_HOST:-localhost:8080}
|
||||
VITE_HOST_ORIGIN: ${HOST_ORIGIN:-http://localhost:8080}
|
||||
image: crearte:prod
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api-prod
|
||||
environment:
|
||||
GAMES_SERVER_NAME: ${GAMES_SERVER_NAME:-*.localhost}
|
||||
ports:
|
||||
- "8080:80"
|
||||
```
|
||||
|
||||
> `PUBLIC_GAMES_HOST`(客户端 host:port 拼接用)与 api 的 `GAMES_BASE_DOMAIN`(CORS 主机名后缀匹配)是两个语义不同的变量,刻意不共用,见 spec §3.2 命名注意。
|
||||
|
||||
- [ ] **步骤 3.5:卷声明追加**
|
||||
|
||||
文件尾 `volumes:` 块追加一行(不动既有卷名):
|
||||
|
||||
```yaml
|
||||
minio-data-prod:
|
||||
```
|
||||
|
||||
- [ ] **步骤 3.6:`.env.example` 整体重写**
|
||||
|
||||
```bash
|
||||
# 复制为 .env 后填写;.env 已被 .gitignore 忽略,绝不提交
|
||||
# 生成 secret:openssl rand -base64 32
|
||||
AUTH_TOKEN_SECRET=
|
||||
BUNDLE_KEK_ACTIVE=k1
|
||||
BUNDLE_KEK_k1=
|
||||
|
||||
# Postgres 口令(0.5.0 起无默认值,缺失 compose 直接报错)。
|
||||
# 老部署务必显式写旧卷的真实口令,否则鉴权失败,见 README「数据与配置」迁移警告。
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# MinIO root 凭据(:? 必填)。本机 dev/prod 演练可填 minioadmin/minioadmin;
|
||||
# 任何对外可达的 prod 必须换强口令:openssl rand -base64 32
|
||||
MINIO_ROOT_USER=
|
||||
MINIO_ROOT_PASSWORD=
|
||||
|
||||
# prod 写侧可选项(默认值即本机自包含演练形态,详见 README prod 小节):
|
||||
# MINIO_PROD_PORT=9001 # prod MinIO 主机端口(dev 用 9000)
|
||||
# HOST_ORIGIN=http://localhost:8080 # 主站对外 origin(CORS 白名单 + VITE_HOST_ORIGIN)
|
||||
# PUBLIC_GAMES_HOST=localhost:8080 # 游玩子域基址 host:port(客户端拼 <sub>:port)
|
||||
# GAMES_SERVER_NAME=*.localhost # nginx 通配 server_name(真域名时 *.games.example.com)
|
||||
# MINIO_BUCKET=crearte
|
||||
|
||||
# macOS / 网络盘上热更新不触发时设为 true
|
||||
# CHOKIDAR_USEPOLLING=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 4:crearte-deploy README 与 CHANGELOG
|
||||
|
||||
- [ ] **步骤 4.1:README「使用」内 dev 桶初始化段之后插入**
|
||||
|
||||
```markdown
|
||||
### prod 栈写侧(本机自包含演练)
|
||||
|
||||
prod 与 dev 互斥(都占 8080)。`minio-prod`(主机端口默认 **9001**)为 prod 启用写侧路由(上传/投稿/审核/预览);virtual 作品站内游玩走 `http://<sub>.localhost:8080`——web-prod 的 nginx 通配块按 `GAMES_SERVER_NAME` 渲染三件套 + `/api/` 同源反代,与 dev 的 vite 行为对齐。
|
||||
|
||||
```bash
|
||||
cp .env.example .env # 必填 5 项:AUTH_TOKEN_SECRET、BUNDLE_KEK_k1、POSTGRES_PASSWORD、MINIO_ROOT_USER、MINIO_ROOT_PASSWORD
|
||||
docker compose --profile prod up -d --build
|
||||
# prod 卷首次创建后一次性初始化桶(单引号刻意——防宿主 shell 展开,容器内凭 compose 注入的凭据完成):
|
||||
docker compose --profile prod exec minio-prod sh -c 'mc alias set local http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" && mc mb --ignore-existing local/crearte && mc anonymous set download local/crearte'
|
||||
```
|
||||
|
||||
迁真实服务器的最小清单(本轮演练未含 TLS):泛解析 `*.games.<域名>` → 主机;`.env` 设 `HOST_ORIGIN`/`PUBLIC_GAMES_HOST`/`GAMES_SERVER_NAME` 为真实值并同步 api 的 `GAMES_BASE_DOMAIN`;`MINIO_PROD_PORT` 收内网或换外部 S3(改 `STORAGE_S3_*` 五项即可,应用零改动);上反代 TLS(Caddy/certbot + 通配证书)后把 `STORAGE_S3_PUBLIC_BASE_URL`、`HOST_ORIGIN` 切 https——server CORS 对 https 子域直接放行。
|
||||
```
|
||||
|
||||
- [ ] **步骤 4.2:README「数据与配置」小节更新**
|
||||
|
||||
卷列表行改为提及 `minio-data-prod`;并在其下追加迁移警告:
|
||||
|
||||
```markdown
|
||||
- **0.5.0 口令迁移警告**:`POSTGRES_PASSWORD` 不再有 `crearte` 默认值。既有主机的 `.env` 若没写过该行,升级后必须显式补 `POSTGRES_PASSWORD=<旧卷实际口令>`(全新 dev 卷旧口令即 `crearte`),否则 Postgres 鉴权失败。`MINIO_ROOT_USER/PASSWORD` 同理改为必填(dev 可继续用 minioadmin 值)。
|
||||
```
|
||||
|
||||
- [ ] **步骤 4.3:CHANGELOG 0.5.0**
|
||||
|
||||
`docs/CHANGELOG.md` 顶部(`## [0.4.0]` 之前)插入:
|
||||
|
||||
```markdown
|
||||
## [0.5.0] - 2026-09-29
|
||||
|
||||
### Added / 新增
|
||||
|
||||
- The prod profile gains a full write side: new `minio-prod` object storage (host port `${MINIO_PROD_PORT:-9001}`, volume `minio-data-prod`, CORS-open) wired to `api-prod` via `STORAGE_S3_*`, plus `GAMES_BASE_DOMAIN=localhost` and `CORS_ALLOWED_ORIGINS` — upload/submission/review/preview routes now register in prod instead of 404-ing. `web-prod` gained `GAMES_SERVER_NAME` (renders the templated nginx wildcard block) and `VITE_GAMES_BASE_DOMAIN`/`VITE_HOST_ORIGIN` build args for the `*.localhost` play runtime. One-time prod bucket init and the real-server TLS checklist are in README.
|
||||
- prod profile 补齐写侧全链路:新增 `minio-prod`(主机端口 `${MINIO_PROD_PORT:-9001}`、卷 `minio-data-prod`、开放 CORS),`api-prod` 配置 `STORAGE_S3_*` 与 `GAMES_BASE_DOMAIN=localhost`、`CORS_ALLOWED_ORIGINS`——上传/投稿/审核/预览路由在生产启用(此前一律 404)。`web-prod` 新增 `GAMES_SERVER_NAME`(渲染 nginx 模板通配块)与游玩运行时构建参数 `VITE_GAMES_BASE_DOMAIN`/`VITE_HOST_ORIGIN`。prod 桶一次性初始化与真服务器 TLS 清单见 README。
|
||||
|
||||
### Changed / 变更
|
||||
|
||||
- `POSTGRES_PASSWORD` lost its `crearte` fallback: the env anchor and both `DATABASE_URL`s use `${POSTGRES_PASSWORD:?…}`, so compose fails fast when missing. Old checkouts must set the line explicitly to match the volume's frozen password (see README migration warning). `MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD` became `:?`-required (empty placeholders in `.env.example`; minioadmin stays fine for local drills) and `minio-dev` now spells its defaults via the same variables.
|
||||
- `POSTGRES_PASSWORD` 去掉 `crearte` 兜底:anchor 与两条 `DATABASE_URL` 改为 `${POSTGRES_PASSWORD:?…}`,缺失即快速失败;既有部署须显式补写旧卷口令(见 README 迁移警告)。`MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD` 升为 `:?` 必填(`.env.example` 空占位;本机演练填 minioadmin 即可),`minio-dev` 默认值经同组变量显式化(行为不变)。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 5:crearte-deploy 验证——config + prod 实跑 + 冒烟链
|
||||
|
||||
**前置:** 任务 2 已完成(prod 镜像构建需要 master 上的模板 nginx)。以下命令全部在 `crearte-deploy/` 执行。
|
||||
|
||||
- [ ] **步骤 5.1:生成 .env**
|
||||
|
||||
```bash
|
||||
test -f .env && echo "ABORT: .env 已存在,停下核对而不是覆盖" || cp .env.example .env
|
||||
{
|
||||
echo "AUTH_TOKEN_SECRET=$(openssl rand -base64 32)"
|
||||
echo "BUNDLE_KEK_k1=$(openssl rand -base64 32)"
|
||||
echo "POSTGRES_PASSWORD=crearte"
|
||||
echo "MINIO_ROOT_USER=minioadmin"
|
||||
echo "MINIO_ROOT_PASSWORD=***"
|
||||
} >> .env
|
||||
sed -i '/^AUTH_TOKEN_SECRET=$/D;/^BUNDLE_KEK_k1=$/D;/^POSTGRES_PASSWORD=$/D;/^MINIO_ROOT_USER=$/D;/^MINIO_ROOT_PASSWORD=$/D' .env
|
||||
```
|
||||
|
||||
- [ ] **步骤 5.2:四 profile config 全静默**
|
||||
|
||||
```bash
|
||||
for p in dev prod debug mock; do docker compose --profile $p config -q || echo "FAIL $p"; done
|
||||
```
|
||||
|
||||
预期:无输出无 FAIL。再验守卫确实生效:`POSTGRES_PASSWORD= docker compose --profile prod config -q` 应报 required variable 错误(测后恢复)。
|
||||
|
||||
- [ ] **步骤 5.3:prod 起栈 + 健康**
|
||||
|
||||
```bash
|
||||
docker compose --profile prod up -d --build
|
||||
docker compose --profile prod ps
|
||||
```
|
||||
|
||||
预期:`api-prod healthy`、`db-prod healthy`、`web-prod`/`minio-prod` running。首拉镜像慢属正常(GOPROXY 已指 goproxy.cn)。
|
||||
|
||||
- [ ] **步骤 5.4:prod 桶初始化**(README 命令原样执行,预期 `mc mb`/`anonymous set` 成功)
|
||||
|
||||
- [ ] **步骤 5.5:冒烟链(写侧 + 游玩链路 curl 等价物)**
|
||||
|
||||
逐条执行(`ADMIN_TOKEN` 全程 shell 变量传递,**不回显、不落盘敏感响应正文**):
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
B=http://localhost:8080
|
||||
|
||||
# 1) healthz(经 web-prod 反代)
|
||||
curl -sf $B/api/games >/dev/null && echo "catalog ok"
|
||||
docker compose exec -T api-prod wget -qO- http://127.0.0.1:8080/healthz | grep -q ok && echo "healthz ok"
|
||||
|
||||
# 2) 注册 -> 提权 -> 重登(set-role 会失效旧 token,必须重登)
|
||||
curl -s -o /tmp/p1reg.json -w '%{http_code}\n' -X POST $B/api/auth/register -H 'Content-Type: application/json' \
|
||||
-d '{"email":"p1@example.com","password":"***","display_name":"P1 Smoke","username":"p1admin"}' # 201
|
||||
docker compose exec -T api-prod /app/crearte-server user set-role p1@example.com admin
|
||||
curl -s -o /tmp/p1login.json -w '%{http_code}\n' -X POST $B/api/auth/login -H 'Content-Type: application/json' \
|
||||
-d '{"email":"p1@example.com","password":"***"}' # 200
|
||||
ADMIN_TOKEN=*** -c "import json;print(json.load(open('/tmp/p1login.json'))['token'])")
|
||||
|
||||
# 3) 上传真实 zip bundle
|
||||
python3 -c "import zipfile;zipfile.ZipFile('/tmp/p1.zip','w').writestr('index.html','<h1>p1</h1>')"
|
||||
curl -s -o /tmp/p1up.json -w '%{http_code}\n' -X POST $B/api/uploads -H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-F kind=bundle -F slug=p1-smoke -F version=v1 -F file=@/tmp/p1.zip # 201
|
||||
UP_ID=$(python3 -c "import json;print(json.load(open('/tmp/p1up.json'))['upload_id'])")
|
||||
|
||||
# 4) 投稿(virtual,new_work,直提)+ external 第二件
|
||||
curl -s -o /tmp/p1sub.json -w '%{http_code}\n' -X POST $B/api/submissions -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' -d '{
|
||||
"kind":"new_work","work_id":"p1admin/p1-smoke","bundle_upload_id":"'"$UP_ID"'","submit":true,
|
||||
"payload":{"id":"p1admin/p1-smoke","runtime":"virtual","name":"P1 Smoke","url":"https://example.com",
|
||||
"author":{"name":"P1"},"description":"prod write-side smoke","durationMinutes":{"min":1,"max":60},
|
||||
"type":"puzzle","tags":["smoke"],"version":"v1"}}' # 201
|
||||
SUB_ID=$(python3 -c "import json;print(json.load(open('/tmp/p1sub.json'))['id'])")
|
||||
curl -s -o /tmp/p1ext.json -w '%{http_code}\n' -X POST $B/api/submissions -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' -d '{
|
||||
"kind":"new_work","work_id":"p1admin/p1-ext","submit":true,
|
||||
"payload":{"id":"p1admin/p1-ext","runtime":"external","name":"P1 Ext","url":"https://example.com",
|
||||
"author":{"name":"P1"},"description":"external smoke","durationMinutes":{"min":1,"max":60},
|
||||
"type":"puzzle","tags":[]}}' # 201
|
||||
|
||||
# 5) 审核上架两作品
|
||||
curl -s -o /dev/null -w 'approve1 %{http_code}\n' -X POST $B/api/admin/submissions/$SUB_ID/approve \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' -d '{"note":"smoke"}' # 200
|
||||
EXT_SUB_ID=$(python3 -c "import json;print(json.load(open('/tmp/p1ext.json'))['id'])")
|
||||
curl -s -o /dev/null -w 'approve2 %{http_code}\n' -X POST $B/api/admin/submissions/$EXT_SUB_ID/approve \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' -d '{"note":"smoke"}' # 200
|
||||
|
||||
# 6) 详情:schema v2、playSubdomain、匿名可读
|
||||
curl -s -o /tmp/p1det.json -w '%{http_code}\n' $B/api/games/p1admin/p1-smoke # 200(无 token)
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
d = json.load(open('/tmp/p1det.json'))
|
||||
assert d['runtime'] == 'virtual' and d['playSubdomain'], d
|
||||
assert d.get('bundle', {}).get('url'), '缺 bundle.url'
|
||||
print('detail ok sub=', d['playSubdomain'])
|
||||
PY
|
||||
|
||||
# 7) 通配子域:三件套 + /api 同源反代 + bundle-key(--resolve 绕 glibc 不解析 *.localhost)
|
||||
SUB=$(python3 -c "import json;print(json.load(open('/tmp/p1det.json'))['playSubdomain'])")
|
||||
for path in /__bootstrap /sw.js /agent.js; do
|
||||
curl -sf --resolve "$SUB.localhost:8080:127.0.0.1" "http://$SUB.localhost:8080$path" >/dev/null && echo "sub $path ok"
|
||||
done
|
||||
curl -s -o /tmp/p1key.json -w 'bundle-key %{http_code}\n' \
|
||||
--resolve "$SUB.localhost:8080:127.0.0.1" -H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
"http://$SUB.localhost:8080/api/games/p1admin/p1-smoke/bundle-key" # 200
|
||||
python3 -c "import json;d=json.load(open('/tmp/p1key.json'));assert d['v']==1;print('key shape ok')"
|
||||
shred -u /tmp/p1key.json 2>/dev/null || rm -f /tmp/p1key.json # 密钥材料不落盘残留
|
||||
|
||||
# 8) 跨源直连:bundle 密文匿名下载 + Origin=CORS 头(server 放行 *.localhost)
|
||||
BU=$(python3 -c "import json;print(json.load(open('/tmp/p1det.json'))['bundle']['url'])")
|
||||
curl -sf -o /tmp/p1bundle.bin "$BU" && echo "bundle object ok ($(stat -c%s /tmp/p1bundle.bin)B)"
|
||||
curl -s -D- -o /dev/null -H "Origin: http://$SUB.localhost:8080" $B/api/games/p1admin/p1-smoke/bundle-key \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" | grep -i 'access-control-allow-origin' | grep -q "$SUB.localhost" && echo "cors wildcard ok"
|
||||
curl -s -o /dev/null -w 'ext detail %{http_code}\n' $B/api/games/p1admin/p1-ext # 200
|
||||
```
|
||||
|
||||
预期全绿。任一步 4xx/5xx:按 spec §4 排查(路由未注册→查 `STORAGE_S3_*` 是否被 `LoadStorage` 判不全;子域 404→查 `GAMES_SERVER_NAME` 渲染 `docker compose exec web-prod cat /etc/nginx/conf.d/default.conf`)。
|
||||
|
||||
- [ ] **步骤 5.6:dev 回归**
|
||||
|
||||
```bash
|
||||
docker compose --profile prod down
|
||||
docker compose --profile dev up -d --build
|
||||
docker compose --profile dev exec -T api-dev wget -qO- http://127.0.0.1:8080/healthz | grep -q ok && echo dev-api ok
|
||||
docker compose --profile dev exec minio-dev sh -c "mc alias set local http://localhost:9000 minioadmin minioadmin && mc mb --ignore-existing local/crearte && mc anonymous set download local/crearte"
|
||||
curl -sf http://localhost:8080/api/games >/dev/null && echo dev-catalog ok
|
||||
docker compose --profile dev down # 无 -v
|
||||
```
|
||||
|
||||
预期全绿(dev 的 minio-dev 仍走默认凭据;`.env` 里的 minioadmin 值与 `:-` 兼容)。
|
||||
|
||||
- [ ] **步骤 5.7:浏览器人工验收(可延后,标记即可)**
|
||||
|
||||
有 GUI 的机器上打开 `http://localhost:8080`(prod 栈),走 UI 上传→投稿→后台审核→站内游玩。本机为 headless,计划以步骤 5.5 的 curl 等价链为验收线,浏览器项标 `manual-deferred`。
|
||||
|
||||
---
|
||||
|
||||
### 任务 6:crearte-deploy 合入并推送
|
||||
|
||||
- [ ] **步骤 6.1**
|
||||
|
||||
```bash
|
||||
cd crearte-deploy
|
||||
git status --short # 应只有 docker-compose.yml、.env.example、README.md、docs/CHANGELOG.md;.env 绝不入列
|
||||
git add -A && git commit -m "feat(prod): write side via minio-prod + games runtime env; drop POSTGRES_PASSWORD default
|
||||
|
||||
- minio-prod (host 9001, volume minio-data-prod) wired to api-prod
|
||||
STORAGE_S3_*/GAMES_BASE_DOMAIN/CORS_ALLOWED_ORIGINS -> upload/submission/
|
||||
review routes register in prod
|
||||
- web-prod: GAMES_SERVER_NAME template var + VITE_GAMES_BASE_DOMAIN /
|
||||
VITE_HOST_ORIGIN build args
|
||||
- POSTGRES_PASSWORD & MINIO_ROOT_* become :?-required; .env.example synced;
|
||||
README gains prod bucket init, password migration warning, TLS checklist
|
||||
- docs: CHANGELOG 0.5.0"
|
||||
git checkout master && git pull --ff-only origin master
|
||||
git merge --no-ff feat/prod-write-side -m "Merge branch 'feat/prod-write-side' — prod write-side self-contained drill (P1)"
|
||||
git push origin master
|
||||
git branch -d feat/prod-write-side && git push origin --delete feat/prod-write-side
|
||||
git log --oneline -2 # 记录 merge hash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 7:wrapper 收尾(状态、账目、计划登记)
|
||||
|
||||
- [ ] **步骤 7.1:ROADMAP 更新**
|
||||
|
||||
P1 行状态列改为:`**完成**(crearte `<任务2 merge hash>` / deploy `<任务6 merge hash>`,2026-09-29 合并推送;TLS 与真域名留清单,见 spec §6)`。文档索引表 P1 行实现计划列改为 ``docs/plans/2026-09-29-prod-write-side.md`(已执行,2026-09-29)`。
|
||||
|
||||
- [ ] **步骤 7.2:wrapper CHANGELOG 0.2.2**
|
||||
|
||||
```markdown
|
||||
## [0.2.2] - 2026-09-29
|
||||
|
||||
### Docs / 文档
|
||||
|
||||
- Roadmap P1 delivered: prod write side went live in the compose drill — minio-prod + api-prod storage/games env + templated nginx wildcard block (crearte `<>` / crearte-deploy `<>`), POSTGRES_PASSWORD default retired, TLS deferred by design with a real-server checklist in README.
|
||||
- 路线图 P1 交付:prod 写侧在本机演练栈完整启用——minio-prod、api-prod 存储/游玩域名环境、nginx 通配块模板化(crearte `<>` / crearte-deploy `<>`);POSTGRES_PASSWORD 默认值退役;TLS 按设计缓做,真机清单已入 README。
|
||||
```
|
||||
|
||||
(`<>` 处以真实 merge hash 替换;英文行紧接中文行。)
|
||||
|
||||
- [ ] **步骤 7.3:Commit + Push(wrapper 可直提 master)**
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
git add docs/ROADMAP.md docs/CHANGELOG.md docs/plans/ docs/specs/
|
||||
git commit -m "docs: P1 delivered — prod write-side drill merged; plan registered, changelog 0.2.2"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
- [ ] **步骤 7.4:终态核验**
|
||||
|
||||
```bash
|
||||
git -C crearte pull --ff-only origin master && git -C crearte log --oneline -1
|
||||
git -C crearte-deploy pull --ff-only origin master && git -C crearte-deploy log --oneline -1
|
||||
./clone_all.sh # 应输出 3 updated
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自检(写完后执行,已内联修复)
|
||||
|
||||
1. **规格覆盖度**:spec §3.1-3.2 端口/服务/env → 任务 3.2-3.5;§3.3 口令链 → 3.1/3.6 + README 4.2;§3.4 nginx 模板 + 守卫测试 → 任务 1;§3.5 桶初始化 → README 4.1 + 步骤 5.4;§4 验证四步 → 5.1-5.7(浏览器项如实标 deferred);§5 影响文件表逐项对应;§6 不做项无对应任务(正确);§7 分支/顺序 → 任务 1/2/3/6/7。✔
|
||||
2. **占位符扫描**:merge hash 占位 `<>` 为运行时事实(任务 2/6 执行时产生),其余无 TODO/待定。✔
|
||||
3. **类型/命名一致性**:`GAMES_SERVER_NAME`(web-prod env ↔ nginx 模板 ↔ 测试断言)、`PUBLIC_GAMES_HOST`/`HOST_ORIGIN`/`MINIO_PROD_PORT`/`MINIO_BUCKET`(compose ↔ .env.example ↔ README)、模板路径 `deploy/nginx.conf.template`(Dockerfile ↔ repo-yaml.test.ts ↔ dev-game-runtime 注释)三向一致。✔
|
||||
|
||||
**范围外提醒(不入本计划)**:HANDOFF §4 前端零散 backlog 与本计划无关;若冒烟 5.5 步骤 7 发现 `sw.js`/`agent.js` 在 prod dist 缺失(`nginx exec ls /usr/share/nginx/html`),那是 crearte 构建产物的独立缺陷,上报而非顺手修。
|
||||
@@ -0,0 +1,131 @@
|
||||
# P1 prod 写侧可用(本机自包含演练)设计 / Prod Write-Side Design
|
||||
|
||||
- **状态**:设计已批准(2026-09-29,方案 A + 跳过 TLS),待 spec 审阅后转实现计划。
|
||||
- **路线图**:`docs/ROADMAP.md` P1(第一波)。
|
||||
- **范围定性**:纯 compose/nginx/配置编排改动;**零后端代码改动、零前端应用代码改动**(仅 `deploy/` 资产与其守卫测试)。
|
||||
|
||||
## 1. 背景与意图
|
||||
|
||||
prod 栈的 `api-prod` 未配置任何 `STORAGE_S3_*`,server 启动时 `storageEnabled=false`,上传/投稿/审核/预览路由整体不注册——生产只读。本设计在**本机 compose 内**把 prod 栈补齐到与 dev 同等的写侧+游玩能力,作为日后迁真实服务器的定型演练。
|
||||
|
||||
已确认的两个上游决策(2026-09-29):
|
||||
|
||||
1. **形态**:本机自包含演练——MinIO 入 prod 栈,域名体系用 `*.localhost`。
|
||||
2. **TLS**:本轮跳过,全程 `http://localhost:8080`;README 记录迁真机时的 TLS 步骤。
|
||||
|
||||
成功标准(prod profile 起来后):注册→上传 bundle→投稿→审核上架→站内游玩(virtual 子域运行时 + external 外链)全链路可用;`docker compose --profile prod up -d --build` 后按 AGENTS.md 验证四步全绿。
|
||||
|
||||
## 2. 方案选型
|
||||
|
||||
| 方案 | 内容 | 取舍 |
|
||||
|---|---|---|
|
||||
| **A. 镜像 dev 直连法(已选)** | `minio-prod`(主机端口 9001),浏览器跨源直连 MinIO 拉密文/封面;nginx 通配块参数化 + 补 `/api/` 反代 | 复用 dev 已验证的跨源模式(`MINIO_API_CORS_ALLOW_ORIGIN` + `STORAGE_S3_PUBLIC_BASE_URL`);改动最小 |
|
||||
| B. nginx 同源反代 `/objects/` | 只暴露 8080,形态更近真 prod | 重写两个 server 块代理/改写 + MinIO 匿名策略变化,验证面大;上真服务器时再迁 |
|
||||
| C. 只做写侧不验游玩 | 范围最小 | P1 明文含 `GAMES_BASE_DOMAIN`,游玩链路是"写侧可用"的完整验收,砍掉不闭合 |
|
||||
|
||||
## 3. 设计详述
|
||||
|
||||
### 3.1 prod 端口与命名基线
|
||||
|
||||
- prod MinIO 主机端口 **`${MINIO_PROD_PORT:-9001}`**(避开 dev 的 9000,误起两栈时冲突面最小化);容器名 `minio-prod`,卷 `minio-data-prod`(新卷,不碰任何既有卷名)。
|
||||
- 桶名沿用 `${MINIO_BUCKET:-crearte}`;路径风格 `STORAGE_S3_FORCE_PATH_STYLE=true`;公开基址 `STORAGE_S3_PUBLIC_BASE_URL=http://localhost:${MINIO_PROD_PORT:-9001}/${MINIO_BUCKET:-crearte}`。
|
||||
|
||||
### 3.2 compose 改动(crearte-deploy/)
|
||||
|
||||
`api-prod` 补环境变量(其余不动):
|
||||
|
||||
```yaml
|
||||
depends_on:
|
||||
db-prod: { condition: service_healthy }
|
||||
minio-prod: { condition: service_started }
|
||||
environment:
|
||||
<<: *api-env
|
||||
DATABASE_URL: postgres://crearte:${POSTGRES_PASSWORD:?…}@db-prod:5432/crearte?sslmode=disable
|
||||
STORAGE_S3_ENDPOINT: http://minio-prod:9000
|
||||
STORAGE_S3_BUCKET: ${MINIO_BUCKET:-crearte}
|
||||
STORAGE_S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER:?…}
|
||||
STORAGE_S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:?…}
|
||||
STORAGE_S3_FORCE_PATH_STYLE: "true"
|
||||
STORAGE_S3_PUBLIC_BASE_URL: http://localhost:${MINIO_PROD_PORT:-9001}/${MINIO_BUCKET:-crearte}
|
||||
GAMES_BASE_DOMAIN: localhost # server 默认即 localhost,显式写出以表意;真域名时改此值
|
||||
CORS_ALLOWED_ORIGINS: ${HOST_ORIGIN:-http://localhost:8080}
|
||||
```
|
||||
|
||||
`minio-prod` 服务:镜像 `pgsty/minio:latest`,`command: ["server", "/data"]`,`restart: unless-stopped`,端口 `${MINIO_PROD_PORT:-9001}:9000`,环境 `MINIO_ROOT_USER/MINIO_ROOT_PASSWORD`(同上 `:?`)+ `MINIO_API_CORS_ALLOW_ORIGIN: "*"`,卷 `minio-data-prod:/data`。
|
||||
|
||||
`web-prod` 补两项:
|
||||
|
||||
```yaml
|
||||
build:
|
||||
args:
|
||||
VITE_API_BASE_URL: /
|
||||
VITE_GAMES_BASE_DOMAIN: ${PUBLIC_GAMES_HOST:-localhost:8080} # host:port,含端口
|
||||
VITE_HOST_ORIGIN: ${HOST_ORIGIN:-http://localhost:8080}
|
||||
environment:
|
||||
GAMES_SERVER_NAME: ${GAMES_SERVER_NAME:-*.localhost} # nginx 模板渲染变量
|
||||
```
|
||||
|
||||
> 命名注意:`PUBLIC_GAMES_HOST`(客户端用,`localhost:8080` 带端口)与 server 的 `GAMES_BASE_DOMAIN`(CORS 后缀匹配用,纯主机名 `localhost`)是两个不同值,compose 变量必须分开,防止一处改动串坏另一处。
|
||||
|
||||
### 3.3 POSTGRES_PASSWORD 去默认值(全链)
|
||||
|
||||
- `x-postgres-env` 与两条 `DATABASE_URL` 中 `${POSTGRES_PASSWORD:-crearte}` 一律改 `${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}`。
|
||||
- **数据风险与迁移**:pgdata 卷内口令已固化;现存 `.env` 没写该变量的用户必须显式补 `POSTGRES_PASSWORD=crearte` 才能保住旧卷,README「数据与配置」加一条迁移警告(dev/prod 同理)。
|
||||
- `.env.example`:`POSTGRES_PASSWORD=crearte`(dev 开箱即用)+ 注释"prod 务必改强口令";新增 `MINIO_ROOT_USER=`/`MINIO_ROOT_PASSWORD=` 空占位(注释说明 dev 可留默认 minioadmin 需显式写值、prod 必填)。注意 `.env.example` 值与 compose `:?` 守卫同步。
|
||||
|
||||
### 3.4 nginx 模板化(crearte/deploy/)
|
||||
|
||||
现状两处死点:通配块 `server_name *.games.example.com` 硬编码;通配块不提供 `/api/` 反代(子域运行时取 bundle-key 会 404——dev 靠 vite 顺带代理才没暴露)。
|
||||
|
||||
改法:
|
||||
|
||||
1. `deploy/nginx.conf` → `deploy/nginx.conf.template`,主块保持 `server_name _;`,通配块改 `server_name ${GAMES_SERVER_NAME};`。nginx 官方镜像 entrypoint(`20-envsubst-on-templates.sh`)按**已定义环境变量**渲染到 `/etc/nginx/conf.d/`,`$uri`/`$host` 等 nginx 内建变量不受影响(非环境变量)。
|
||||
2. 通配块新增 `location /api/`,逐字照抄主块的 resolver + `proxy_pass http://$api_upstream` + `Host`/`X-Forwarded-For` 写法;其余维持"三件套 + `location / { return 404; }`"骨架不变。子域页面因此**同源**取 API,bundle-key 链路不再依赖跨源。
|
||||
3. Dockerfile:`COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf` → `COPY deploy/nginx.conf.template /etc/nginx/templates/default.conf.template`。
|
||||
4. **守卫测试同步**(`src/scripts/repo-yaml.test.ts`):读取路径改 `deploy/nginx.conf.template`;通配块查找键改 `server_name ${GAMES_SERVER_NAME};`;新增断言通配块含 `location /api/` 且 proxy 指向上游写法;"no /data/ no /assets/" 断言保留。`dev-game-runtime.ts` 头注里"对齐生产 nginx 通配 server block"的语义仍然成立,不动。
|
||||
|
||||
### 3.5 一次性桶初始化(prod)
|
||||
|
||||
与 dev 同型、端口不同,README「使用」prod 小节新增一行:
|
||||
|
||||
```bash
|
||||
docker compose --profile prod exec minio-prod sh -c 'mc alias set local http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" && mc mb --ignore-existing local/crearte && mc anonymous set download local/crearte'
|
||||
```
|
||||
|
||||
**单引号是刻意的**:宿主机 shell 不展开 `$MINIO_ROOT_USER/$MINIO_ROOT_PASSWORD`(`.env` 不会自动 export 到用户 shell),由容器内 shell 用 compose 注入的同名环境变量完成展开——与 dev 版(写死 minioadmin)行为等价但不再复制凭据字面量。不引入自动初始化——保持"卷首建需手动一次性初始化"的既有约定。
|
||||
|
||||
## 4. 验证(AGENTS.md 四步,逐条执行)
|
||||
|
||||
1. `docker compose config -q` 四个 profile 全过(dev/prod/debug/mock)。
|
||||
2. `--profile prod up -d --build` + `ps` 健康全绿;跑 prod MinIO 桶初始化。
|
||||
3. 冒烟链:`localhost:8080` 注册→`docker compose exec api-prod /app/crearte-server user set-role <email> admin`→登录→上传→投稿→管理端 approve→开 `<hash>.localhost:8080` 玩 virtual 作品(bootstrap→sw→bundle-key→解密→可玩);external 作品外链正常;匿名只读目录/详情 200。
|
||||
4. 回归:dev 栈 `up -d --build` 再起一次确认 9001/9000 无端口冲突、写侧未受 POSTGRES_PASSWORD 改动影响(`.env` 补值后);`repo-yaml.test.ts` 等前端单测在容器内全绿。
|
||||
5. `down`(**绝不 `-v`**)。
|
||||
|
||||
## 5. 影响文件(预估)
|
||||
|
||||
| 仓库 | 文件 | 改动 |
|
||||
|---|---|---|
|
||||
| crearte-deploy | `docker-compose.yml` | minio-prod 服务、api-prod 补 env、web-prod 补 args/env、POSTGRES_PASSWORD 去默认、卷 minio-data-prod |
|
||||
| crearte-deploy | `.env.example` | POSTGRES_PASSWORD、MINIO_ROOT_* 占位与注释 |
|
||||
| crearte-deploy | `README.md` | prod 写侧用法、桶初始化、迁移警告、真机 TLS/域名 checklist |
|
||||
| crearte-deploy | `docs/CHANGELOG.md` | 0.5.0 条目 |
|
||||
| crearte | `deploy/nginx.conf.template`(新,替代 nginx.conf) | 通配块 server_name 参数化 + `/api/` 反代 |
|
||||
| crearte | `deploy/Dockerfile` | 模板 COPY 改路径 |
|
||||
| crearte | `src/scripts/repo-yaml.test.ts` | 读模板、断言更新 |
|
||||
| crearte | `docs/CHANGELOG.md` | 0.15.0 条目 |
|
||||
| wrapper | `docs/ROADMAP.md` | P1 状态、文档索引登记本 spec |
|
||||
|
||||
## 6. 明确不做(YAGNI / 出圈)
|
||||
|
||||
- 不做 TLS、不装反代层(真机时:Caddy/certbot + 泛解析 + 通配证书,README 记步骤即可)。
|
||||
- 不动 server/前端应用代码;不引 nginx `/objects/` 同源反代(方案 B,留待真服务器)。
|
||||
- 不改任何卷名;不新增 compose 全局网络拓扑。
|
||||
- P1 的"密码默认值"仅指 `POSTGRES_PASSWORD` 链(S3 root 凭据入 `:?` 见 3.2/3.3);`AUTH_TOKEN_SECRET`/`BUNDLE_KEK_k1` 已有守卫,不动。
|
||||
|
||||
## 7. 分支与提交
|
||||
|
||||
- crearte:`feat/prod-nginx-wildcard-api`,完成后 `--no-ff` 合入 master 并删分支。
|
||||
- crearte-deploy:`feat/prod-write-side`,同上。
|
||||
- wrapper:spec/ROADMAP/CHANGELOG 记录可直提 master。
|
||||
- 提交顺序:crearte(模板+测试)→ crearte-deploy(compose+文档,验证依赖前者)→ wrapper 收尾。
|
||||
Reference in New Issue
Block a user