OFFSET偏移
← 文章目录OFFSET / FIELD NOTESNO. 109

Nix CI:求值、构建与输出契约

用六种主题输出的教学示例区分 Nix 求值、构建、Actions 记录和桌面运行证据。

所在专题GitHub Actions 与 CI ↗
约 29 分钟阅读开始阅读

example-theme-project 的 GitHub Actions 是一个单 job、只读权限的 Nix CI:在 ubuntu-latest 上安装 Nix,先求值,再构建六个主题变体,最后运行一个检查输出契约的 derivation。它能证明某个源码快照在这个 runner 上能通过 Nix 求值、构建和文件契约检查;它不能证明 aarch64-linux 已被 CI 构建,也不能证明 Fcitx5 或任何桌面 compositor 正在实际运行。

本文以一个匿名化主题项目演示如何分别记录三种证据;下面的仓库、配置与记录均为教学情境,不报告个人项目的真实运行:

  1. 本地 checkout/flake 证据:源码、flake.nix 声明的 outputs,以及本地命令的结果。
  2. GitHub Actions 运行证据:GitHub 服务器上某一次 workflow run 的事件、job、步骤和结论。
  3. 桌面运行时证据:Fcitx5、fcitx5-gtk 和 compositor 在真实用户会话中的启动、配置加载和视觉行为。

把三者混在一起会导致典型误判:把 nix flake check --all-systems --no-build 当成跨架构构建,把 package build 当成桌面兼容性测试,或把 release 页面当成 workflow 自动发布的结果。

1. 案例目标与证据边界#

1.1 项目情境与源码身份

假设 example-theme-project 是一个面向 Fcitx5 的主题 flake,提供浅色、深色及两者组合,并分别有 blur/solid 两种风格。项目声明 x86_64-linux 和 aarch64-linux,另外导出 Home Manager 模块。名字、主题目录和仓库所有者均为示例占位。

复核时必须记录 checkout 的完整 revision、是否有未提交文件、flake.lock 与构建系统。与 origin/main 对齐只说明查看的是哪个源码快照,不能说明主题已经在用户会话激活,也不能证明所有 compositor 兼容。

如果 README 区分“某桌面已实测”与“其他桌面仅理论协议支持”,应保留这个区别并检查证据,不能仅因为欢迎 Issue/PR 就扩大支持范围。缺少 CONTRIBUTING、CODEOWNERS、SECURITY 或模板也不代表没有贡献者活动。本教学示例没有任何真实桌面测试结论。

1.2 三层证据矩阵

问题 本地 checkout/flake 能证明什么 Actions 能证明什么 仍需真实桌面验证什么
Nix 表达式是否能求值? 本地指定 checkout 的 flake 求值结果 远端 runner 对该事件 checkout 的求值结果 不涉及桌面
六个输出能否构建? 本地机器实际构建的输出 本例中的 x86_64 Ubuntu runner 构建结果 不涉及 Fcitx5 运行
输出内容是否满足契约? checks derivation 的本地结果 checks.x86_64-linux.variants 的远端结果 不涉及 compositor
aarch64-linux 是否可用? 需在对应架构的本机或远程 builder 上实际构建;仅指定 --system 不提供 builder 本 workflow 没有证明,因为没有架构矩阵 仍不涉及桌面
Fcitx5/GTK 候选窗是否显示? 仅有部署到真实用户会话并操作才可证明 示例 workflow 没有 Fcitx5、GTK 或 compositor 步骤 需要 Fcitx5、fcitx5-gtk 与当前 compositor
Hyprland、niri、KWin、GNOME 是否模糊? 需要对应会话和协议实际交互 CI 没有图形会话 逐桌面记录已测、未测和失败原因

1.3 可以复核的本地入口

在待核验的 checkout 中(以下仅为命令示例),最小复核路径是:

cd <repo-path>
git status --short --branch
git rev-parse HEAD
nix flake check --all-systems --no-build

若 nix flake check --all-systems --no-build 返回成功但报告 unknown flake output 'homeManagerModules',应记录“求值通过且有 warning”。具体 warning 随 Nix 版本和 flake 结构变化,本例不声称已经运行这些命令。该 warning 应单独记录,不应改写成“完全无 warning”;同样,它也不是运行时测试结果。要检查 package 实现本身,应额外运行 workflow 中的六个 nix build --no-link 命令和 variants check,而不是只重复 --no-build。

