Civitai Auth Hub 的 CI 发布管线:pnpm Monorepo 中从 Git Tag 到 ghcr 镜像的 GitHub Actions → Flux 落地实践
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
本文基于仓库内的设计决策文档 ci.md 展开,完整还原登录中心apps/auth(部署为 auth.civitai.com)的 CI/发布方案:为什么选择 GitHub Actions 推送 ghcr 而非 Tekton、auth-app-v*标签如何驱动构建、镜像标签策略与 Flux 自动部署链路。读完你可以理解一条「PR 只做编译门禁、tag 才发布镜像」的 per-app CI 是如何在一个大型 pnpm monorepo 中设计并落地的,并能掌握从切 tag 到 Flux 滚动更新的完整发布流程。
背景:为什么需要这条 per-app CI
apps/auth是 Civitai 的集中式登录中心(SvelteKit + Svelte 5 + adapter-node),也是 monorepo 中第一个拥有独立 per-app CI 的应用(见 apps/auth/README.md)。该文档 ci.md 明确记录了决策依据,核心事实有三点:
- 主应用 civitai-web 的镜像由 Tekton(
tekton.civitai.com)构建。在文档决策时点,origin/main上唯一的 workflow 是 submodule-pin-guard.yml——它只是一个 guard,不构建任何镜像。因此主应用这边没有可以沿用或仿照的「Actions 构建镜像」先例。当前仓库快照中.github/workflows目录下另有 lint、schema-drift 等非构建类 workflow,同样不产出主应用镜像。 - 兄弟应用走的是 GitHub Actions → ghcr:
civitai/civitai-chat(ci.yml:buildx +type=gha缓存,semver 取自package.json,在main上推送)和civitai/civitai-image-cacher(main.yml:仅在v*tag 上推送)。两者都直接用内置GITHUB_TOKEN发布到 ghcr。 - Flux 已经在消费 ghcr 的 semver:datapacket-talos 仓库(集群部署仓库)中已有
ImagePolicy(semver>=0.0.1)+ImageUpdateAutomation监听ghcr.io/civitai/civitai-auth。一个能推送干净 semver tag 的 Actions workflow 是自包含的,可以直接接入这套既有机制——无需 Tekton pipeline/trigger 接线,也无需新增任何 secret。
决策记录:为什么是 GitHub Actions 而不是 Tekton
文档把方案定为「由证据决定,而非默认习惯」。若选 Tekton,意味着为一个小型应用单独搭建一套 Pipeline + trigger + dashboard 暴露,重复了兄弟应用已经用更简单方式完成的事情;而 Actions 方案直接复用 ghcr + Flux 的既有链路。结论一句话:证据支持 Actions。
这条决策还带来一个对 monorepo 很关键的好处——per-app 的 tag 命名空间。下文会看到,auth-app-v前缀让其他应用(如chat-app-v)可以各自采用自己的前缀,互不抢占共享的v*tag 命名空间。
触发条件与标签(tag)方案
文档中的完整触发矩阵如下,这是理解整条管线入口的关键:
| 事件 | 构建? | 推送镜像? |
|---|---|---|
PR 触及apps/auth/**、packages/**、lockfile、patches/**或该 workflow 本身 | 是 | 否(仅作编译门禁) |
推送auth-app-v*标签(如auth-app-v0.1.0) | 是 | 是 |
从auth-app-v*tag ref 发起的workflow_dispatch | 是 | 是 |
从普通分支发起的workflow_dispatch | 是 | 否 |
几个要点:
auth-app-v前缀将发布限定在本应用内。monorepo 中其他应用可各自采用自己的前缀(例如chat-app-v),避免争夺共享的v*命名空间(该命名空间属于主应用civitai-web的发布流程)。- semver 从 tag 派生:
auth-app-v0.1.0→0.1.0,并用正则^\d+\.\d+\.\d+([-+].+)?$校验(支持预发布/构建后缀)。 - git tag 才是版本的事实来源。文档写就时,应用
package.json的版本被刻意保持为0.0.0、不作为 source of truth;当前快照中 apps/auth/package.json 的版本已随历次发布同步到0.1.34,但「tag 为准」的原则不变。
发布时推送的镜像标签
在 release tag 上,会推送两个 tag:
ghcr.io/civitai/civitai-auth:<semver>—— 这是 Flux 的 ImagePolicy 实际选择的标签;ghcr.io/civitai/civitai-auth:sha-<short>—— 用于溯源与精确 pinning。
tag 构建不推:latest。文档给出的理由是::latest是一个会漂移的指针,在同一个 repo 上开启了 Flux image automation 时,存在被自动部署到错误 digest 的风险;而 Flux 本来就按 semver 选择镜像,:latest在这里不带来任何收益。
构建细节:根目录 context 与 Dockerfile
- 构建 context = 仓库根目录,
file: apps/auth/Dockerfile。原因在于 Dockerfile 的 COPY 语句全部相对仓库根:pnpm-lock.yaml、pnpm-workspace.yaml、package.json、patches/、packages/以及apps/auth/package.json。这与文档中记载的本地构建命令docker build -f apps/auth/Dockerfile .一致——Dockerfile 头部注释也明确要求以仓库根为 context。 platforms: linux/amd64(集群为 amd64)。- docker buildx + GitHub Actions 缓存(
cache-from/to: type=gha)。 - workflow 的
permissions: { contents: read, packages: write };ghcr 登录仅使用内置GITHUB_TOKEN,且只在 push run 时进行。
结合 Dockerfile 的源码可以看到这条构建链的四个阶段,每一处都有针对性设计:
- deps 阶段:
node:22-alpine以 digest 固定基础镜像;pnpm install --frozen-lockfile --filter @civitai/auth-app... --ignore-scripts只安装 hub 及其 workspace 依赖。注释特别说明了两点:patches/必须进入构建 context(因为根package.json声明了pnpm.patchedDependencies,pnpm 在安装时会对 patch 文件做哈希,即使--ignore-scripts也不能缺);--ignore-scripts会跳过根目录的 Prismadb:generate——hub 用的是手写的 Kysely 类型、从不 import Prisma client,所以无需生成。 - build 阶段:
pnpm --filter @civitai/auth-app build(svelte-kit sync && vite build,见 apps/auth/package.json 的 scripts)。这里设置了一个仅存在于 build 阶段的占位DATABASE_URL:db.ts在模块加载时就构造pg.Pool(new URL(env.DATABASE_URL)),而 SvelteKit 的analysepostbuild 步骤会 import 服务端模块,未设DATABASE_URL时会抛ERR_INVALID_URL。该 dummy 值不会带入runtime阶段(独立的FROM),运行时真实值来自 k8s secret。 - prod-deps 阶段:
pnpm --filter @civitai/auth-app deploy --prod --legacy --ignore-scripts /app/deploy产出一个自包含的、拍平的 productionnode_modules(无 workspace 符号链接),供运行时直接 COPY。 - runtime 阶段:
NODE_ENV=production,以非 root 用户(uid 1001sveltekit)运行,EXPOSE 3000,CMD ["node", "build"];adapter-node通过ORIGIN(以及代理后的PROTOCOL_HEADER/HOST_HEADER)计算正确的url.origin。
如何切一个 release
按文档给出的最小操作(在默认分支上、位于你要发布的 commit 处):
# from the default branch, at the commit you want to ship: git tag auth-app-v0.1.0 git push origin auth-app-v0.1.0推 tag 之后的四步链路:
- Actions 构建
apps/auth/Dockerfile,推送ghcr.io/civitai/civitai-auth:0.1.0(外加:sha-<short>)。 - Flux 的
ImageRepository扫描 ghcr(1–5 分钟);ImagePolicy(semver>=0.0.1)选出0.1.0作为最新版本。 ImageUpdateAutomation把提升后的 tag 提交到 datapacket-talos 的trunk分支。- Flux 完成 reconcile;auth-hub 的 Deployment 滚动更新到新镜像。
仓库内的配套发布工具
除了手工git tag,仓库还提供了脚本化的发布入口,可作为同一 tag 方案的自动化前身来理解:根 package.json 中定义了release:auth脚本族(release:auth/release:auth:minor/release:auth:major),它们都调用 scripts/release-app.mjsapps/auth auth-app-v <bump>。该脚本的关键行为包括:
- 强制干净工作树且处于
main分支(可用RELEASE_ALLOW_BRANCH=1覆盖),随后git pull --rebase; - 通过 scripts/lib/release-version.mjs 做版本偏斜(skew)检查——若当前分支落后于该应用已发布的历史则拒绝执行,防止在过期基线上切出错误 tag;
- 用
npm --prefix apps/auth version <bump> --no-git-tag-version只 bump 应用自身的package.json(根package.json不动)。脚本注释解释了为什么必须显式--no-git-tag-version:monorepo 的.git在根目录,npm --prefix ... version只会改写版本而静默跳过commit + tag(exit 0); - 只
git add该应用的package.json,创建 annotated tagauth-app-vX.Y.Z,然后git push --follow-tags。
完整的操作规范(包括 push 失败后的回滚:git tag -d auth-app-vX.Y.Z+git reset --hard origin/main)见 releasing.md。该文档还记录了一条重要约束:Flux 按最高 semver 选镜像,所以永远不要手工推送比已部署版本更低的 tag;另有一个「一次只发一个应用」的约定,因为并发发布会在main的 push 上互相竞争。
需要说明的仓库现状:apps/auth/README.md 的 Releasing 小节与 release-app.mjs 的注释目前仍描述旧的「集群内 Tekton
tag-webhook接收器」流程,而本文讨论的 ci.md 是把 hub 构建迁移到 GitHub Actions 的决策记录——仓库正处于两种方案的过渡状态,以 ci.md 为准。
现状与适用限制(Status)
文档对这条 workflow 的状态描述值得完整保留,它界定了好文之外的「尚未验证」边界:
- workflow 已完成编写且通过 actionlint 静态检查;
- 未经真实运行验证——GitHub Actions 无法在本地运行,且 tag 触发的 workflow 只有在 workflow 文件位于默认分支后才会执行;
- 首次真实运行要等到该 PR(#2468)合入
main之后,再推送第一个auth-app-v*tag 时发生; - 底层 Docker 构建已被验证——线上在跑的
0.0.1镜像就是用这份完全相同的 Dockerfile + 根目录 context 构建的。
当前仓库快照同样印证了这一状态:.github/workflows目录中尚不存在auth-app.yml,只有 guard/lint 等非构建 workflow——即该 workflow 文件在快照时点还未合入默认分支,tag 触发发布尚未生效。
发布管线的配套验证:e2e 冒烟测试
与发布管线配套的验证层在 apps/auth/e2e/:这是一个针对已部署环境的 Playwright 冒烟框架(Layer 2),直接指向HUB_URL(如https://auth.civitai.com)而非本地webServer,并带有确定性的 stub OIDC 服务(stub-oidc-server.mjs --selftest可自测),使真实的/login/[provider]/callback路径能在无真实 provider 的情况下被驱动。认证断言依赖「可信签名密钥 vs 临时密钥」模式:配置了 hub 信任的AUTH_JWT_PRIVATE_KEY时才断言 identity 接口,否则仅跑未认证路径。这为「tag → 镜像 → Flux 部署」链路的部署结果提供了发布后可执行的验证手段。
小结
这条 auth hub CI 是 monorepo 中第一个 per-app 发布管线,其设计要点可以概括为四点:PR 只当编译门禁、tag 才推镜像(auth-app-v*前缀隔离各应用命名空间);只推<semver>与sha-<short>两个标签、不推:latest以适配 Flux 的 semver 选择机制;构建 context 固定为仓库根目录,与 Dockerfile 的多阶段(filtered frozen install → vite build →pnpm deploy --prod→ 非 root runtime)精确匹配;下游完全复用 datapacket-talos 中既有的 ghcrImageRepository/ImagePolicy/ImageUpdateAutomation机制,不引入 Tekton 接线与新 secret。对同仓库后续拆出的应用(notifications、storage、moderator 等),文档给出的模式是可复制的:选一个<app>-vX.Y.Ztag 前缀,加一个同构的 workflow 与release:<app>脚本族即可。
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考