跳到正文

Blog

从 GitHub Actions 到 Appaloft:一次部署应该留下哪些证据

Appaloft 如何把 GitHub Actions 触发的部署变成可审计的证据链:commit SHA、image digest、config snapshot、provenance、readback 和 rollback。

AppaloftGitHub ActionsDeployment controlProvenanceAI

GitHub Actions 很适合触发部署。

代码合并、打 tag、创建 release、给 PR 加 label,都可以让 workflow 自动跑起来。它能 checkout 代码、安装依赖、构建镜像、上传 artifact、调用 CLI 或 API。对团队来说,这比靠某个人在本机敲命令稳定得多。

但 workflow 成功不等于部署事实足够清楚。几周后如果有人问“线上现在跑的是哪个版本”,一个绿色的 Actions run 往往不够。你还需要知道:这次部署来自哪个 commit,构建出了哪个 artifact,使用了哪份配置,runtime 最后拉取的是哪个 image digest,健康检查验证了什么,失败时能退回到哪里。

这篇接着面向 AI 时代的部署控制面Skill 不是 MCPAI Agent 生成了静态站点,接下来该怎么上线?往下说:GitHub Action 可以是入口,但 Appaloft 更关心部署完成后留下的证据。

CI log 不是部署状态

很多部署事故复盘,最后都会回到一个很朴素的问题:我们到底部署了什么?

分支名回答不了这个问题。main 会继续移动,release 分支可能被更新,latest tag 也可能被覆盖。commit SHA 稳定一些,但它只能说明源代码版本,不等于 runtime 最终运行的 artifact。一个 Docker image tag 也不够,因为 tag 是名字,不是不可变内容。

更接近部署事实的是一组绑定关系:

  • source revision:仓库、commit SHA、submodule SHA 或 source snapshot;
  • build execution:workflow path、run id、runner 环境和构建命令;
  • artifact identity:image repository、tag 和不可变 digest;
  • config identity:部署配置、环境引用、secret ref 和 config snapshot digest;
  • deploy request:actor、target environment、resource、request id 和 idempotency key;
  • runtime readback:实际拉取的 digest、health check、URL、日志和状态;
  • recovery target:上一个 verified digest、上一份配置 snapshot 或明确的人工恢复缺口。

CI log 可以帮助排查,但它不是控制面状态。它会被截断、过期、权限隔离,也很难被 CLI、Web Console、AI agent 和后续 workflow 稳定消费。部署控制面需要把关键事实结构化保存下来。

commit SHA 解决不了所有问题

我们喜欢用 Git SHA,因为它给了代码版本一个很硬的名字。问题是,部署不是只运行 Git。

同一个 commit 可以因为基础镜像更新、package registry 变化、构建参数不同、secret 引用缺失、submodule checkout 不一致,生成不同的 artifact。就算构建完全可复现,runtime 也可能拉到了 tag 指向的另一份 image。反过来,一个已经构建好的 image digest 可以被部署到多个环境,使用不同 config snapshot。

所以证据链里要同时保留 SHA 和 digest。

SHA 告诉我们源头是什么。Digest 告诉我们实际运行的 artifact 是什么。Config snapshot digest 告诉我们这次部署不是只换了代码,也绑定了当时的运行配置。少了任何一个,回滚和审计都会变模糊。

对 Appaloft 来说,image://...prebuilt-image 这类入口不是为了让 CI 绕过控制面。它们的意义是把“Action 已经构建好的 artifact”作为输入,交给 Appaloft 继续做计划、执行、验证和恢复。

一个简化后的调用可以长这样:

appaloft deploy image://ghcr.io/acme/web:pr-123-abc123 \
  --config appaloft.yml \
  --preview pull-request \
  --preview-id pr-123

真正要保存的不是这行命令本身,而是这行命令背后的 source、artifact、config、target 和 readback。

provenance artifact 应该很无聊

Provenance 不应该是一篇散文,也不应该是把整个 CI log 打包上传。它最好是一份 schema-versioned 的小 JSON,让控制面和人类都能读。

一份实用的部署 provenance 至少可以包含:

  • schemaVersion
  • source repository 和 commit SHA;
  • public dependency 或 submodule SHA,如果部署依赖它;
  • workflow path、run id、run attempt;
  • build source,例如 Dockerfile path 或 build context digest;
  • image repository、tag、digest;
  • deployment config path 和 config snapshot digest;
  • Appaloft CLI/API 版本或控制面版本;
  • target environment、resource id 或 preview id;
  • actor、trigger event 和 request id;
  • previous verified artifact,用于 rollback;
  • createsExternalResources 这类外部影响标记。

它不应该包含:

  • API token、deploy token、GitHub token;
  • registry credential;
  • database URL;
  • SSH private key;
  • webhook secret;
  • OTLP、Sentry、Stripe、邮件或对象存储密钥;
  • 未脱敏的 provider endpoint、account id 或客户数据。

这个边界很重要。Provenance 是给审计、恢复和诊断用的证据,不是把 secret 搬进另一个 artifact。