2. Workflow 逐段解析#

2.1 教学 YAML 的完整形状

下面是 nix.yml 的完整教学配置;它没有隐藏的 matrix、cache、artifact 或 release step:

name: Nix checks

on:
  push:
  pull_request:

permissions:
  contents: read

jobs:
  flake:
    runs-on: ubuntu-latest
    steps:
      - name: Check out source
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - name: Install Nix
        uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
        with:
          extra_nix_config: |
            experimental-features = nix-command flakes

      - name: Evaluate the flake
        run: nix flake check --all-systems --no-build

      - name: Build all theme variants
        run: >-
          nix build --no-link
          .#both-blur .#light-blur .#dark-blur
          .#both-solid .#light-solid .#dark-solid

      - name: Check package output contracts
        run: nix build --no-link .#checks.x86_64-linux.variants

2.2 触发器和权限

  • push: 和 pull_request: 都没有 branches、paths 或 paths-ignore。因此,示例写法可对分支/tag push 和默认活动类型的 PR 触发;PR 默认并非响应每一种活动;它没有只针对 main 的分支过滤,也没有按目录节省运行。
  • permissions: contents: read 把 GITHUB_TOKEN 权限限制为读取仓库内容。示例 workflow 不需要写标签、写 release、评论 PR 或访问包仓库。
  • 只有一个 job:flake,运行在 ubuntu-latest。没有 OS、架构、Nix 版本或主题变体的 strategy.matrix。
  • cachix/install-nix-action 的 extra_nix_config 打开 nix-command 和 flakes,让后续的 nix flake/nix build 命令可用;它不是 Nix 二进制缓存配置。

这里的“所有 push”包括 tag push。因此示例中的 v0.2.0 tag push 也可以触发同一个检查,但不等于 workflow 会发布 release。

2.3 固定 SHA 的供应链意义

actions/checkout 固定到 3d3c42e5aac5ba805825da76410c181273ba90b1,并用注释标出 v7.0.1;cachix/install-nix-action 固定到 13d8dd58da0234aa297dedd986986ccb8e7f3e24,并标出 v31.11.1。两个完整 SHA 与版本标签于 2026-10-08 通过上游 tag API 核对;这里保留所采用版本,不保证它们是最新或没有漏洞。

固定完整 commit SHA 是供应链实践:它避免一个可变 tag 在未来悄悄指向另一份代码,方便审计和复现。它不是“Action 已被证明安全”的证明;仍应审查 action 所属仓库、源码、权限和升级变更。这里也没有把固定 SHA 夸大成签名验证、SBOM 或依赖扫描。

3. Flake outputs 与 output contract#

3.1 输入、系统和 package 输出

本示例假设 flake.nix 使用 github:NixOS/nixpkgs/nixos-unstable,并声明两个系统:x86_64-linux 和 aarch64-linux;其中 forAllSystems 会把 package 与 checks 的定义映射到这两个系统。

假设核心 package 变体在 flake.nix 中定义如下:

输出 主题目录 模式 预期行为
both-blur(也是 default) example-theme、example-theme-dark blur 两套主题,请求 compositor 原生模糊
light-blur example-theme blur 仅浅色主题,请求 compositor 原生模糊
dark-blur example-theme-dark blur 仅深色主题,请求 compositor 原生模糊
both-solid 两套主题 solid 两套主题,不请求模糊
light-solid example-theme solid 仅浅色主题,不请求模糊
dark-solid example-theme-dark solid 仅深色主题,不请求模糊

default 与 example-theme-package 都指向 both-blur,而六个具名变体保留给消费者选择。这是 Nix output 命名契约,不是 GitHub release asset 列表。

3.2 Home Manager output 与 CI 的边界

flake 还导出 homeManagerModules.default 和 homeManagerModules.example-theme,对应 flake.nix。workflow 没有专门的 Home Manager consumer、module option、NixOS dry-build 或配置实例测试;所以“导出了 Home Manager module”与“CI 已验证 Home Manager 集成”不能互换。

若本地出现 unknown flake output 'homeManagerModules' warning,也应和 package/checks 的成功分开记录。不要因为 warning 非致命就声称该 module 已在 consumer 配置中激活,更不要把它解释为 Actions 进行了 Home Manager 专项测试。

