example-theme-project的 GitHub Actions 是一个单 job、只读权限的 Nix CI:在ubuntu-latest上安装 Nix,先求值,再构建六个主题变体,最后运行一个检查输出契约的 derivation。它能证明某个源码快照在这个 runner 上能通过 Nix 求值、构建和文件契约检查;它不能证明aarch64-linux已被 CI 构建,也不能证明 Fcitx5 或任何桌面 compositor 正在实际运行。
本文以一个匿名化主题项目演示如何分别记录三种证据;下面的仓库、配置与记录均为教学情境,不报告个人项目的真实运行:
- 本地 checkout/flake 证据:源码、
flake.nix声明的 outputs,以及本地命令的结果。 - GitHub Actions 运行证据:GitHub 服务器上某一次 workflow run 的事件、job、步骤和结论。
- 桌面运行时证据: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.variants2.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 求值不等于跨架构构建
这是本案例最容易误读的点:
nix flake check --all-systems --no-build会对 flake 的各系统输出进行求值/检查发现,但--no-build明确禁止通过该命令实际构建 derivation。- 六个无系统限定的
nix build --no-link .#...在本例ubuntu-latestrunner 上解析为该 runner 的系统,即实际构建的是x86_64-linux输出。 - 最后一步明确写了
.#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/REPORelease 是围绕 tag 的平台发布记录,是否有资产也要单独查看。本示例 workflow 只有 checkout、安装 Nix、求值、构建和契约检查,没有 gh release create、release Action 或等价发布步骤。
因此,若 tag push 的检查成功,只能写“该 tag 对应的 Nix CI 通过”。仓库中另外存在 Release,不等于此 workflow 创建了它;没有 upload/release 步骤也不能声称上传过制品。这里没有实际仓库发布、Latest 标记或 tag 类型的事实声明。
5.4 可选增强:不是示例实现
如果未来需求变成“更快、更宽或可发布”,可以分别加能力;下面只是设计方向,不应倒填为本案例现状:
- 架构覆盖:为
x86_64-linux与可用的aarch64-linuxrunner 建 matrix,或明确配置 remote builder;每个矩阵单元必须选中并实际实现对应系统输出,配置可用的同架构 builder;例如nix build .#packages.aarch64-linux.both-blur,不能只把--all-systems留在--no-build求值里。 - 缓存:按锁文件、系统和 Nix 输入设计缓存键,明确缓存失效和信任边界;缓存命中只能改善速度,不能替代构建结果。
- 产物:只在确有调试或分发需要时上传日志、
nix path-info或经过审查的 package;artifact 不是 release,也不应包含凭据。 - Home Manager 检查:增加最小 consumer flake,验证 module import、选项求值和 dry-build,并把它与 package output check 分开命名。
- lint/format:若项目建立 Nix/Markdown 格式契约,再增加独立 job;示例没有对应 job,不能把 shell contract check 称为 lint。
- 发布自动化:若要让 tag 创建 release,新增独立、受保护的 release workflow,声明写权限、tag 条件、资产来源和审批;不要把当前 tag push check 误认为发布流水线。
- 运行时 smoke test:只有在可控图形 runner、虚拟 compositor 或真实测试主机准备好后,才把 Fcitx5/GTK/compositor 验证加入 CI;否则继续把它记录为部署后的人工/主机测试。
6. 故障排查清单#
6.1 先确认失败属于哪一层
- 触发未发生:确认是 push 还是 PR,检查该 commit 是否包含
.github/workflows/nix.yml;示例没有分支过滤,所以不应先假设是main限制。 - checkout/Action 失败:查看固定 SHA 对应的 Action 版本、runner 镜像和权限;不要为绕过失败改成可变 tag。
- 求值失败:本地同一 revision 运行
nix flake check --all-systems --no-build,先区分语法/输入/系统属性错误和非致命 warning。 - 构建失败:本地逐个运行
nix build --no-link .#both-blur等六个 output,定位是某一变体、依赖下载、平台还是源文件问题;不要只看flake check。 - contract check 失败:把目标 derivation 实现与
flake.nix对照,检查主题是否缺文件、错误暴露未选主题、blur/solid 字段或 SVG opacity 不符。 - 怀疑 aarch64:不能用一个绿色 run 作结论;在有 aarch64 builder 的环境中选择对应系统输出并实际构建,并单独记录 builder、revision 和结果。
- 桌面显示异常:先确认 package 内容和 Fcitx5 搜索路径,再在真实会话 reload/restart Fcitx5;最后按 Hyprland、niri、KWin、GNOME 分别记录协议和视觉结果。
- 把 warning 当故障:
unknown flake output 'homeManagerModules'是可能出现的求值 warning;应追踪其 flake 兼容性,但不能用它掩盖真正的非零退出码。 - 询问是否自动发布:回看 workflow 是否有 release action 和写权限;本案例没有,因此应分别查询并报告 tag check 与 Release,不能假定 Release 已存在,不报告自动上传。
- 涉及凭据:不要把 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 的解释参考核验时的官方手册。
参考与延伸
- GitHub Actions workflow syntax ↗
- GitHub Actions security hardening ↗
- GitHub Actions 安全使用参考 ↗
- GitHub Actions 事件参考 ↗
- actions/checkout v7.0.1 ↗
- cachix/install-nix-action v31.11.1(第三方) ↗
- Nix flake check 官方手册 ↗
根据技术笔记重新整理,示例使用通用名称。