部署请求要引用证据,而不是复述证据

当 GitHub Action 调 Appaloft 时,它不需要把所有细节塞进一条长命令。更好的形状是:构建阶段产出 provenance artifact,部署请求引用它,并把最关键的 identity 放在结构化 payload 里。

例如:

source:
  kind: docker-image
  repository: ghcr.io/acme/web
  tag: pr-123-abc123
  digest: sha256:...
  provenanceRef: github-actions-artifact://run-123456/deploy-provenance.json
config:
  path: appaloft.yml
  snapshotDigest: sha256:...
target:
  environment: preview
  resource: web
  previewId: pr-123
rollback:
  previousVerifiedDigest: sha256:...

这不是在要求每个项目都马上写一个复杂平台。恰恰相反,它是在把最小必要事实从 terminal output 里拿出来:哪个 artifact,哪份配置,哪个目标,如何读回,怎么撤回。

Appaloft 的角色是接住这份请求,然后继续生成部署计划、执行 runtime 变更、记录 events、检查 health、暴露 status/logs/readback,并在失败时给出 retry、redeploy 或 rollback 的路径。

readback 要证明 runtime 真的拿到了它

构建系统说“我 push 了 image”还不够。

部署控制面至少要读回几类事实:

  • registry 里存在这个 image digest;
  • runtime 拉取的是 digest,而不是一个漂移的 tag;
  • workload、service 或 container metadata 能关联 request id;
  • health endpoint 或 access check 通过;
  • logs/status 里能看到同一条 deployment attempt;
  • 当前 environment 的 active revision 指向这次 verified artifact;
  • rollback candidate 指向上一个 verified artifact,而不是靠人猜。

有些 readback 需要 provider adapter,有些可以先由 Appaloft runtime 记录,有些在本地开发或预览环境里只能标成 deferred。这个差异应该被写出来,而不是被绿色 CI status 盖过去。

这也是 Appaloft 对 AI agent 友好的地方。Agent 不需要 SSH 到机器上猜容器版本,也不需要去 GitHub Actions 里翻长日志。它应该问控制面:当前部署 attempt 是什么状态?active digest 是什么?health 证据在哪里?rollback 目标是哪一个?

rollback 应该回到一个 verified target

“回滚到上一个版本”听起来简单,实际经常说不清。

上一个版本是上一个 commit?上一个 image tag?上一个 workflow run?还是上一个真正通过 health check 的 artifact?如果上一次部署换过环境变量或依赖资源,只回滚代码是否足够?

Appaloft 更希望 rollback 指向 verified target:

  • 上一个成功激活的 image digest;
  • 对应的 config snapshot digest;
  • 当时的 resource/profile readback;
  • 已知的健康检查结果;
  • 如果不能自动恢复,就明确列出 manual gap。

这样 rollback 就不是“重新跑一次旧脚本”。它是一条新的控制面操作:选择已验证目标,生成计划,确认影响,执行切换,再读回结果。

这个模型对 preview deployment 也有用。PR preview 的 cleanup 不应该按模糊条件删除资源;它应该按 preview id、source fingerprint、provenance label 或 deployment record 找到自己创建的东西。部署和清理都需要证据,不然自动化越多,遗留状态越难解释。

GitHub Action 仍然只是入口

GitHub Actions 的价值很大:它把部署放进仓库事件、review、secret 管理和执行记录里。Appaloft 不想替代它。

但 Action 不应该变成第二套部署产品。它不应该在 workflow 里手写一堆 provider SDK 调用,然后让 Appaloft 只能事后旁观。更好的分工是:

  • GitHub Actions 负责 checkout、test、build、publish artifact、生成 provenance;
  • Appaloft 负责部署语义、计划、权限、执行、readback、日志、状态和恢复;
  • CLI、Web Console、MCP 和 AI skill 读取同一组 deployment records。

这样本地 CLI 和团队 CI 不会分叉。开发者可以先用 CLI 接通部署路径,再把稳定命令放进 GitHub Action。CI 失败后,本地或 Web 仍然能看到同一个 deployment attempt,而不是只能去 Actions UI 里翻日志。

我们还不想假装这已经全自动完成

Digest-first 部署会带来额外工作。

你要管理 registry 权限、image retention、submodule checkout、config snapshot、provenance schema、artifact readback、runtime labels、rollback target 和 cleanup policy。对于一个周末项目,这可能显得太重。对于团队、AI agent、PR preview、托管控制面和生产环境,它会少很多含糊地带。

Appaloft 当前的方向不是把所有部署都包装成一个看不见细节的按钮,而是让入口更轻,证据更硬。GitHub Action 可以触发部署,AI agent 可以协助生成或调用 workflow,人类可以在 CLI 和 Web 里复核状态。但它们都应该围绕同一组事实工作。

一次好的部署不只应该留下“成功”两个字。它应该留下足够的信息,让六周后的你还能回答:部署了什么、谁触发的、基于哪份配置、runtime 实际运行哪个 artifact、怎么验证、怎么撤回。