ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenSEO Alchemy 预览部署实践:Cloudflare Access 共享边界、PR 暂存环境到生产割接的完整链路

OpenSEO Alchemy 预览部署实践:Cloudflare Access 共享边界、PR 暂存环境到生产割接的完整链路 OpenSEO Alchemy 预览部署实践Cloudflare Access 共享边界、PR 暂存环境到生产割接的完整链路【免费下载链接】open-seoOpen source alternative to Semrush and Ahrefs项目地址: https://gitcode.com/GitHub_Trending/op/open-seoOpenSEO一个开源的 Semrush/Ahrefs 替代项目使用 Alchemy 将预览、生产与自托管统一部署为一套 Alchemy v2 栈每个预览 stage 拥有隔离的 Cloudflare 资源而所有预览共享一个持久的、按邮箱放行的 Cloudflare Access 安全边界。读完本篇你可以掌握如何一次性部署该 Access 门、如何在本地和 GitHub Actions 中创建/销毁open-seo-stage预览环境、如何安全地预览来自外部 fork 的 PR以及第一次用 Alchemy 接管adopt生产资源前的完整检查清单。整套机制的权威说明位于 docs/PREVIEW_DEPLOYMENTS.md本文在该文档基础上结合仓库源码逐项展开。一、Stage 模型与两个 Alchemy 栈OpenSEO 的部署体系围绕stage阶段组织。所有非生产 stage 都会获得一套带 stage 后缀的全新资源唯一的例外是hosted-prod它指向 openseo.so 线上资源的既有名字让--adopt导入而不是新建。这个 stage 名被刻意取为hosted-prod而非prod这样自托管用户的 stage 名永远不会与 adoption 路径冲突——这一点在 alchemy.access.ts 中由常量HOSTED_PROD_STAGE固定下来注释里也明确解释了动机// The one stage that adopts openseo.sos live hosted resources (unsuffixed // names, app.openseo.so domain, Postgres). Deliberately not prod so a // self-hosters stage name cant collide with the adoption path. export const HOSTED_PROD_STAGE hosted-prod; export const workerName (stage: string) stage HOSTED_PROD_STAGE ? WORKER_PREFIX : ${WORKER_PREFIX}-${stage}; export const previewWildcard (subdomain: string) ${WORKER_PREFIX}-*.${subdomain};这套命名是安全边界的地基预览 Worker 一律叫open-seo-stage从open-seo-stage.WORKERS_SUBDOMAIN提供服务而生产 Worker 是不带后缀的open-seo域名app.openseo.so/www.app.openseo.so不匹配open-seo-*这个通配符Cloudflare Access 的每个点分段只允许一个通配符因此天然落在预览 Access 门之外。仓库中有两个职责分离的 Alchemy 栈文件alchemy.run.ts主部署栈负责 Worker、D1、R2、KV、Durable Objects、Workflows以及生产的 Hyperdrive。alchemy.preview-access.run.ts独立的持久 Access 栈创建并维护那个保护open-seo-*.WORKERS_SUBDOMAIN的 Access 应用。把 Access 边界拆成独立 stack 正是文档强调的安全模型核心某个预览部署失败或某个 stage 被销毁都不可能移除保护其他预览的那道门。alchemy.preview-access.run.ts 开头的注释直接点明了这一设计意图并补充了一个容易被忽略的细节——Cloudflare 的 version preview URLversion-open-seo-stage.sub位于该通配符之外但 alchemy 上传每个版本时has_preview: false因此这类 URL 根本不会被服务不存在绕过。主栈的资源命名规则可以从 alchemy.run.ts 的makeResources中确认非生产 stage 的 D1/R2/KV 分别命名为open-seo-db-stage、open-seo-r2-stage、open-seo-kv-stage生产则采用 wrangler 时代的历史物理名D1open-seo、R2open-seo、KVevery-super-seo、OAUTH_KV、Hyperdriveopenseoadoption 就是按这些精确名字匹配的。非生产 stage 的 R2 还带一条生命周期规则dataforseo-cache/前缀下的 DataForSEO 缓存对象 7 天后过期删除。二、Cloudflare Access 共享边界持久 Access 栈创建一个 self-hosted 类型的 Access 应用其公网 hostname 为open-seo-*.your-subdomain.workers.dev这里的子域必须填Workers Pages页签显示的账号 Workers 子域而不是 Zero Trust 团队域。在 alchemy.access.ts 中readWorkersSubdomain会对该值做形状校验非空时必须以.workers.dev结尾否则整个部署直接失败。这个值写入.env.preview的WORKERS_SUBDOMAIN预览 URL 由此推导为https://open-seo-stage.WORKERS_SUBDOMAIN托管模式预览用它作为BETTER_AUTH_URLCI 的验证步骤也探测它——填错会在部署后的验证环节立刻暴露。local_noauth或cloudflare_access模式可以不填因为这两种模式下没有任何代码读取BETTER_AUTH_URL。Access 策略由 alchemy.access.ts 中的emailAccessGate统一构造一个 decision 为allow的邮箱策略加一个 self-hosted 应用。ACCESS_ALLOWED_EMAILS通过requireAllowedEmails解析为逗号分隔的精确邮箱列表未设置时栈会带明确的补救提示失败。仓库提供的.env.preview.example中相关配置形如AUTH_MODElocal_noauth # WORKERS_SUBDOMAINyour-subdomain.workers.dev # ACCESS_ALLOWED_EMAILSyouyourdomain.com部署该门只需一条命令它始终使用--adopt所以即使本地 Alchemy 状态丢失也能把匹配的既有基础设施找回——随时可以安全重跑pnpm preview:access对应 package.json 中的脚本是pnpm alchemy deploy alchemy.preview-access.run.ts --env-file .env.preview --stage preview-access --adopt。与之相对普通预览销毁只会销毁被请求的 application stage碰不到 Access 栈。三、凭据管理本地登录、State Store 与 CI SecretsAlchemy 自己管理 Cloudflare 凭据——env 文件里不出现任何看起来像凭据的东西。分三个层面本地。执行一次pnpm alchemy login对Customize OAuth scopes?回答 yes在默认 scope 之上额外开启access:write预览 Access 门需要它如果之后要部署生产——Hyperdrive 需要——再加query_cache:write。凭据全局存储后续包括非交互式的运行都会静默复用。State。状态存放在账号的 Cloudflare state store 中一个内嵌 SQLite 的alchemy-state-storeWorker被每台机器和 CI 共享用pnpm alchemy cloudflare bootstrap一次性开通它会向账号 Secrets Store 签发一个 auth token 和加密密钥本地运行则在~/.alchemy/下缓存凭据。alchemy.run.ts 中state: Cloudflare.state()一行即指向该 store并附有CI 每次运行从 Secrets Store 获取 auth token的注释。GitHub Actions。runner 上的CI环境变量让 alchemy 改从环境读取CLOUDFLARE_API_TOKEN和CLOUDFLARE_ACCOUNT_IDrepo secrets并在每次运行时从 Secrets Store 解析 state-store token。该 token 需要的权限为Workers Scripts、KV、D1、R2、Workflows 的写权限外加Secrets Store read和Account Settings read后两者是 alchemy state-store 登录所用——它通过一个临时的 edge-preview worker 获取自己的 token。注意CI 从不触碰 Access那道门是一次性的本地设置。alchemy.run.ts 还给出了一条实用的权限排错提示遇到权限错误时重新执行pnpm alchemy login --configure在 scope 自定义中选择access:write。四、本地预览部署、共享管理员与销毁文档给出的本地预览最小流程原样继承可直接复制运行pnpm alchemy login # once — see Credentials above cp .env.preview.example .env.preview pnpm preview:access # once — the shared Access gate (safe to re-run) pnpm deploy:preview --stage manual-preview --yesdeploy:preview在 package.json 中展开为vite build --mode preview pnpm alchemy deploy --env-file .env.previewVite 以 preview 模式构建--mode preview会把.env.preview加载进客户端 bundle随后对.env.preview执行alchemy deploy额外传入的 flags如--stage原样落到 deploy 命令上。CI 跑的是同一条命令。有两个 alchemy 约定值得知道省略--stage时目标是 alchemy 默认的 per-user stagedev_$USER用.env.preview部署 stagehosted-prod会失败——预览 env 里没有BETTER_AUTH_URL生产请走pnpm deploy:postgres。这一失败在源码里可以直接对应alchemy.run.ts 中生产 stage 若读不到BETTER_AUTH_URL会Effect.die要求设置https://app.openseo.so非生产 stage 则从WORKERS_SUBDOMAIN推导https://open-seo-stage.sub。AUTH_MODE的默认行为同样是失败关闭未设置时取cloudflare_access与应用自身的默认一致绝不会退化成公开注册hosted/local_noauth必须显式设置。应用侧 src/lib/auth-mode.ts 对无效值也会打印警告并回落到cloudflare_access。每个预览从空数据库开始打开 URL、通过 Access 挑战即可进入。预览默认AUTH_MODElocal_noauth所有被门放行的人共享一个自动创建的管理员账号若想在预览里走真实的注册流程把.env.preview改为AUTH_MODEhosted示例文件已注释列出BETTER_AUTH_SECRET、BYPASS_EMAIL_VERIFICATION、GOOGLE_CLIENT_ID/SECRET等配套项。销毁 stage 同样简单——由于状态经 Cloudflare state store 共享任何持有凭据的机器都能执行pnpm destroy:preview --stage manual-preview --yes五、CI每个 PR 一个隔离 stage.github/workflows/pr-preview.yml 为每个私有仓库 PR 部署 stagepr-nPR 关闭时销毁它全程复用与本地完全相同的命令。工作流的关键设计仓库门只有github.repository bensenescu/open-seo且 PR head 来自本仓库时才运行——公共镜像仓库中这份同步过来的文件是惰性的paths-ignore纯文档、web/**、badseo/**等不能改变被部署 Worker 的路径不会触发部署且 filters 看到的是 PR 完整文件列表保证部署与关闭时销毁的判定一致并发控制同一 PR 的新 push 会取代进行中的部署alchemy 的 per-resource 状态在下次运行中干净地对齐而 close 触发的销毁永不取消、只排队等待secretsCLOUDFLARE_API_TOKENscope 见上文、CLOUDFLARE_ACCOUNT_ID以及ENV_PREVIEW.env.preview全文。部署前CI 会先从ENV_PREVIEW里 grep 出WORKERS_SUBDOMAIN并校验其形如*.workers.dev据此一次性推导PREVIEW_URLhttps://open-seo-pr-n.subdomain——在为一个坏掉的 secret 花掉一次部署之前就失败。部署步骤就是pnpm deploy:preview --stage $STAGE --yes。随后是关键的Verify Access protection步骤循环最多 8 次每次 sleep 5 秒容忍传播延迟对预览 URL 发 curl只要Location指向https://*.cloudflareaccess.com/cdn-cgi/access/login即认为门在位立即通过一旦收到任何 2xx/3xx 且没有 Access 重定向的确定性应用响应立即判定预览已公开并失败——此时正确的处置是销毁 stagepnpm destroy:preview --stage n --yes仅传播期错误超时、无响应才进入重试8 次耗尽后 job 失败但提示该预览仍位于通配 Access 应用之后可以重跑。也就是说一个不经过挑战就应答的预览等于公开必须销毁一个仅不可达的预览可以留着因为它仍在通配应用之后。验证通过后才会在 PR 上留下或更新预览 URL 评论PR 关闭时同一工作流执行销毁并更新评论为 Preview destroyed。由于状态经 state store 共享CI 与本地看到的是同一批 stage——任何钉子户都能在本地用pnpm destroy:preview --stage pr-n --yes清掉。六、公共镜像 PR只构建不执行 fork 的部署代码外部every-app/open-seoPR绝不从 CI 部署——fork 代码不能带着部署 secrets 运行。替代方案是在本地预览且遵循一条严格边界fork 的代码只做构建在一个 detached 的兄弟 worktree 中部署则从可信 checkout 的 alchemy 栈针对 fork 的dist/执行。fork 自己的部署脚本永远不会被执行fork PR 甚至可能早于 alchemy 这套配置。文档特别提醒构建会执行 fork 的配置代码而你的机器上有.env.preview可用——因此要先读 PR diff 中所有可执行的构建面package.json、lockfile、vite.config*、scripts/、patches/、.npmrc。文档给出的完整操作序列原样保留git fetch https://github.com/every-app/open-seo.git pull/pr/head git worktree add --detach ../open-seo-pub-pr FETCH_HEAD cp .env.preview ../open-seo-pub-pr/ (cd ../open-seo-pub-pr pnpm install --frozen-lockfile pnpm exec vite build --mode preview) rm -rf dist cp -R ../open-seo-pub-pr/dist dist git worktree remove --force ../open-seo-pub-pr pnpm alchemy deploy --env-file .env.preview --stage pub-pr --yes注意最后一步不是deploy:preview而是直接pnpm alchemy deploy——因为dist/已经由 fork 构建好可信仓库只需部署产物到pub-prstage。分享 URL 之前先验证预览确实重定向到 Access 登录页PR 结束后销毁pnpm destroy:preview --stage pub-pr --yes七、生产部署与首次割接检查清单生产走同一个 Alchemy 栈stage 为hosted-prod通过 package.json 的deploy:postgres部署pnpm deploy:postgres该脚本先跑 Postgres 迁移db:migrate:pg、构建再执行pnpm alchemy deploy --env-file .env.production --stage hosted-prod --adopt——--adopt和 stage 都烤在脚本里不会被遗忘且 alchemy 应用前会先展示 plan 供确认。它使用与预览相同的pnpm alchemy login凭据确保已为 Hyperdrive 开启query_cache:write。从 alchemy.run.ts 可以看到生产栈的完整约束面Worker 绑定域名[app.openseo.so, www.app.openseo.so]zone 由 hostname 推断部署vite build预构建产物main: ./dist/server/index.jsbundle: falseassets 指向./dist/client兼容日期/flags、crons、DO、Workflows 全部以wrangler.jsonc为单一事实来源读取生产 D1 使用migrationsDir: drizzle、migrationsTable: d1_migrations——与 wrangler 时代同一张 wrangler 兼容的迁移账本生产 Hyperdrive 连接池指向既有openseo配置且显式关闭缓存SaaS 需要 per-user 的读后写一致性生产必须显式声明DATABASE_PROVIDERpostgres或d1否则Effect.die——防止静默回落到d1打到 Postgres 割接前的陈旧数据Workflow 名是账号级的生产拥有不带后缀的名字预览携带 stage 后缀避免并发 stage 通过 name 的 PUT-as-upsert 互相重定向所有生产资源worker、D1、R2、KV、Hyperdrive带RemovalPolicy.retain首次生产部署时写入状态销毁hosted-prod只会忘记状态线上资源原封不动Workflow 注册是例外——它们在 worker provider 内部创建、不可单独 retain——但重新注册是无损 upsert。首次割接清单一次性文档要求在首次让 Alchemy 接管生产资源之前逐项完成在 alchemy 命令后追加--dry-run读 plan——每个生产资源D1open-seo、KVevery-super-seo/OAUTH_KV、R2open-seo、Hyperdriveopenseo、Workeropen-seo都应是 adopted 而非 created将.env.production与线上 Worker 的 secrets 做 diffGET /accounts/:id/workers/scripts/open-seo/secretsalchemy 部署会替换完整的 binding 集任何 env 文件中缺失的线上 secret 都会被部署为——由于多数变量是可空且有默认值的部署会成功却静默禁用该集成在一个镜像生产形态的 scratch stage 上彩排 adoption——先用wrangler部署 scratch Worker含migrations块让它带上 wrangler 时代的迁移标签和类似生产的 DO 命名空间全新的 alchemy stage 走不到生产将要走的那条 adoption 路径对比生产 D1 上SELECT name FROM d1_migrations与ls drizzle/*.sql——alchemy 会在首次部署时应用缺失的 D1 迁移wrangler 兼容账本已验证而 Postgres 割接后那套休眠的 D1 一直没有再迁移过核对.env.production中HYPERDRIVE_ORIGIN_*与线上 Hyperdrive 配置一致——Cloudflare从不返回origin 凭据不一致就会改写 origin注意生产已经跑在open-seo.subdomain.workers.dev上alchemy 保持其启用workers.dev 开关根本不会出现在--dry-run里。.env.production.example展示了该 env 文件的完整面貌AUTH_MODEhosted、BETTER_AUTH_URLhttps://app.openseo.so、DATABASE_PROVIDERpostgres、HYPERDRIVE_ORIGIN_*五项以及 hosted 认证Google、计费/邮件Autumn、Loops、GDPR、PostHog、OpenRouter 等可选集成项。八、Cloudflare 自托管自托管用户在同一套栈下以固定selfhoststage 部署pnpm deploy:selfhost无需传 stage使用自己的 env 文件。Alchemy 会按名开通全新的 D1/KV/R2/WorkflowsD1 即数据库——没有 Postgres/Hyperdrive外加一个门住该 Worker 的 Cloudflare Access 应用AUTH_MODEcloudflare_accessACCESS_ALLOWED_EMAILS。Access 应用不必手工抄配alchemy.run.ts 中的resolveSelfHostAccess会自动推导TEAM_DOMAIN/POLICY_AUD——一次 API 读取团队域账号没有团队时按 workers.dev 子域名自动创建并由 alchemy 以ACCESS_ALLOWED_EMAILS为 allow-policy 供给 Access 应用显式设置的 env 值总是优先手工维护的 Access 应用同时设TEAM_DOMAIN和POLICY_AUD继续可用届时不会触发任何自动供给。预览 Access 通配符和 PR 工作流是 OpenSEO 特有的自托管不需要。完整的分步说明在 docs/SELF_HOSTING_CLOUDFLARE.md自托管部署脚本在 package.json 中为node scripts/selfhost-deploy-preflight.mjs vite build --mode selfhost tsc --noEmit pnpm alchemy deploy --env-file .env.selfhost --stage selfhost。小结这套部署体系的核心取舍可以用三句话概括预览与生产共用一个 Alchemy 栈和同一套命名约定open-seo-stage/open-seo-*.workers.dev使得一个持久、独立的通配 Access 应用能先于任何预览存在地罩住全部预览--adopt让 Access 门、生产资源和自托管应用都具备状态丢失后的可恢复性而 CI 的 curl 验证、公共镜像 PR 的只构建不执行边界、以及首次割接的 dry-run 清单则把门缺失即失败和fork 代码不碰部署 secrets落实成了可执行的检查。对维护者而言理解 alchemy.access.ts 这一个共享模块worker 命名、通配符、邮箱解析、Access 应用形状基本就理解了整个安全边界的契约——两个 Access 门都从它派生而 CI 里那份无法 import 的 shell 拷贝则靠部署后的 Access 验证步骤兜底。【免费下载链接】open-seoOpen source alternative to Semrush and Ahrefs项目地址: https://gitcode.com/GitHub_Trending/op/open-seo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表