Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1f1a454e7 | |||
| d779245e09 | |||
| 968e7ff353 | |||
| 28b9fb1464 |
@@ -6,15 +6,10 @@ on:
|
|||||||
- 'apps/python/**'
|
- 'apps/python/**'
|
||||||
- 'scripts/deploy-python.sh'
|
- 'scripts/deploy-python.sh'
|
||||||
- '.gitea/workflows/python.yml'
|
- '.gitea/workflows/python.yml'
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'apps/python/**'
|
|
||||||
- 'scripts/deploy-python.sh'
|
|
||||||
- '.gitea/workflows/python.yml'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify-and-deploy:
|
verify-and-deploy:
|
||||||
|
if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push'
|
||||||
runs-on: ces-control
|
runs-on: ces-control
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
steps:
|
steps:
|
||||||
@@ -24,5 +19,5 @@ jobs:
|
|||||||
- name: Build and verify python
|
- name: Build and verify python
|
||||||
run: docker build -f apps/python/Dockerfile -t "prompt-python:${{ gitea.sha }}" .
|
run: docker build -f apps/python/Dockerfile -t "prompt-python:${{ gitea.sha }}" .
|
||||||
- name: Release python
|
- name: Release python
|
||||||
if: gitea.ref == 'refs/heads/main' && (gitea.event_name == 'push' || gitea.event_name == 'workflow_dispatch')
|
if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push'
|
||||||
run: bash scripts/deploy-python.sh "${{ gitea.sha }}"
|
run: bash scripts/deploy-python.sh "${{ gitea.sha }}"
|
||||||
|
|||||||
@@ -13,22 +13,10 @@ on:
|
|||||||
- 'content/studio-examples/**'
|
- 'content/studio-examples/**'
|
||||||
- 'content/studio-tools/layouts-api.md'
|
- 'content/studio-tools/layouts-api.md'
|
||||||
- 'pnpm-workspace.yaml'
|
- 'pnpm-workspace.yaml'
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'apps/studio/**'
|
|
||||||
- 'scripts/deploy-studio.sh'
|
|
||||||
- 'scripts/verify-studio-production.sh'
|
|
||||||
- 'scripts/verify-studio-library.mjs'
|
|
||||||
- 'scripts/verify-studio-visual.cjs'
|
|
||||||
- '.gitea/workflows/studio.yml'
|
|
||||||
- 'scripts/*studio*'
|
|
||||||
- 'content/studio-examples/**'
|
|
||||||
- 'content/studio-tools/layouts-api.md'
|
|
||||||
- 'pnpm-workspace.yaml'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify-and-deploy:
|
verify-and-deploy:
|
||||||
|
if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push'
|
||||||
runs-on: ces-control
|
runs-on: ces-control
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
steps:
|
steps:
|
||||||
@@ -44,10 +32,10 @@ jobs:
|
|||||||
- name: Build and verify studio
|
- name: Build and verify studio
|
||||||
run: docker build -f apps/studio/Dockerfile -t "prompt-studio:${{ gitea.sha }}" .
|
run: docker build -f apps/studio/Dockerfile -t "prompt-studio:${{ gitea.sha }}" .
|
||||||
- name: Release studio
|
- name: Release studio
|
||||||
if: gitea.ref == 'refs/heads/main' && (gitea.event_name == 'push' || gitea.event_name == 'workflow_dispatch')
|
if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push'
|
||||||
run: bash scripts/deploy-studio.sh "${{ gitea.sha }}"
|
run: bash scripts/deploy-studio.sh "${{ gitea.sha }}"
|
||||||
- name: Verify the deployed Studio API and native pages
|
- name: Verify the deployed Studio API and native pages
|
||||||
if: gitea.ref == 'refs/heads/main' && (gitea.event_name == 'push' || gitea.event_name == 'workflow_dispatch')
|
if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push'
|
||||||
run: bash scripts/verify-studio-production.sh
|
run: bash scripts/verify-studio-production.sh
|
||||||
- name: Save server acceptance evidence
|
- name: Save server acceptance evidence
|
||||||
id: acceptance_evidence
|
id: acceptance_evidence
|
||||||
|
|||||||
@@ -12,21 +12,10 @@ on:
|
|||||||
- 'scripts/auth-admin.sh'
|
- 'scripts/auth-admin.sh'
|
||||||
- 'scripts/integration.sh'
|
- 'scripts/integration.sh'
|
||||||
- 'pnpm-workspace.yaml'
|
- 'pnpm-workspace.yaml'
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'apps/waterfall-wiki/**'
|
|
||||||
- '.gitea/workflows/waterfall-wiki.yml'
|
|
||||||
- 'Dockerfile'
|
|
||||||
- 'compose.yml'
|
|
||||||
- 'scripts/deploy.sh'
|
|
||||||
- 'scripts/infra.sh'
|
|
||||||
- 'scripts/auth-admin.sh'
|
|
||||||
- 'scripts/integration.sh'
|
|
||||||
- 'pnpm-workspace.yaml'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify-and-deploy:
|
verify-and-deploy:
|
||||||
|
if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push'
|
||||||
runs-on: ces-control
|
runs-on: ces-control
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
steps:
|
steps:
|
||||||
@@ -47,5 +36,5 @@ jobs:
|
|||||||
WATERFALL_WIKI_TEST_IMAGE: waterfall-wiki-tests:${{ gitea.sha }}
|
WATERFALL_WIKI_TEST_IMAGE: waterfall-wiki-tests:${{ gitea.sha }}
|
||||||
run: bash scripts/integration.sh
|
run: bash scripts/integration.sh
|
||||||
- name: Deploy waterfall-wiki
|
- name: Deploy waterfall-wiki
|
||||||
if: gitea.ref == 'refs/heads/main' && (gitea.event_name == 'push' || gitea.event_name == 'workflow_dispatch')
|
if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push'
|
||||||
run: bash scripts/deploy.sh "${{ gitea.sha }}"
|
run: bash scripts/deploy.sh "${{ gitea.sha }}"
|
||||||
|
|||||||
@@ -49,8 +49,11 @@ async function main() {
|
|||||||
const buildId = readFileSync('.next/BUILD_ID','utf8').trim();
|
const buildId = readFileSync('.next/BUILD_ID','utf8').trim();
|
||||||
assert.equal((await request('/')).status,200,'Public knowledge homepage');
|
assert.equal((await request('/')).status,200,'Public knowledge homepage');
|
||||||
assert.equal((await request('/account')).status,200,'Public account page');
|
assert.equal((await request('/account')).status,200,'Public account page');
|
||||||
|
const homeData = await request(`/_next/data/${buildId}/index.json`);
|
||||||
|
assert.ok([200,404].includes(homeData.status),'Public homepage data is readable or absent for static pages');
|
||||||
|
assert.ok(!isLoginRedirect(homeData.headers.get('location') || homeData.headers.get('x-nextjs-redirect'),base));
|
||||||
assert.equal((await request('/api/posts',{method:'POST',headers:{Cookie:'wiki_account='+ 'a'.repeat(64)},body:'{}'})).status,401,'Account cookies never grant admin rights');
|
assert.equal((await request('/api/posts',{method:'POST',headers:{Cookie:'wiki_account='+ 'a'.repeat(64)},body:'{}'})).status,401,'Account cookies never grant admin rights');
|
||||||
for (const path of ['/library/import','/skills/shadow-puppet/egret-example.png','/studio',`/_next/data/${buildId}/index.json`]) {
|
for (const path of ['/library/import','/skills/shadow-puppet/egret-example.png','/studio',`/_next/data/${buildId}/library/import.json`]) {
|
||||||
const response = await request(path); assert.equal(response.status,307,path); const destination = response.headers.get('location') || response.headers.get('x-nextjs-redirect'); assert.ok(isLoginRedirect(destination,base),path+' must redirect to login: '+destination);
|
const response = await request(path); assert.equal(response.status,307,path); const destination = response.headers.get('location') || response.headers.get('x-nextjs-redirect'); assert.ok(isLoginRedirect(destination,base),path+' must redirect to login: '+destination);
|
||||||
}
|
}
|
||||||
for (const path of ['/api/feed','/api/upload','/studio/api/export/test','/api/media/missing']) assert.equal((await request(path)).status,401,path);
|
for (const path of ['/api/feed','/api/upload','/studio/api/export/test','/api/media/missing']) assert.equal((await request(path)).status,401,path);
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ test('proxy protects APIs, data routes, static content and Studio; fails closed'
|
|||||||
const temp=mkdtempSync(join(tmpdir(),'prompt-auth-')); const old=process.env.AUTH_CONFIG_FILE;
|
const temp=mkdtempSync(join(tmpdir(),'prompt-auth-')); const old=process.env.AUTH_CONFIG_FILE;
|
||||||
try{
|
try{
|
||||||
const config=newAuthConfig(); process.env.AUTH_CONFIG_FILE=join(temp,'config.json'); writeFileSync(process.env.AUTH_CONFIG_FILE,JSON.stringify(config));
|
const config=newAuthConfig(); process.env.AUTH_CONFIG_FILE=join(temp,'config.json'); writeFileSync(process.env.AUTH_CONFIG_FILE,JSON.stringify(config));
|
||||||
for(const path of ['/library/import','/skills/example.png','/_next/data/build/index.json','/studio']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path);
|
for(const path of ['/library/import','/skills/example.png','/_next/data/build/library/import.json','/studio']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path);
|
||||||
for(const path of ['/api/upload','/studio/api/export','/api/feed']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,401,path);
|
for(const path of ['/api/upload','/studio/api/export','/api/feed']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,401,path);
|
||||||
for(const path of ['/','/write','/documents','/api/documents','/api/documents/preview','/guide/welcome','/protocol','/api/guides/welcome','/account','/api/account/session','/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path);
|
for(const path of ['/','/_next/data/build/index.json','/write','/documents','/api/documents','/api/documents/preview','/guide/welcome','/protocol','/api/guides/welcome','/account','/api/account/session','/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path);
|
||||||
const headers={cookie:'prompt_session='+signSession(config),host:'wiki.test'};
|
const headers={cookie:'prompt_session='+signSession(config),host:'wiki.test'};
|
||||||
assert.equal(proxy(new NextRequest('http://wiki.test/api/feed',{headers})).status,200);
|
assert.equal(proxy(new NextRequest('http://wiki.test/api/feed',{headers})).status,200);
|
||||||
assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://evil.test'}})).status,403);
|
assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://evil.test'}})).status,403);
|
||||||
|
|||||||
@@ -4,16 +4,18 @@
|
|||||||
|
|
||||||
`ces-control` 必须预装 Docker、Compose,并允许 Runner 访问持久 Docker daemon。CI 只检查环境,不安装系统包、不启停主机服务。当前服务器没有 buildx,先继续使用 daemon 自带层缓存;不要直接粘贴依赖 GitHub cache 服务的 `type=gha` 参数。后续若换 Docker daemon 或清理缓存,会重新冷构建。
|
`ces-control` 必须预装 Docker、Compose,并允许 Runner 访问持久 Docker daemon。CI 只检查环境,不安装系统包、不启停主机服务。当前服务器没有 buildx,先继续使用 daemon 自带层缓存;不要直接粘贴依赖 GitHub cache 服务的 `type=gha` 参数。后续若换 Docker daemon 或清理缓存,会重新冷构建。
|
||||||
|
|
||||||
Dockerfile 保持单次 COPY,`.dockerignore` 排除非构建输入;根 Markdown 和 docs 不再使应用构建层失效。push 和 PR 对纯文档改动使用 paths,但 public 下的 Markdown 属于产品资源,必须构建部署。public 下的技能 Markdown 继续纳入构建。旧 builder 每条 COPY 实测约 47 秒,避免拆成多条 COPY 增加快照成本。完整路径保留类型检查、单元测试、smoke 与完整隔离栈集成测试。
|
Dockerfile 保持单次 COPY,`.dockerignore` 排除非构建输入;根 Markdown 和 docs 不再使应用构建层失效。只有 main 的 push 事件触发 CI/CD(对应 PR 合并后的主线更新),工作分支 push、PR 创建/更新和手动 dispatch 均不触发;三个应用均有同样的作业级守卫。main 对纯文档改动使用 paths,但 public 下的 Markdown 属于产品资源,必须构建部署。public 下的技能 Markdown 继续纳入构建。旧 builder 每条 COPY 实测约 47 秒,避免拆成多条 COPY 增加快照成本。完整路径保留类型检查、单元测试、smoke 与完整隔离栈集成测试。
|
||||||
|
|
||||||
当前页面为 Next.js SSR 与编译组件,改 TSX 仍需 next build。Web 快速路径只编译生产 Web,并以不可变的版本目录挂载完整 standalone 产物,保持版本与回滚一致;不重新编译 worker 或导出运行镜像。
|
当前页面为 Next.js SSR 与编译组件,改 TSX 仍需 next build。Web 快速路径只编译生产 Web,并以不可变的版本目录挂载完整 standalone 产物,保持版本与回滚一致;不重新编译 worker 或导出运行镜像。
|
||||||
|
|
||||||
## Web 快速发布
|
## Web 快速发布
|
||||||
|
|
||||||
main 的 push 与当前已部署 SHA 比较;只有页面、组件、样式、技能文档数据和 public 改动才走 Web 路径,API、共享库、依赖、配置、worker、脚本及数据库变化均回退完整流水线。PR 和手动运行始终完整验证;依赖构建镜像缺失也回退完整流水线。
|
main 的 push 与当前已部署 SHA 比较;只有页面、组件、样式、技能文档数据和 public 改动才走 Web 路径,API、共享库、依赖、配置、worker、脚本及数据库变化均回退完整流水线。PR 阶段只做不会触发编译的阅读、差异审查与轻量配置检查,完整远程验证在用户手动合并到 main 后执行;依赖构建镜像缺失也回退完整流水线。
|
||||||
|
|
||||||
Web 路径复用当前 runtime 对应的已验证 integration 镜像作为工具环境,在临时容器构建生产 Web,不运行 docker build。编译缓存按基础镜像隔离持久保存。类型检查、单元测试和 Web smoke 仍同步执行,完整隔离栈测试只在全量路径执行。
|
Web 路径复用当前 runtime 对应的已验证 integration 镜像作为工具环境,在临时容器构建生产 Web,不运行 docker build。编译缓存按基础镜像隔离持久保存。类型检查、单元测试和 Web smoke 仍同步执行,完整隔离栈测试只在全量路径执行。
|
||||||
|
|
||||||
完整 standalone 产物存入 `/opt/prompt/web-artifacts/<sha>.<run>`,只读挂载到 Web 容器;仅重建 Web,保留 runtime 镜像、worker、Postgres 和 RustFS。切换前检查 main 与当前发布,部署锁与全量流程共用;失败自动恢复旧 Web Compose 配置。发布文件保留变量表达式,不写入生产秘密。健康检查通过后切换 current;原完整发布也能回滚到 Web 快速发布。
|
完整 standalone 产物存入 `/opt/prompt/web-artifacts/<sha>.<run>`,只读挂载到 Web 容器;仅重建 Web,保留 runtime 镜像、worker、Postgres 和 RustFS。切换前检查 main 与当前发布,部署锁与全量流程共用;失败自动恢复旧 Web Compose 配置。发布文件保留变量表达式,不写入生产秘密。健康检查通过后切换 current;原完整发布也能回滚到 Web 快速发布。
|
||||||
|
|
||||||
首次接入流水线变更仍走完整验证。2026-10-23 workflow 291 实测:Web 构建与发布91秒,拉取代码44秒,合计2分15秒,不包含共享 Runner 排队。运行镜像复用,worker、Postgres、RustFS 容器 ID 均不变;编译缓存已持久化。不能承诺 HMR 的毫秒级反馈。每次发布完整 Web 产物,不把个别 chunk 混入正在运行的 Next.js 版本。
|
首次接入流水线变更仍走完整验证。2026-10-23 workflow 291 实测:Web 构建与发布91秒,拉取代码44秒,合计2分15秒,不包含共享 Runner 排队。运行镜像复用,worker、Postgres、RustFS 容器 ID 均不变;编译缓存已持久化。不能承诺 HMR 的毫秒级反馈。每次发布完整 Web 产物,不把个别 chunk 混入正在运行的 Next.js 版本。
|
||||||
|
|
||||||
|
工作流变更不会自动取消旧配置已创建的运行。历史 PR 排队任务须单独取消,不能将其当作新触发规则仍然生效的证据。
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
- 邮箱会话仅由新接口校验,不授予管理员 API 权限。发布是公开行为;目前没有草稿服务、私有文档、恢复撤下或账户删除功能。tags 是发现元数据,通过 XML 可重复的 tag 元素往返保存。
|
- 邮箱会话仅由新接口校验,不授予管理员 API 权限。发布是公开行为;目前没有草稿服务、私有文档、恢复撤下或账户删除功能。tags 是发现元数据,通过 XML 可重复的 tag 元素往返保存。
|
||||||
- 数据库通过行锁分配下一版本并拒绝过时编辑,触发器禁止修改/删除已发布内容。数据库所有者仍可执行数据库维护,触发器不是对数据库管理员的安全隔离。
|
- 数据库通过行锁分配下一版本并拒绝过时编辑,触发器禁止修改/删除已发布内容。数据库所有者仍可执行数据库维护,触发器不是对数据库管理员的安全隔离。
|
||||||
|
|
||||||
验证由 ces-control PR CI 执行:严格 XML 单元测试;PostgreSQL 真实发布、所有权、并发版本冲突、不可变内容和撤下保留历史的集成测试。PR 不自动部署,服务器视觉验收须主 chat 人工合并后的部署成功再执行。
|
用户手动合并到 main 后,由 ces-control CI 执行验证:严格 XML 单元测试;PostgreSQL 真实发布、所有权、并发版本冲突、不可变内容和撤下保留历史的集成测试。PR 不自动部署,服务器视觉验收须用户手动合并且部署成功后再执行。
|
||||||
|
|
||||||
首页现已通过 `/api/documents` 加载真实发布摘要,并在 GuidanceShell 提供用户文档与发布入口。`/guide` 继续只读取静态协议样例,不覆盖主 chat 的阅读实现。
|
首页现已通过 `/api/documents` 加载真实发布摘要,并在 GuidanceShell 提供用户文档与发布入口。`/guide` 继续只读取静态协议样例,不覆盖主 chat 的阅读实现。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user