3.3 checks.x86_64-linux.variants 的内容契约

checks 的六个变体定义在 flake.nix;检查脚本在 flake.nix 由 pkgs.runCommand、coreutils 和 gnugrep 生成。它检查的是最终 Nix store 输出中的文件和文本契约:

下面是内容契约的伪代码,省略了实际 Nix 表达式和 shell 实现,不能直接作为 .nix 文件运行:

selectedThemes =
  # theme.conf、panel.svg、highlight.svg、theme.conf.tpl、highlight.svg.tpl 存在

unselectedThemes =
  # 未选主题的两个目录不存在

styleChecks =
  if variant.blur then
    # EnableBlur=True、BlurMask=blur-mask.svg、fill-opacity:0.25、mask 存在
  else
    # EnableBlur=False、BlurMask=、fill-opacity:1、mask 不存在

因此这是一种轻量的 output-contract test:它用 shell test 和 grep 约束消费者会看到的路径、主题选择、模糊字段、SVG 透明度和 mask 文件。它不是单元测试框架,也没有模拟 Fcitx5、渲染 GTK 候选窗或启动 compositor。

3.4 求值不等于跨架构构建

这是本案例最容易误读的点:

  1. nix flake check --all-systems --no-build 会对 flake 的各系统输出进行求值/检查发现,但 --no-build 明确禁止通过该命令实际构建 derivation。
  2. 六个无系统限定的 nix build --no-link .#... 在本例 ubuntu-latest runner 上解析为该 runner 的系统,即实际构建的是 x86_64-linux 输出。
  3. 最后一步明确写了 .#checks.x86_64-linux.variants,更直接地锁定了 x86_64 check。

所以,flake.nix 声明 aarch64-linux 只表示它有该系统的输出定义;它不等于 GitHub Actions 已构建 aarch64。示例 workflow 没有 matrix、native aarch64 runner、remote builder 或跨架构 emulation,不能写成“CI 已验证双架构”。

4. 本地、Actions 与运行时:三次验证要分别记账#

4.1 本地 checkout/flake 证据

本地证据回答“这个 checkout 的 Nix 结构是否成立”。以下是待填记录格式,没有真实执行结果:

checkout: <repo-path>
revision: <完整源码提交 SHA>
branch: <实际分支及远程对齐情况>
command: nix flake check --all-systems --no-build
result: <实际退出码与结果>
note: <实际 warning;无则明确记录无>

要把本地结果和远端结果对应起来,应另外运行六个 package build 与 variants check,并记录 Nix 版本、nixpkgs lock、系统架构和是否使用未提交文件。一次本地成功不能替代 Actions run;求值成功不能替代实际构建。

4.2 如何记录 GitHub Actions 运行证据

以 main push 和 tag push 为例,逐次核对事件、head SHA、job 和步骤,而不是只看到一个绿色徽章。本表是记录模板,未填入真实成功结果:

Run 事件/引用 结果 必须观察的步骤
<main-run-id> push 到 main,<source-commit-sha> <实际 conclusion> checkout、安装 Nix、求值、六变体 build、output contract check
<tag-run-id> push 到 v0.2.0,<source-commit-sha> <实际 conclusion> 同一 job 的实际步骤及是否跳过

可使用下列命令读取相应运行;只查看日志,不需要发布权限:

gh run list --repo OWNER/REPO --workflow nix.yml --limit 10
gh run view RUN_ID --repo OWNER/REPO
gh run view RUN_ID --repo OWNER/REPO --log-failed

真实 run 只能证明 runner 对相应事件 checkout 执行了哪些步骤,不证明本地 GUI 状态。没有写出的 Fcitx5、GTK、Wayland/X11 或 compositor 测试不会因为 job 变绿自动发生;本表也没有声称任何 run 已成功。

4.3 桌面运行时证据

桌面证据应来自真实用户会话:记录软件版本、已加载主题、Fcitx5/GTK 候选窗行为、compositor 的协议与视觉结果。是否需要 reload/restart 以实际安装方式和软件文档为准;这些动作不属于本文 CI。

  • CI 成功不证明 Fcitx5 ClassicUI 已加载主题。
  • CI 成功不证明 fcitx5-gtk 的 GTK 内嵌候选窗显示正确。
  • CI 成功不证明 Hyprland、niri、KWin 或 GNOME 的模糊协议已工作。
  • 若只测过其中一个桌面,应把其他桌面标为未测或理论支持,并给出单独的后续验证条件。

