diff --git a/docs/40-deployment/production-registry.md b/docs/40-deployment/production-registry.md index 7552fc8..cb9e474 100644 --- a/docs/40-deployment/production-registry.md +++ b/docs/40-deployment/production-registry.md @@ -1,19 +1,31 @@ # 生产 Registry 与 PostgreSQL 切换 -公开 origin 保持 `https://registry.inkcre.dev`。迁移目标是一个运行于 Heroku Eco dyno 的 CPython / FastAPI 服务容器、Registry 专属 Neon PostgreSQL 项目和既有私有 R2 桶。代码实现不等于生产切换;旧生产仍由 Worker 与 D1 服务,直至获得明确生产授权并完成以下步骤。当前资源身份、演练结果及切换状态记录在活动 task packet。 +公开 origin 保持 `https://registry.inkcre.dev`。2026-09-13 起,由一个运行于 Heroku Eco dyno 的 CPython / FastAPI 服务容器、Registry 专属 Neon PostgreSQL 项目和既有私有 R2 桶提供服务。Web、API、静态资源和原生分发由同一镜像交付。旧 Worker / D1 已退出公开路由,保留为本次迁移的恢复依据。 `production.yml` 只接受精确 current-main SHA。`verify` 执行仓库检查和容器构建,不修改远程资源。`deploy` 在受保护 production 环境中,重新核验 main 后对已配置 app 向前迁移、配置数据库和单桶 S3 凭据、发布同一镜像,并验证 Heroku app origin。它不自动导入 D1、不改变域名、不创建示例数据,也不删除旧资源。 production 环境需要 `HEROKU_APP_NAME`、`S3_ENDPOINT_URL`、`S3_BUCKET` variables,以及 `HEROKU_API_KEY`、`MIGRATION_DATABASE_URL`、`DATABASE_URL`、`AWS_ACCESS_KEY_ID`、`AWS_SECRET_ACCESS_KEY` secrets。首次配置保持既有 R2 桶,S3 token 仅授予该桶的对象读写。`MIGRATION_DATABASE_URL` 使用 `registry_owner`,仅交给迁移容器与可信角色配置命令;`DATABASE_URL` 使用同一数据库的普通 `registry_app` 角色和独立密码。迁移创建该角色及业务表授权,控制器设置密码后以 `web=1:eco` 启动应用。owner 连接与平台控制 token 不进入运行服务配置。数据库连接属于独立 Registry 项目,不复用 core-py 的数据库或发布生命周期。 +Heroku 交付为 Uvicorn 设置 `FORWARDED_ALLOW_IPS=*`,由平台 HTTP 入口提供外部请求协议,补斜杠跳转保持 HTTPS。转发头不参与 namespace 授权或身份判断;认证仍由 publisher credential 决定。部署 smoke 同时检查 `/simple` 跳转到该 origin 的 HTTPS `/simple/`。 + +## 生产资源与 TLS + +Registry 使用 Heroku app `inkcre-ext-reg-production`,其默认 origin 为 `https://inkcre-ext-reg-production-a6bbc8ada2d2.herokuapp.com`;web formation 为一个 Eco dyno。Neon 项目为 `wandering-base-13707928`,数据库 `registry` 位于根分支 `br-muddy-term-aw4iive7`。R2 使用原有私有桶 `inkcre-extension-registry-production-v2`。PR 分支从固定空 `preview-base` 创建,生产导入不改变这个基础分支。 + +Cloudflare zone `inkcre.dev` 中的 `registry.inkcre.dev` 使用代理 CNAME,目标为 Heroku 为该域名分配的 `classical-mapusaurus-sbqqxbde9bay1zrkw4vh1zhp.herokudns.com`。这个 hostname 的 Configuration Rule 设置 `ssl=strict`,Single Redirect Rule 将 HTTP 以 308 转向 HTTPS,并保留路径和查询参数;不修改其他 hostname 的 TLS 配置。 + +Registry 的 Cache Rule 对此 hostname 设置 `cache=true`、`edge_ttl.mode=bypass_by_default`、`browser_ttl.mode=respect_origin`、`origin_cache_control=true` 与 `respect_strong_etags=true`。页面和 API 的 `no-store` 禁止缓存;文件的 `public, no-cache` 要求边缘和浏览器每次回源重验证,不覆盖为固定 TTL。这样 release 的可读状态仍由应用在每次请求中检查,ETag 匹配时返回 304,边缘复用已经验证的字节。Cloudflare 默认 Browser Cache TTL 会把部分静态后缀改成四小时缓存,不能沿用这个默认值。切换后按 hostname 清除 Registry 缓存,不清除整个 zone。 + +Heroku SNI endpoint `gallimimus-68400` 绑定了仅覆盖 `registry.inkcre.dev` 的 Cloudflare Origin CA 证书,到期时间为 **2041-09-09 06:29 UTC**。公开客户端校验 Cloudflare 边缘证书,Cloudflare 校验 Heroku 的 Origin CA 证书;Heroku 默认域名仍使用平台的公开证书。此 app 不启用 ACM。Origin CA 不自动续期或发送到期通知,运维应在到期前签发新证书、用 `heroku certs:update` 替换并核验域名映射;私钥只保存在受限恢复目录与 Heroku 证书存储。保持 DNS 代理开启,关闭代理会将 Origin CA 证书直接暴露给不信任它的浏览器。参见 [Cloudflare Origin CA](https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/) 与 [Heroku SSL](https://devcenter.heroku.com/articles/ssl)。 + ## 首次切换 1. 明确来源 D1、目标 PostgreSQL、R2 桶、Heroku app、公开路由和获准变更窗口。先验证空库迁移、快照导入及整个发布流程;保存旧 Worker 版本与现有路由配置。 -2. 暂停旧入口的发布写入并等待在途请求结束。停止 Web、Toolkit、CI publisher 等所有写入来源;只读分发可以继续。不要在旧入口仍接收写入时进行最终导入,也不要引入临时双写。 +2. 暂停旧入口的发布写入并等待在途请求结束。本次使用 hostname 级临时 WAF 规则,拒绝 `registry.inkcre.dev` 的非 GET / HEAD / OPTIONS 请求,并实测规则生效;旧 Worker 的 workers.dev 和版本预览入口均关闭。停止 Web、Toolkit、CI publisher 等所有写入来源;只读分发可以继续。不要在旧入口仍接收写入时进行最终导入,也不要引入临时双写。 3. 使用 Wrangler 的 `d1 export --remote --output ` 保存最终快照。来源 ID 显式传入,不从已删除的 Worker 配置猜测。限制快照文件权限;它包含凭据哈希和私有元数据。用 SQLite 将这份受信任导出转换成临时 SQLite 文件,执行完整性与外键检查。原始 token 从不进入快照。 4. 对空目标运行 `pnpm db:migrate`,然后运行 `pdm run python -m inkcre_extension_registry.import_d1 `。导入器拒绝非空目标,在同一事务中转换内部 ID 并核验每张表全部逻辑字段的规范化内容摘要;任何失败回滚全部导入。旧时间戳、空值、JSON、凭据哈希、release 状态及 R2 key 都参与比较。 5. 再运行同一命令并加 `--verify-only`。保存仅含行数和摘要的报告;抽查既有 wheel、Core Metadata、MF manifest / assets 的实际 R2 内容与公开身份。导入不移动、重写或公开 R2 对象。旧 `migrations/` 是来源 schema 历史,包内 Tortoise 迁移是新库 schema 历史。 -6. 启动已验证镜像,先通过 app origin 验证只读目录、详情、私有认证和原生协议。将 `registry.inkcre.dev` 指向新服务并配置证书,验证缓存、ETag、GET / HEAD、Simple 内容协商及下载。确认旧 Worker 不再接收发布写入后,恢复新服务的发布能力。 +6. 启动已验证镜像,先通过 app origin 验证只读目录、详情、私有认证和原生协议。预先完成 Heroku 域名及回源证书配置并校验证书与主机名,再移除 Worker Custom Domain、创建上述代理 CNAME。按 hostname 清除旧缓存,等待路由收敛,并验证缓存、ETag、GET / HEAD、Simple 内容协商及下载。再次导出旧 D1,确认隔离后摘要仍与导入来源相等,随后移除临时 WAF 写入暂停规则,恢复新服务的发布能力。 7. 保存最终来源快照、导入摘要、镜像 revision、数据库迁移位置及路由验收。旧 D1 / Worker 的删除须在约定保留窗口后另行执行,不能以 PR 合并代替资源删除授权。 ## 故障恢复 @@ -23,3 +35,9 @@ production 环境需要 `HEROKU_APP_NAME`、`S3_ENDPOINT_URL`、`S3_BUCKET` vari 新库尚未接受写入时,可以恢复旧路由与旧 Worker。新库已经接受写入后,旧 D1 不再完整;此时先停止新写入并保全两侧数据,通过修复新服务或经核验的数据合并恢复。直接回退旧 Worker 不能声称无损恢复。 PostgreSQL 事务不覆盖 R2。中断上传可能留下不可达 staging 对象;公开可见性仍由数据库关联和 release 状态决定。后续修复追加迁移,不回写已部署历史,也不使用 `--fake` 掩盖 schema 与迁移记录不一致。 + +## 本次迁移的恢复坐标 + +旧 Worker 为 `inkcre-extension-registry`,保留版本 `ef4f71a1-a596-4b91-a33c-e78835e5b015`;旧 D1 为 `af52114f-55b5-476f-b986-8ff8d8601d77`。它们不再承接公开流量,不作为新 PostgreSQL 写入后的实时副本。删除旧生产资源仍需单独约定保留窗口和授权。 + +最终 SQL / SQLite 快照、逐表逻辑摘要、R2 内容摘要、旧路由与 Worker 设置、回源证书私钥等恢复材料存放在执行机的 `~/.local/state/inkcre/ext-reg/production-cutover-2026-09-13/`。目录权限为 0700,文件为 0600;其中含凭据哈希和私钥,不得上传到 Git 或公开日志。生产运行凭据由 GitHub production environment 和 Heroku app 配置持有,owner 连接仅保留在交付权限边界内。 diff --git a/docs/40-deployment/pull-request-previews.md b/docs/40-deployment/pull-request-previews.md index 2a8018a..7d415cf 100644 --- a/docs/40-deployment/pull-request-previews.md +++ b/docs/40-deployment/pull-request-previews.md @@ -8,7 +8,7 @@ PR 预览使用 `Dockerfile` 构建的 CPython 服务镜像,覆盖目录、扩 可信控制器使用 Docker / Heroku CLI 交付镜像,使用官方 REST API 配置分支、app 和 R2 凭据。REST 调用只覆盖 CLI 不适合保密结构化配置的部分,数据库业务仍由包内 ORM 命令执行。角色密码使用交付端 psycopg 的 SQL 字面量编码设置;Neon 不接受 libpq 预先散列的密码。psycopg 只属于开发和交付依赖,不进入服务镜像。Neon 分支密码更换与 R2 桶授权分别约束数据库和对象存储的访问范围。 -Heroku 始终配置 `web=1:eco`。Eco 的休眠与唤醒由平台管理;健康探测允许启动等待,不在页面中加入环境提示或改变产品流程。 +Heroku 始终配置 `web=1:eco` 和 Uvicorn 的 `FORWARDED_ALLOW_IPS=*`,让平台 HTTP 入口传递外部协议。部署 smoke 会检查 `/simple` 的补斜杠跳转仍保持当前 origin 的 HTTPS;转发头不用于 publisher 授权。Eco 的休眠与唤醒由平台管理;健康探测允许启动等待,不在页面中加入环境提示或改变产品流程。 GitHub `preview` 环境需要: diff --git a/scripts/registry_preview.py b/scripts/registry_preview.py index 6bd0272..221b64c 100644 --- a/scripts/registry_preview.py +++ b/scripts/registry_preview.py @@ -78,6 +78,9 @@ def smoke(origin: str, revision: str) -> None: assert client.get("/livez").json() == {"status": "ok", "revision": revision} for path in ("/", "/publish", "/v1/extensions", "/simple/"): client.get(path).raise_for_status() + redirect = client.get("/simple") + assert redirect.status_code == 307 + assert redirect.headers["location"] == f"{origin}/simple/" assert client.get("/v1/publisher").status_code == 401 return except (httpx.HTTPError, AssertionError, ValueError): @@ -200,6 +203,8 @@ def deploy(heroku, neon, cloudflare, name: str, branch_name: str, image: str, re ) config.update( PUBLIC_ORIGIN=origin, + # Heroku's router is the dyno's HTTP ingress; preserve the external scheme. + FORWARDED_ALLOW_IPS="*", S3_BUCKET=name, S3_ENDPOINT_URL=f"https://{os.environ['CLOUDFLARE_ACCOUNT_ID']}.r2.cloudflarestorage.com", # The image owns revision; a config change must not relabel an old process. diff --git a/scripts/registry_production.py b/scripts/registry_production.py index a351433..4af4ae2 100644 --- a/scripts/registry_production.py +++ b/scripts/registry_production.py @@ -54,7 +54,10 @@ def main() -> None: ) } config.update( - PUBLIC_ORIGIN=origin, REGISTRY_SOURCE_REVISION=None, MIGRATION_DATABASE_URL=None + PUBLIC_ORIGIN=origin, + FORWARDED_ALLOW_IPS="*", + REGISTRY_SOURCE_REVISION=None, + MIGRATION_DATABASE_URL=None, ) request(heroku, "PATCH", f"apps/{app}/config-vars", json=config) run( diff --git a/tasks/registry-web-lovable/browser-acceptance/catalog-empty-mobile.png b/tasks/registry-web-lovable/browser-acceptance/catalog-empty-mobile.png deleted file mode 100644 index ac5d7be..0000000 Binary files a/tasks/registry-web-lovable/browser-acceptance/catalog-empty-mobile.png and /dev/null differ diff --git a/tasks/registry-web-lovable/browser-acceptance/detail-dark-mobile.png b/tasks/registry-web-lovable/browser-acceptance/detail-dark-mobile.png deleted file mode 100644 index 2e25a8b..0000000 Binary files a/tasks/registry-web-lovable/browser-acceptance/detail-dark-mobile.png and /dev/null differ diff --git a/tasks/registry-web-lovable/browser-acceptance/detail-desktop.png b/tasks/registry-web-lovable/browser-acceptance/detail-desktop.png deleted file mode 100644 index 7f89d96..0000000 Binary files a/tasks/registry-web-lovable/browser-acceptance/detail-desktop.png and /dev/null differ diff --git a/tasks/registry-web-lovable/browser-acceptance/publisher-mobile.png b/tasks/registry-web-lovable/browser-acceptance/publisher-mobile.png deleted file mode 100644 index fbdd40c..0000000 Binary files a/tasks/registry-web-lovable/browser-acceptance/publisher-mobile.png and /dev/null differ diff --git a/tasks/registry-web-lovable/browser-acceptance/remote-detail-dark-mobile.png b/tasks/registry-web-lovable/browser-acceptance/remote-detail-dark-mobile.png deleted file mode 100644 index 2e25a8b..0000000 Binary files a/tasks/registry-web-lovable/browser-acceptance/remote-detail-dark-mobile.png and /dev/null differ diff --git a/tasks/registry-web-lovable/browser-acceptance/remote-detail-desktop.png b/tasks/registry-web-lovable/browser-acceptance/remote-detail-desktop.png deleted file mode 100644 index 7f89d96..0000000 Binary files a/tasks/registry-web-lovable/browser-acceptance/remote-detail-desktop.png and /dev/null differ diff --git a/tasks/registry-web-lovable/browser-acceptance/remote-preview-empty.png b/tasks/registry-web-lovable/browser-acceptance/remote-preview-empty.png deleted file mode 100644 index e7ac6e0..0000000 Binary files a/tasks/registry-web-lovable/browser-acceptance/remote-preview-empty.png and /dev/null differ diff --git a/tasks/registry-web-lovable/browser-acceptance/remote-preview-publisher.png b/tasks/registry-web-lovable/browser-acceptance/remote-preview-publisher.png deleted file mode 100644 index 3148f8d..0000000 Binary files a/tasks/registry-web-lovable/browser-acceptance/remote-preview-publisher.png and /dev/null differ diff --git a/tasks/registry-web-lovable/browser-acceptance/remote-publisher-desktop.png b/tasks/registry-web-lovable/browser-acceptance/remote-publisher-desktop.png deleted file mode 100644 index 43a5416..0000000 Binary files a/tasks/registry-web-lovable/browser-acceptance/remote-publisher-desktop.png and /dev/null differ diff --git a/tasks/registry-web-lovable/evidence.md b/tasks/registry-web-lovable/evidence.md deleted file mode 100644 index bec39ea..0000000 --- a/tasks/registry-web-lovable/evidence.md +++ /dev/null @@ -1,122 +0,0 @@ -# 现有证据与迁移验证 - -本文件属于 [Registry 父任务](packet.md),拥有跨实现阶段的证据和适用范围。Worker / D1 部分是实施前历史基线;CPython / PostgreSQL 的当前结果见文末实施记录。生产切换仍无完成证据。 - -## Worker / D1 基线 - -Registry 分支为 `feat/registry-web-lovable`,实施前提交 `03de36b`,最初基线为 `422a6cb`。开始实施前只有 packet 调查记录未提交。 - -已交付的 Web 对齐 client-web:实际几何 logo、普通字号、直角边框与紧凑布局,复用 design 已有公开 Sass package。目录有搜索和发布者筛选;详情有精确版本及原生分发;Publisher 支持准备、上传、发布、撤回、恢复及私有分页。环境信息不塑造产品页面、控件和 URL。 - -现有 PR 预览是完整 Python Worker,配套独立 D1 / R2。部署保留数据,关闭时按 PR 清理资源。发布代码与可信部署控制器分开处理;源提交检查、凭据隔离和清理边界是需要保留的交付结果,Worker 具体机制不是新运行时必须继承的方案。 - -### Demo 清理结果 - -已移除部署时创建示例 Release 的代码,以及样例详情 / manifest 专用检查;删除 66 行,没有增加产品机制。PR #33 预览中已清理 `demo/github` 的 `1.0.0`、`1.10.0`、`2.0.0-rc.1`,以及 `demo/memos`、`demo/rss`、`demo/notes` 各 `1.0.0`,合计 4 个扩展、6 个版本、12 个精确 R2 对象。 - -清理前核对 source repository / revision、snapshot hash 与 asset paths;未重置数据库。D1 UUID `7d67ae5e-c0b3-4fca-841c-4c4c7661dd73` 保持不变,namespace、凭据和迁移记录数保持不变,外键检查通过。旧 demo 详情返回 404,空目录与 Publisher 登录、空 release 列表已验证。该结论只适用于当时的预览状态,不能据此假定生产为空。 - -### SQLAlchemy Core 交付结果 - -当时的 `service/database.py` 和 `service/repository.py` 使用 SQLAlchemy Core 表定义、查询表达式及 SQLite 编译器;D1 binding 执行编译出的 SQL 和绑定参数,原生 `batch()` 保持关联创建的原子性。没有 ORM Session、关系加载或实体变更跟踪。 - -此前替换了 Repository 的 27 处 SQL 及预览凭据维护 SQL。SQLAlchemy 2.0.52 的 pure-Python wheel 在当前 Pyodide Worker 中运行,PDM 与 Worker lock 对齐。已检查的 `sqlalchemy-cloudflare-d1` 0.3.11 使用同步 `run_sync`,Worker 路径的 commit / rollback 为空操作;这是当时没有接入其 ORM 事务的原因,不是对全部 D1 工具的普遍结论。 - -`scripts/check_registry.py` 使用一次性本地真实 Worker / D1 / R2,验证完整发布、幂等、私有分页、namespace 范围及关联写入故障后扩展和版本一同回滚,已纳入 `pnpm check`。完整检查曾通过,具体最终 CI / 部署证据见 [ext-reg PR #33](https://github.com/InKCre/ext-reg/pull/33)。这些断言中的业务不变量需迁到真实 PostgreSQL 上重新证明。 - -### 旧预览交付状态 - -历史预览地址为 `https://inkcre-ext-reg-pr-33.lanzhijiang.workers.dev/`。最近的代码更新保留既有 D1 / R2 和 Publisher 凭据,没有初始化凭据或上传验收数据。 - -旧流程中 GitHub preview 环境已配置 `REGISTRY_PREVIEW_PUBLISHER_TOKEN`,`CLOUDFLARE_PREVIEW_API_TOKEN` 尚未配置;远程预览由本机 Wrangler 登录执行同一控制器更新。控制器进入 main 后才能自动触发,但本任务尚未合并或修改生产资源。迁移方向已改变,不应为了旧流程而默认补齐这个前置条件。 - -配套 [.github PR #32](https://github.com/InKCre/.github/pull/32) 基于 Worker 预览编写;其适用内容须在交付方案中重新审阅。本记录不宣布该 PR 自动作废,也不代替修改或关闭它的实际操作。 - -## 选型与复用依据 - -已核对现有 D1 schema:`releases`、两类 distribution 和 `python_files` 使用多列身份及复合外键。这是 ORM 比较必须覆盖的实际关系;可以提出代理主键方案,但必须保留业务唯一性和对外身份,不能为了通过库限制而静默弱化约束。 - -当前候选调查发现:Tortoise 1.x 已有内置迁移,但仅支持单列主键;Peewee 4.x 已有异步扩展和迁移生成,指向复合主键的外键仍受限;Django 6.0 的普通 ForeignKey 仍不能引用复合主键模型;Edgy 对复合主键与外键组合仍有成熟度提醒。SQLModel、Ormar、Edgy 基于 SQLAlchemy,API 风格不同不等于底层驱动独立。Piccolo 对当前关系的完整支持尚未确认。调查时尚没有候选在本项目完成过 ORM 实现验收。 - -来源:[Tortoise 模型](https://tortoise.github.io/models.html)与[迁移](https://tortoise.github.io/migration.html)、[Peewee 模型](https://docs.peewee-orm.com/en/latest/peewee/models.html)与[异步](https://docs.peewee-orm.com/en/latest/peewee/asyncio.html)、[Django 复合主键](https://docs.djangoproject.com/en/6.0/topics/composite-primary-key/)、[Edgy 模型](https://edgy.dymmond.com/models/)、[Piccolo](https://github.com/piccolo-orm/piccolo)。后续选型时复核最终采用版本,不将这些调查冻结为长期事实。 - -core-py 的 `app/engine.py`、`docs/40-deployment/neon.md` 和 `heroku.md` 已核对:当前使用 SQLModel / SQLAlchemy 与 psycopg,常规 Python OCI 镜像在 Heroku 上运行,Neon 已有连接池、迁移及隔离预览经验。其当前预览从去除应用数据的 `preview-base` 创建 PR 分支;这仅是复用依据,不自动成为 Registry 的新要求。core-py 源码和资源未变更。 - -Cloudflare 的 PostgreSQL 接入主要展示 JavaScript 驱动;仅换 Neon 不证明 Python Worker 的 ORM 驱动可用。官方 Django / D1 后端注明 `transaction.atomic` 不生效。因此已确认将常规 CPython 一并纳入迁移。R2 可通过标准 S3 SDK 使用,但本项目仍需验证现有对象访问与返回语义。 - -来源:[Cloudflare PostgreSQL](https://developers.cloudflare.com/hyperdrive/examples/connect-to-postgres/)、[Python 包](https://developers.cloudflare.com/workers/languages/python/packages/)、[Django 后端事务](https://developers.cloudflare.com/workers/languages/python/packages/django/)、[R2 boto3](https://developers.cloudflare.com/r2/examples/aws/boto3/)。 - -## 证据边界 - -| 主张 | 当前证据边界 | 应在哪一步获得 | -| --- | --- | --- | -| ORM 关系、事务和迁移 | Tortoise 在真实 Neon 的发布验收已通过 | 最终 CI 持续验证 | -| CPython 单一产物 | 同一镜像的 CI、Eco 部署及远程浏览器验收已通过 | 02 CI 镜像、03 远程预览 | -| D1 导入保真 | 一次性 SQLite → Neon 演练已通过,真实生产来源尚未核验 | 04 最终来源核验 | -| 新预览隔离、更新保留数据、无部署 demo | 真实隔离、重部署保留数据和临时资源清理已通过 | 03 预览生命周期验证 | -| 公开协议、视觉、权限和失败语义保持 | CPython / Neon / R2 协议与 Chromium 验收已通过 | 02 行为检查、03 浏览器与协议验收 | -| 生产已使用 Neon,旧写入路径已停止 | 没有生产变更或切换证据 | 04 获准切流及观察 | - -后续每次更新记录具体提交 / 产物、目标环境、执行结果及剩余事项。生产数据、原始凭据、数据库连接串与导出文件不得写入 packet、Git 或测试日志。 - -## 2026-09-13 CPython / PostgreSQL 实施 - -- `pnpm registry:check` 已在 Neon 一次性分支 `br-dry-meadow-awo759yq` 通过完整发布流程。使用 Tortoise 实际迁移、真实 PostgreSQL 事务、FastAPI ASGI 请求以及本地 Moto S3 服务;覆盖 Python / MF 上传与重试、发布、撤回恢复、关联失败回滚、并发关联冲突、分页、namespace / blocked 边界、原始文件与 GET / HEAD。 -- D1 导入验收已单独通过:临时 SQLite 使用原始 `0001_registry.sql`,包含全部七张表和四种 release 状态。导入后全部字段的规范化摘要一致,包含凭据哈希、空值、JSON、时间戳与 R2 key;非空目标拒绝导入。来源为一次性 fixture,不是生产数据。 -- 静态类型、Ruff 和公开契约检查已通过;公开 OpenAPI / schema 无变化。最终完整仓库检查与导入失败回滚检查仍在收尾,最终结果以之后记录为准。 -- 新建独立 Neon 项目 `wandering-base-13707928`,数据库 `registry`,PostgreSQL 17 / aws-us-east-1。空基础分支 `preview-base` 为 `br-polished-mode-awi5dixy`,无 compute;根分支、PR 33 分支与测试分支见 packet。没有修改 core-py 的项目或数据。 -- GitHub preview 已配置 `NEON_API_KEY`(限制于 Registry 项目)、`NEON_PROJECT_ID` 和 `NEON_PREVIEW_PARENT_BRANCH_ID`。该环境已有审阅者 token,但缺少新的 Cloudflare 控制 token 与 Heroku 凭据;本机 Heroku 登录也已失效。已发出所需输入请求,尚未部署 CPython 远程预览。 -- 生产 D1、R2、Worker 和 `registry.inkcre.dev` 未变更。旧 Worker 预览不能证明新运行时通过验收。 - -- 最终验收发现 Tortoise 1.1 初始 CreateModel 忽略 Meta.constraints,PostgreSQL 实际目录中缺少 CHECK。初始迁移尚未提交或部署到预览 / 生产;现已改为 15 个原生 AddConstraint 操作。只重建一次性测试分支的 schema,再从空库执行完整检查;旧 D1 历史、根分支和 PR 分支未变更。 - -- 最终本地 `pnpm check` 全部通过,使用 Python 3.13 / Node 22.22.3 与 Neon PostgreSQL 17;覆盖实际 CHECK 拒绝非法导入并回滚、S3 元数据对象缺失时拒绝发布、修复后发布成功。公开契约 / runtime 生成绑定无 diff,design 样式比对、格式、静态类型、全部独立包构建通过。SVC 14.0 healthy。 -- Registry 项目范围的 Neon key 查询本项目返回 200,查询 core-py 项目返回 404;GitHub 仓库级没有额外 Heroku / R2 secrets 可复用。配套治理提交为 `dafd33c`,已推送并更新 PR #32。容器实际构建 / 启动与 CI 的最终结果以 PR #33 的同提交 checks 为准。 - -- 原生 `tortoise upgrade` 已成功将初始迁移应用到 PR 33 分支;PostgreSQL 目录确认 15 个 CHECK,namespace / extension / release 均为 0。没有 demo 或审阅者初始化数据;运行时部署待凭据完成。该初始迁移自此冻结,后续只追加。 - -- `7ea304e` 的 [CI](https://github.com/InKCre/ext-reg/actions/runs/34736306427) 全部通过,包含依赖审查、PostgreSQL 17 行为验收、容器构建与实际 HTTP 启动。后续收尾给健康探测增加镜像内的 revision 校验;新提交的结果以 PR checks 为准。 -- PR 33 与一次性测试分支均通过正式 Neon API 完成独立角色密码轮换,控制器的分支查询、操作等待与连接 URI 读取已在真实 API 验证;这些密码不再继承根分支。Heroku / R2 部署仍未验证。 - -## 2026-09-13 浏览器验收与远程阻塞复核 - -源码提交 `4d7987488865cf3f17f3fd6f47769112b698c470` 的 [CI](https://github.com/InKCre/ext-reg/actions/runs/34736919622) 全部通过,镜像实际启动检查包含构建提交号匹配。本轮没有修改产品源码或部署实现。 - -本地实际启动 CPython / Uvicorn,经 Chromium 的真实 HTTP 与页面操作完成验收。数据库为一次性 Neon 分支 `br-dry-meadow-awo759yq`,对象存储为本地 Moto S3;没有拦截浏览器请求或替换页面数据。该结果证明页面与当前服务、PostgreSQL 的组合行为,不证明真实 R2 或远程容器平台已通过。 - -- 空目录、无效凭据反馈、Publisher 登录和空工作区通过。 -- 使用 Publisher 表单准备、上传 ZIP 并发布 `1.0.0` 和 `1.1.0`;上传前发布按钮禁用。 -- 目录进入详情、版本切换、复制 ID、manifest 与文件 GET / HEAD 通过。 -- 撤回后版本退出详情列表,恢复后重新出现;搜索无结果、发布者筛选、键盘搜索快捷键及退出登录通过。 -- 桌面 `1365 × 950` 和手机 `390 × 844` 检查没有页面横向溢出;主题切换与重载保持通过。观察期间没有页面脚本异常或 HTTP 5xx。 - -已检查并保存[空目录手机截图](browser-acceptance/catalog-empty-mobile.png)、[详情桌面截图](browser-acceptance/detail-desktop.png)、[详情深色手机截图](browser-acceptance/detail-dark-mobile.png)和 [Publisher 手机截图](browser-acceptance/publisher-mobile.png)。截图中的 `acceptance/browser-check` 只存在于一次性测试分支;验收服务停止时已删除对应凭据及所有测试业务行,并停止本地 Moto。清理后分别读取测试分支和共享 PR 33,确认两者的七张业务表均为空。 - -本机 Heroku 登录仍无效,GitHub preview 仍缺 `HEROKU_API_KEY` 与 `CLOUDFLARE_PREVIEW_API_TOKEN`;已补齐 `CLOUDFLARE_ACCOUNT_ID` variable。托管平台选择另待 Sir 确认。没有部署新的远程 CPython 服务,没有修改生产资源,也没有将旧 Worker 预览记作本次验收。 - -## 2026-09-13 Eco 预览与缓存、权限验证 - -`fe483609e06d04b71216135149691b1e841e0097` 已部署到 [PR 33 CPython 预览](https://inkcre-ext-reg-pr-33-02f4786fe565.herokuapp.com)。Heroku API 确认 web formation 为 Eco、quantity 为 1;`/livez` 返回相同 SHA。[完整 CI](https://github.com/InKCre/ext-reg/actions/runs/34740527800) 通过,包括依赖审查、真实 PostgreSQL 17 上从空库迁移与完整业务验收、镜像构建与 HTTP 启动。 - -普通 `registry_app` 角色通过了真实 Neon 发布、并发、D1 导入保真及 ETag / blocked 检查;创建表、修改业务表、删除迁移记录及创建角色均被拒绝。PR Heroku 配置只含普通应用连接和单桶 S3 凭据,不含 owner 连接及云平台控制令牌。PR 桶对象数为 0,其 S3 凭据读取独立验收桶返回 403。PR 只登记 reviewer namespace 和凭据,无扩展、版本或文件。 - -临时 app `inkcre-ext-reg-qa-33`、Neon `br-plain-wildflower-awu0s6yy` 与同名 R2 桶执行了完整远程 HTTP 发布演练,使用 Eco、真实 Neon 普通账号和 R2;包含双类型上传、幂等与并发冲突、发布、撤回恢复、缺失对象、条件 GET / HEAD 和 blocked 拒绝。该环境仅用于本次验收,结束后删除。 - -浏览器检查复现了脚本加载竞态:在 defer 脚本尚未返回时提交 Connect,浏览器会进行原生 GET 导航。使用无效 canary 证实查询参数携带输入内容;此前验收用过的 PR 凭据已轮换,旧凭据返回 401,新凭据返回 200。受影响资产是审阅凭据,路径是浏览器 URL / 路由日志;没有证据显示凭据被第三方使用,也不涉及生产或云平台控制令牌。修复移除输入框的原生表单字段名,并在提交处理器绑定后才启用 Connect;用延迟和阻断脚本加载的 Chromium 检查验证,不增加产品提示或新的部署流程。 - -## 远程浏览器与生命周期验收完成 - -功能验收最终提交为 `a86549309726c713f1ea10e6f6636c42ffcddeff`,[对应完整 CI](https://github.com/InKCre/ext-reg/actions/runs/34741279081) 全部通过。本机通过仓库 Dockerfile 构建 linux/amd64 镜像,manifest digest 为 `sha256:db7322cb0051c55f22cea5f0418cf5582249feb9631501edae3370ee5ebb6583`;Heroku `/livez` 确认相同源码 SHA,两个实例均为一个 Eco dyno。 - -真实 Chromium 先验证 PR 的空目录、reviewer 登录空态、移动布局和断开,再在临时环境完成两个 Web 版本的准备、ZIP 上传和发布。详情与版本切换、复制 ID、manifest、原始文件 GET / HEAD、主题持久化、移动端无横向溢出、撤回恢复、搜索和 publisher 筛选均通过,没有页面异常或 HTTP 5xx。浏览器检查等待复制完成提示后再读取剪贴板。延迟和阻断脚本加载验证了 Connect 不进行原生凭据导航;延迟加载也证明复制和主题按钮在处理器绑定后才可操作。 - -携带双版本数据重新部署临时实例后,公开 ExtensionRecord 全部内容与部署前相等,包括时间戳、provenance 和文件身份;新版本仍能读取实际 R2 内容。之后通过仓库 cleanup 控制器停止并删除临时 app、全部桶对象、桶及对象 token、Neon 分支,API 确认 app / bucket / branch 均不存在。单独的迁移检查分支也已删除。这里验证的是控制器真实资源生命周期;GitHub closed-PR 触发条件仍由工作流检查,它须在可信默认分支落地后启用。 - -截图中的扩展仅是已删除的临时验收记录,未写入共享 PR 预览: - -- [共享预览空目录](browser-acceptance/remote-preview-empty.png)与[reviewer 空工作区](browser-acceptance/remote-preview-publisher.png)。 -- [扩展详情与版本](browser-acceptance/remote-detail-desktop.png)、[暗色移动详情](browser-acceptance/remote-detail-dark-mobile.png)与[Publisher 双版本](browser-acceptance/remote-publisher-desktop.png)。 - -本机补充检查已通过。曾有一次本地完整命令在临时 PostgreSQL 镜像尚未拉取完成时连接失败;此前格式、契约、类型与构建阶段均通过,随后相同源码的 GitHub PostgreSQL 17 完整检查通过。镜像拉取的另一次 TLS 超时在原命令重试后恢复,没有因此改变产品实现或降低验收范围。 - -生产仍未变更,真实生产 D1 导入与公开域名切换不属于本次预览通过的结论。自动预览尚待合并可信控制器,本次发布使用其同一实现从本机执行。 diff --git a/tasks/registry-web-lovable/packet.md b/tasks/registry-web-lovable/packet.md deleted file mode 100644 index 376ad97..0000000 --- a/tasks/registry-web-lovable/packet.md +++ /dev/null @@ -1,48 +0,0 @@ -# Registry Web:产品完善与 Python / PostgreSQL 迁移 - -## 目标与当前状态 - -完成 Registry 的 minimum lovable product:目录、扩展详情和 Publisher 对齐 client-web,复用 design 的公开样式包;由一个 Python 服务交付 Web、API 和静态资源。服务从 Python Workers / D1 迁到常规 CPython / Neon PostgreSQL,保留 R2 与公开协议,减少特殊运行时适配和两套数据库技术的维护成本。 - -**2026-09-13,Sir 已接受 Registry Web 的能力与界面达到 MLP,明确授权继续推进直至生产部署完成,仍使用 Heroku Eco。** 单一 CPython 服务、普通数据库角色、文件重验证及浏览器就绪修复已交付。完整 CI、真实 Eco / Neon / R2 协议、浏览器与重部署保留数据验收已通过;临时验收资源已清理。生产尚未切换。 - -本 packet 是父任务唯一的工作控制入口,保存目标、决策、执行位置和证据边界,不拥有长期架构事实,也不代替 Sir 的验收。[计划](plan.md)拥有推进顺序;[证据](evidence.md)区分当前结果、旧 Worker 基线与待验证事项。父任务关闭前保留整个 packet。 - -## 决策、当前实现与授权 - -| 对象 | From → To | 不变量 | -| --- | --- | --- | -| 运行时 | Python Worker → CPython / FastAPI / Jinja 单一容器,Heroku Eco dyno | Web、API、分发与静态资源同一应用,无独立前端部署 | -| 持久化 | D1 / SQLAlchemy Core → Neon PostgreSQL / Tortoise ORM / asyncpg | 业务唯一身份、namespace 边界、发布状态及不可变关联保持兼容 | -| 关系与迁移 | 复合主键 → Release、Distribution、File 内部主键与普通外键,业务身份用唯一约束保持 | 内部 ID 不进入公开契约;旧 D1 历史不改写,新迁移仅向前执行 | -| 文件存储 | R2 binding → boto3 S3 SDK | 原有 key、文件身份、流式读取与先 staging 后可见的语义保持 | -| 预览 | 独立 Heroku Eco app / Neon branch / 私有 R2 bucket | 更新保留数据,不生成示例扩展,不复制生产业务数据 | -| 产品 | 保留已接受的简洁目录、详情与 Publisher,复用 design | 无 hero、额外教程面板、错误 logo 或环境驱动的产品分支 | - -此前“恢复原始 SQL”不再是当前路线。Tortoise 使用独立关联主键,避免库对外键兼主键的加载限制;没有自建驱动、查询语言或事务模拟。R2 与 PostgreSQL 不组成分布式事务。导入在一次数据库事务中转换关系,并比较全部逻辑内容摘要,不能只比较行数。 - -实现、验证、提交、推送和 PR 更新沿用明确授权,不重复请求。Sir 本轮已提供仓库 AGENTS.md 要求的生产明确授权,包含完成部署所需的 PR 合并、资源配置、最终迁移和公开路由切换。保持旧 D1 / Worker 与私有最终快照作为恢复依据,不在本轮直接删除旧生产数据。 - -## 归属与范围 - -持久化和发布设计归 `docs/30-unit-tdd/registry-control-plane.md`;Web 与 design 消费归 `registry-web.md`;运行、迁移、交付和恢复归 `docs/40-deployment/`。源码、模型、迁移、配置与 CI 拥有可执行事实。Registry、Toolkit、Host SDK 与 first-party Extensions 保持独立发布;不修改 core-py 业务数据或合并其发布周期。 - -配套 `.github` PR #32 中的 Worker / D1 假设随实现更新。未直接修改 Spoke 的 `docs/_shared/`;本次内部运行时与部署细节由 Registry Unit 文档及组织交付约定承担。 - -## 执行位置与下一步 - -当前进入计划 04 生产切换;[PR 33 预览](https://inkcre-ext-reg-pr-33-02f4786fe565.herokuapp.com) 已运行单个 Eco dyno。功能验收提交为 `a86549309726c713f1ea10e6f6636c42ffcddeff`;后续证据提交只更新文档和截图,发布后以 `/livez` 核对最终 PR head。完整结果和截图见[证据](evidence.md)。 - -应用使用普通 `registry_app`,owner 仅用于交付迁移;文件每次先检查 release 状态,再依据 ETag 返回内容或 304。真实浏览器暴露的脚本就绪竞态也已修复:Connect、复制 ID 和主题按钮等待事件处理器;凭据输入没有原生表单字段名。已轮换验收中使用过的 PR 凭据,旧凭据被拒绝。 - -GitHub preview 的 Heroku、Cloudflare、Neon 与 reviewer 凭据及变量均已配置。Cloudflare 控制 token 只有当前账户的 R2 存储与账户 token 管理权限;应用只拿所属桶的对象 token。主工作流的可信控制器尚未合入默认分支,本次通过本机执行仓库同一 Dockerfile 和控制器完成 pre-merge 验收,没有增加产品交付流程。 - -Registry Neon 项目为 `wandering-base-13707928`。根分支 `br-muddy-term-aw4iive7` 保持空库,无 compute 的空基础分支为 `br-polished-mode-awi5dixy`;PR 33 使用 `br-falling-breeze-awj9y9ao`。PR 只登记 reviewer namespace 和凭据,没有扩展、版本或文件。临时远程验收 app / bucket `inkcre-ext-reg-qa-33`、Neon `br-plain-wildflower-awu0s6yy` 和迁移检查分支 `br-dry-meadow-awo759yq` 已删除并确认不存在。 - -当前获准执行生产切换:核验真实来源、停止旧写入、导入并核对最终快照、切换公开路由。操作及恢复条件已在 `production-registry.md` 准备好。本轮起点尚未合并 PR,也未修改生产 D1 / R2 / Worker / 公开域名。先核对资源和 TLS 路由,再完成真实快照演练;全部准备就绪后暂停旧写入,导入最终快照并核验,然后切流。父任务保留到生产行为与最终验收完成。 - -## 工作坐标 - -- Registry 工作树:`/Volumes/WorkSSD/Development/InKCre/.worktrees/registry-web/ext-reg`;分支 `feat/registry-web-lovable`;[PR #33](https://github.com/InKCre/ext-reg/pull/33)。 -- [旧 Worker 预览](https://inkcre-ext-reg-pr-33.lanzhijiang.workers.dev/)仍代表 `03de36b`,不是本次 CPython 迁移的验收地址。 -- 治理工作树:`/Volumes/WorkSSD/Development/InKCre/.worktrees/registry-web/governance`;分支 `feat/registry-worker-preview`;[PR #32](https://github.com/InKCre/.github/pull/32)。 diff --git a/tasks/registry-web-lovable/plan.md b/tasks/registry-web-lovable/plan.md deleted file mode 100644 index 4ce9cb4..0000000 --- a/tasks/registry-web-lovable/plan.md +++ /dev/null @@ -1,27 +0,0 @@ -# 执行计划 - -本计划属于 [Registry 父任务](packet.md),只拥有推进顺序,不另建任务控制入口。 - -## 01 方案与授权(完成) - -Sir 已确认 CPython / Neon 方向并授权实施。采用 Tortoise 的异步 ORM、asyncpg 和内置迁移,独立内部主键配合业务唯一约束;FastAPI / Jinja 与 boto3 在同一 CPython 服务中运行。当前部署实现使用 OCI / Heroku,Registry 拥有独立 Neon 项目和 R2 存储权限。Sir 已重新确认完整部署方案与普通应用角色、文件缓存调整,要求使用 Heroku Eco dyno。 - -不引入 Django、不自写模板渲染器,不恢复 D1 手写业务 SQL,不复制生产数据到预览。生产切换采用明确的暂停写入、最终快照、导入核验和切流窗口,不建设双写同步系统。长期事实已经写入源码和 Unit / Deployment 文档。 - -## 02 服务实现与本地验收(完成) - -实现已覆盖连接生命周期、ORM 关系和事务、状态锁、S3 staging 与流式读取、GET / HEAD、单一容器入口、运维凭据和 D1 导入。公开契约和现有视觉资源保持不变。真实 Neon 发布流程与临时 D1 快照导入分别通过。 - -最终本地 `pnpm check` 已通过。PostgreSQL 17 CI 与完整容器构建由同一提交的 PR checks 记录;交付前必须确认其结果。真实数据库的失败回滚、并发冲突、私有分页、blocked 读取和导入保真必须继续通过;旧 Worker 的通过结果不能替代它们。 - -## 03 迁移演练、远程预览与 PR(完成) - -完整 CI、镜像和远程 Eco / Neon / R2 验收已通过。共享 PR 33 预览保持真实空态,只登记 reviewer 凭据;发布演练在独立临时环境完成。验收覆盖双版本 Publisher、详情与版本选择、剪贴板、明暗主题、窄屏、搜索、撤回恢复、原生协议、普通数据库角色、ETag / blocked,以及重部署完整保留数据。 - -临时 app、Neon 分支、桶和对象凭据已由同一 cleanup 控制器清理;迁移检查分支也已删除。浏览器发现的脚本加载竞态已修复,并通过延迟与失败加载验证。收尾提交记录证据与截图,随后将同一最终 head 更新到预览并更新两个 PR;可信自动预览等待控制器进入默认分支。 - -## 04 获准切换并关闭父任务(进行中,已获授权) - -Sir 已接受 MLP 并明确授权推进到生产部署完成。按获准方案保全来源、停止旧写入、导入并核验、切换公开路由、恢复新写入;验证实际消费者和分发内容。 - -旧 D1 / Worker 按明确保留窗口处理,删除另需授权。只有代码、生产行为、文档和跨仓库约定一致且父任务实际验收后,才清理整个 packet;不因某个子步骤或 PR 完成就删除它。