不要把源码声明、协议兼容推断、作者 README 描述和自己实际测得的行为记在同一个“已验证”栏。此匿名化示例没有任何桌面已实测结论。

5. 供应链、发布和当前缺口#

5.1 示例配置已声明的能力

能力 示例配置 边界
只读仓库权限 contents: read 示例的 permissions
Action 引用可审计 GitHub 官方 actions/checkout 与第三方 cachix/install-nix-action 均使用完整 commit SHA 示例的 uses
push/PR 基础 CI push 和 pull_request 均触发 示例的 on
Nix 求值 nix flake check --all-systems --no-build 示例的求值步骤
package 构建 六个变体在一个 job 中构建 示例的构建步骤
输出契约 x86_64 variants check 示例的 checks 选择与对应契约实现

5.2 示例未声明的能力

以下是上文完整示例 workflow 的边界,不是对真实仓库目录的审计结果:

  • 没有 strategy.matrix:不是多系统/多架构矩阵。
  • 没有 actions/cache、Cachix cache 配置或其他缓存步骤:不能声称 CI 有依赖缓存。
  • 没有 upload-artifact:不能声称失败日志、包或测试报告会作为 artifact 保留。
  • 没有 concurrency:没有取消同一分支过时运行的策略。
  • 没有 workflow_dispatch、schedule 或 release automation:不能声称支持手动/夜间检查或自动发布。
  • 没有 Home Manager 专项 consumer test、NixOS dry-build、lint/format job 或桌面运行时 job。
  • 没有 Secrets、OIDC、部署权限或发布 token;示例只读 CI 不需要它们。

未配置 Actions cache 或 Cachix 上传步骤,不等于 Nix 不会从默认 substituter 获取已构建输出;需要区分 Actions 目录缓存、Nix binary cache 和实际本地构建。

5.3 Tag 与 Release 不等于自动发布

可以通过 ref API 区分 lightweight 与 annotated tag:若 ref 的 object type 是 commit,它直接指向提交;若为 tag,还应读取 tag object 并核对其目标。

gh api repos/OWNER/REPO/git/ref/tags/v0.2.0 --jq '.object.type'
gh release view v0.2.0 --repo OWNER/REPO

Release 是围绕 tag 的平台发布记录,是否有资产也要单独查看。本示例 workflow 只有 checkout、安装 Nix、求值、构建和契约检查,没有 gh release create、release Action 或等价发布步骤。

因此,若 tag push 的检查成功,只能写“该 tag 对应的 Nix CI 通过”。仓库中另外存在 Release,不等于此 workflow 创建了它;没有 upload/release 步骤也不能声称上传过制品。这里没有实际仓库发布、Latest 标记或 tag 类型的事实声明。

5.4 可选增强:不是示例实现

如果未来需求变成“更快、更宽或可发布”,可以分别加能力;下面只是设计方向,不应倒填为本案例现状:

  1. 架构覆盖:为 x86_64-linux 与可用的 aarch64-linux runner 建 matrix,或明确配置 remote builder;每个矩阵单元必须选中并实际实现对应系统输出,配置可用的同架构 builder;例如 nix build .#packages.aarch64-linux.both-blur,不能只把 --all-systems 留在 --no-build 求值里。
  2. 缓存:按锁文件、系统和 Nix 输入设计缓存键,明确缓存失效和信任边界;缓存命中只能改善速度,不能替代构建结果。
  3. 产物:只在确有调试或分发需要时上传日志、nix path-info 或经过审查的 package;artifact 不是 release,也不应包含凭据。
  4. Home Manager 检查:增加最小 consumer flake,验证 module import、选项求值和 dry-build,并把它与 package output check 分开命名。
  5. lint/format:若项目建立 Nix/Markdown 格式契约,再增加独立 job;示例没有对应 job,不能把 shell contract check 称为 lint。
  6. 发布自动化:若要让 tag 创建 release,新增独立、受保护的 release workflow,声明写权限、tag 条件、资产来源和审批;不要把当前 tag push check 误认为发布流水线。
  7. 运行时 smoke test:只有在可控图形 runner、虚拟 compositor 或真实测试主机准备好后,才把 Fcitx5/GTK/compositor 验证加入 CI;否则继续把它记录为部署后的人工/主机测试。

6. 故障排查清单#

6.1 先确认失败属于哪一层

  1. 触发未发生:确认是 push 还是 PR,检查该 commit 是否包含 .github/workflows/nix.yml;示例没有分支过滤,所以不应先假设是 main 限制。
  2. checkout/Action 失败:查看固定 SHA 对应的 Action 版本、runner 镜像和权限;不要为绕过失败改成可变 tag。
  3. 求值失败:本地同一 revision 运行 nix flake check --all-systems --no-build,先区分语法/输入/系统属性错误和非致命 warning。
  4. 构建失败:本地逐个运行 nix build --no-link .#both-blur 等六个 output,定位是某一变体、依赖下载、平台还是源文件问题;不要只看 flake check。
  5. contract check 失败:把目标 derivation 实现与 flake.nix 对照,检查主题是否缺文件、错误暴露未选主题、blur/solid 字段或 SVG opacity 不符。
  6. 怀疑 aarch64:不能用一个绿色 run 作结论;在有 aarch64 builder 的环境中选择对应系统输出并实际构建,并单独记录 builder、revision 和结果。
  7. 桌面显示异常:先确认 package 内容和 Fcitx5 搜索路径,再在真实会话 reload/restart Fcitx5;最后按 Hyprland、niri、KWin、GNOME 分别记录协议和视觉结果。
  8. 把 warning 当故障:unknown flake output 'homeManagerModules' 是可能出现的求值 warning;应追踪其 flake 兼容性,但不能用它掩盖真正的非零退出码。
  9. 询问是否自动发布:回看 workflow 是否有 release action 和写权限;本案例没有,因此应分别查询并报告 tag check 与 Release,不能假定 Release 已存在,不报告自动上传。
  10. 涉及凭据:不要把 token、Secrets、完整环境变量或私有 runner 信息复制进日志、笔记或 artifact;本案例没有 Secrets/OIDC 证据,也不需要补造。

7. 如何解释结果#

这个案例的价值在于它展示了一个足够小但可审计的 Nix CI 契约:固定 Action SHA、只读 token、统一 push/PR 入口、先求值再构建六个变体,并用 test/grep 检查消费者可见的输出结构。它的边界同样清楚:只有一个 ubuntu-latest job,在假定的 x86_64 runner 上只选择 x86_64-linux 输出;没有 cache、artifact、matrix、release automation、Home Manager 专项测试或桌面运行时验证。

阅读或复用这份配置时,最短的正确结论是:

绿色 run 证明 Nix CI 通过,不证明所有系统都构建、不证明 release 自动创建,也不证明 Fcitx5 或任何桌面 compositor 已运行。

相关概念可回看 04:Actions 基础与执行模型、05:Actions CI 配置方法 和 06:Actions 安全与排障。Workflow 语法以 GitHub Actions workflow syntax 为准;第三方 Action 固定 SHA 的安全背景见 GitHub Actions security hardening。

版本与来源边界:本示例保留源材料的六变体结构、Home Manager 模块与 output-contract 思路,移除了个人项目身份和运行记录。示例固定 Action SHA 的标签对应关系经核对,工作流本身没有在本次编辑中运行;第三方 Nix 安装 Action 的行为以其上游文档和该固定提交为准。Nix CLI 属于持续演进的接口,本文对 --no-build/--all-systems 的解释参考核验时的官方手册。

参考与延伸

  1. GitHub Actions workflow syntax ↗
  2. GitHub Actions security hardening ↗
  3. GitHub Actions 安全使用参考 ↗
  4. GitHub Actions 事件参考 ↗
  5. actions/checkout v7.0.1 ↗
  6. cachix/install-nix-action v31.11.1(第三方) ↗
  7. Nix flake check 官方手册 ↗

根据技术笔记重新整理,示例使用通用名称。

沿专题继续阅读

GitHub Actions 与 CI

  1. 01GitHub Actions 执行模型
  2. 02GitHub Actions CI 配置
  3. 03GitHub Actions 安全与排障
  4. 04Nix CI:求值、构建与输出契约
CONTINUE READING / 下一篇Docker 基础:镜像、容器与守护进程

沿客户端、API、containerd 与 OCI 运行时理解容器启动。