Ivan's Blog
项目实践

Flux Purr

Flux Purr 是 embedded firmware + React control console 的设备 mono-repo。README.md 将仓库组织为 firmware/、web/、tools/flux-purr-devd/、docs/specs/、docs/hardware/、.

  • Device mono-repo
  • Rust source quality
  • ESP32 firmware
  • shadcn/ui
  • Storybook accessibility
  • GitHub Actions scheduling
  • Worktree bootstrap

目录

技术栈

运行与交付

CI 工作流
  • ci-main.yml
  • ci.yml
  • label-gate.yml
  • notify-release-failure.yml
  • release-completion.yml
  • release-preparation.yml
  • release.yml
能力标记
  • bun_lock
  • cargo_toml
  • github_actions
  • package_json
JSON

Purpose

Flux Purr 是 embedded firmware + React control console 的设备 mono-repo。README.md 将仓库组织为 firmware/、web/、tools/flux-purr-devd/、docs/specs/、docs/hardware/、.github/ 和 scripts/;它是从硬件研究、固件 bring-up 到控制台、host tooling 与 release workflow 的完整产品,而非单一前端或 crate。

Taxonomy

  • Tags: Device mono-repo, Rust source quality, ESP32 firmware, shadcn/ui, Storybook accessibility, GitHub Actions scheduling, Worktree bootstrap

Repository map

  • firmware/ 是 Rust no_std ESP32-S3 domain crate,firmware/Cargo.toml 暴露 flux-purr 主 binary 和 host-preview binaries。
  • web/ 是 Vite/React/shadcn/ui 控制台,web/components.json 记录 new-york style、Tailwind CSS variables、Lucide icon library 和 @/components/ui aliases。
  • tools/flux-purr-devd/ 提供本机 daemon 与 flux-purr CLI;它与 firmware、Web 共同消费产品 identity。
  • VERSION、scripts/product-version.py 与 .github/scripts/release_chain.py 分别定义版本解析及 VERSION-only preparation / main-merge provenance 验证。
  • .github/quality-gates.json、label-gate.yml、release-preparation.yml、release-completion.yml、release.yml 与 notify-release-failure.yml 构成统一产品 release surface,不再以过期的 firmware/Web 独立发布表示产品身份。

Runtime and delivery

根 package.json 编排 firmware fmt/clippy/build、Web check/build、Storybook build 与 e2e;mcu-agentd.toml 声明配置化硬件操作。Web 保持独立的 Storybook、交互测试与 bundle build;firmware 与 UI 也不共享一个“build succeeded”的含义。Worktree bootstrap 恢复 root/web Bun 依赖和 Cargo fetch 预热,同时把 Playwright browser cache 与系统前置条件保留为 warning 或手工修复路径(scripts/bootstrap-dev.sh、scripts/test-worktree-bootstrap.sh)。

根 VERSION 是唯一产品版本权威。开发、测试、firmware、flux-purr-devd、CLI、Web build、bundle、manifest 与 release 都只读它;开发 identity 是 nextPatch(VERSION)-dev.<short-sha>,不会写回文件。Cargo/NPM package metadata、Git tag、PR label、snapshot、环境变量和旧 manifest 都不得成为数字版本来源或 fallback(docs/specs/version-source/SPEC.md、scripts/product-version.py)。

产品 PR 在完整 PR CI 与 Validate PR labels 成功后,由 Prepare product version 在同一分支追加只修改 VERSION 的 preparation commit。其 trailers 固化 source SHA、产品版本和已验证 intent;Release completion 核验准备提交、base、labels 与源提交 CI,PR 仍以普通 merge commit 进入 main。Release Product 从合并后的 main 读取锁定 VERSION,一次构建、发布、校验 Web、public demo、firmware 与 host-tools 资产,并从同一归档各部署一次。v0.23.1 是这条完整链路已验证发布;recovery 只能重发同一 main commit/version(docs/adr/0005-pr-local-version-preparation.md、docs/adr/0006-tag-reservation-and-legacy-release-reconciliation.md、.github/workflows/release.yml)。

当 GitHub Actions 需要在 signed-commit 规则下创建该 preparation commit 时,workflow 将写入权限限定为 contents: write,并按 PR/check metadata 验证需要声明 pull-requests: read、checks: read,再使用 job 的 GITHUB_TOKEN 调用 GraphQL createCommitOnBranch。mutation 将 source SHA 作为 expectedHeadOid,只添加 VERSION,并在返回 OID 前后核对 PR branch head;因此并发 source 更新会失败,而不会被静默覆盖。它不配置 RELEASE_GPG_PRIVATE_KEY 或 RELEASE_GPG_PASSPHRASE。无论 preparation 是刚创建还是已存在,后续都验证 branch head、单父/仅 VERSION/trailers provenance,并在有界重试后要求 GitHub commit verification.verified 为真;任一检查失败即停止,而不降低 signed-commit 规则(.github/workflows/release-preparation.yml、.github/scripts/release_chain.py、.github/scripts/test-release-workflows.sh、docs/solutions/release/github-native-verified-version-commit.md)。

Architecture notes

可复用的架构记忆是“双运行面 + 共享验收 + 单一产品 identity”:firmware 负责 ESP32-S3 front-panel 行为,Web 负责控制台和组件状态,tools/flux-purr-devd/ 负责本机控制面,docs/specs/ 持有可执行验收合同。host-preview binary 在无板条件下提供固件早期验证。

当工程同时有 firmware、daemon、CLI、Web 与 release manifest 时,不能让各 consumer 从 package manifest 或环境变量各自重算版本。此仓库通过 scripts/product-version.py 集中派生稳定/RC、开发 qualifier、channel、source SHA 与 build ID,再投递给各 runtime 与 asset identity(docs/specs/version-source/IMPLEMENTATION.md)。

Workflow and quality gates

.github/quality-gates.json 要求 signed commits、PR-only main 与 workflow-backed checks。每个 PR 必须且仅能有一个 type:* 和一个 channel:*;Validate PR labels 是 merge 前 required check,但 labels 只表达发布意图,不能计算或覆盖 VERSION 数字内容(.github/workflows/label-gate.yml、docs/adr/0003-version-file-is-the-product-version-source.md)。

Validate PR labels 与 Release completion 是每 PR 的 required gate:它们分别使用 label-gate-<pr> 和 release-completion-<pr> concurrency group,并设 queue: max、不设 cancel-in-progress: true,使已排队的 gate 以非抢占方式完成。每个 job 都在 trusted base revision github.event.pull_request.base.sha || github.sha checkout;它以 pull-requests: read 的 job token 在运行时调用 gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.labels',把当前 labels JSON 传入 validator。这样 gate 不会信任可能在排队期间过时的 event payload labels,也不会为读取 labels 执行不可信 PR 内容(.github/workflows/label-gate.yml、.github/workflows/release-completion.yml)。

Rust source-quality gate 独立于产品 release gate,按四层执行:cargo fmt --all -- --check 负责格式;firmware 与 devd 的 crate-local clippy.toml 配合 -D warnings、too_many_lines、too_many_arguments、excessive_nesting 负责语义 lint;cargo +esp clippy 在 Xtensa target 和 12V/20V/28V feature matrix 上覆盖 target-only code;tools/rust-style-check/ 使用 syn 检查入口文件预算、inline tests 和结构性 allow/expect。这些入口由 package.json、scripts/check-firmware-fmt.sh、scripts/check-firmware-clippy.sh、scripts/check-firmware-clippy-xtensa.sh 与 scripts/check-rust-style.sh 编排。100/7/4 是当前 firmware/devd 的 crate-local 配置和 checker profile,不是跨项目阈值;零结构债务和禁止结构性豁免是该仓库的本地边界,也不替代 target-aware lint(firmware/clippy.toml、tools/flux-purr-devd/clippy.toml、tools/rust-style-check/src/main.rs、docs/solutions/engineering/rust-style-debt-remediation.md)。

PR #102 已于 2026-09-09 合并到 f34dffcac6b3385237e0fe6a31d21f52867b0bc1;其 .github/scripts/test-release-workflows.sh 直接断言两个 required workflow 都含 queue: max、不含 cancel-in-progress: true,并使用当前 labels API snapshot。该验证也记录了本地 actionlint 1.7.12 尚不识别该 GitHub 已文档化语法:在 GitHub Actions concurrency documentation 与 runtime 支持明确之前,它们是权威;兼容性进展跟踪在 actionlint #687。

完整 PR CI 只针对产品源提交运行一次;preparation commit 只做结构验证,CI Main 验证 normal merge relation。带有效 preparation 的 merge 才可进入产品发布。type:docs 与 type:skip non-product merge 使 Release Product 成功 skip,不发布、不部署,也不触发失败通知(.github/workflows/release.yml、.github/workflows/notify-release-failure.yml)。发布失败后,预先锁定在 main/VERSION 的版本必须通过同 SHA recovery 收口,不能计算 successor 或把多次变更压缩为一个 release(docs/specs/version-source/SPEC.md)。

Reusable decisions

  • 将 Rust source quality 拆成格式、语义 lint、源码结构和 target/features coverage 四层;入口脚本分别表达这些层,避免把 rustfmt 或 host Clippy 当成完整质量证明。
  • 把函数行数、参数数和嵌套深度放进 crate-local clippy.toml,让 firmware 与 host tooling 可以共享方向但独立演进;当前 100/7/4 只代表 Flux Purr 的参考 profile。
  • 对 firmware 入口同时保留 host feature matrix 与 Xtensa target matrix;target-only code 必须在实际 release target 上 lint,不能用 host 结果替代。
  • 用 syn checker 约束 source shape 和结构性豁免边界,配合按职责拆分入口文件;不设全仓文件长度阈值,也不通过 allow/expect 或宏隐藏结构债务。
  • 将 components.json 与 Storybook a11y 保持在 Web 子项目,避免 shadcn/ui 约定扩散至 firmware。
  • host preview binary 是硬件项目降低验证成本的有效模式。
  • Worktree bootstrap 负责 repo-managed dependency recovery,但不应在 hook 中隐式进行不可控外部下载。
  • 用一个 checked-in 产品版本文件作为唯一权威,让 tags、package metadata、labels、snapshots 与环境变量退回为派生或 intent 信息。
  • 用同一个 resolver 为 runtime、CLI、bundle、Web build-info 和 manifest 提供 identity,避免多语言 mono-repo 版本漂移。
  • 将版本写入置于同一产品 PR、完整 CI 之后且 normal merge 之前:preparation 只改 VERSION 并携带可验证 provenance。
  • 对受 signed-commit 规则保护且由 GitHub Actions 创建的 preparation,使用 GitHub-native createCommitOnBranch、GITHUB_TOKEN 与 expectedHeadOid,而非请求或保存仓库 GPG 私钥/口令;created 与 existing 两种状态都必须验证 GitHub 签名。
  • 保留 label gate 作为 required intent gate;自动 patch 与受控 exact 只决定写入操作,写入后版本文件独占数字版本事实。
  • 普通 PR candidate 可按最新提交取消过期工作;Validate PR labels 与 Release completion 这类 required gate 则用 per-PR queue: max 非抢占排队,并在 job runtime 取得当前 labels snapshot。
  • 当 actionlint 版本落后 GitHub workflow syntax 时,不为静态工具绿灯删除 required-gate 调度语义;以 GitHub 官方文档、runtime 和项目 workflow fixture 作为临时权威,并保留 issue 跟踪。
  • 从 normal main merge 一次构建、校验、发布并部署归档;发布失败时只允许同 SHA recovery。

Risks and open questions

  • 主分支必须保留 merge commit,且 Release completion 与 Validate PR labels 同为远端 required checks;否则无法证明 preparation 仍是受保护合并的一部分。
  • 版本解析、preparation diff/provenance、main merge relation 与 asset manifest 必须一起验证;只检查 Web display 或 Git tag 会漏掉跨 consumer 漂移。
  • tag reservation 需要在 preparation 前和 release build 前重复检查;已有 tag 只有精确指向同一 main merge 时才可被 recovery 复用。
  • GitHub 的签名状态可能晚于 mutation 返回;不能把 existing preparation 或返回的 OID 当作签名证明,必须在有界重试后 fail closed。
  • queue: max 最多保留 100 个 pending run,且 FIFO 以开始等待 concurrency group 的时间计;不要把 dispatch 时间当成严格全序,也不要与 cancel-in-progress: true 组合。
  • label snapshot 必须随 required gate 的实际执行重新读取;只以事件 payload 或 preparation 前的 labels 推断 release intent,会让延迟 gate 对当前 PR 状态作出过时判断。

Evidence index

  • README.md
  • firmware/Cargo.toml
  • firmware/clippy.toml
  • web/package.json
  • package.json
  • tools/flux-purr-devd/Cargo.toml
  • tools/flux-purr-devd/clippy.toml
  • tools/rust-style-check/Cargo.toml
  • tools/rust-style-check/src/main.rs
  • web/components.json
  • web/.storybook/main.ts
  • mcu-agentd.toml
  • VERSION
  • scripts/product-version.py
  • .github/scripts/release_chain.py
  • .github/quality-gates.json
  • .github/workflows/label-gate.yml
  • .github/workflows/release-preparation.yml
  • .github/workflows/release-completion.yml
  • .github/workflows/release.yml
  • .github/workflows/notify-release-failure.yml
  • .github/scripts/test-release-workflows.sh
  • scripts/check-firmware-fmt.sh
  • scripts/check-firmware-clippy.sh
  • scripts/check-firmware-clippy-xtensa.sh
  • scripts/check-rust-style.sh
  • scripts/bootstrap-dev.sh
  • scripts/test-worktree-bootstrap.sh
  • docs/specs/version-source/SPEC.md
  • docs/specs/version-source/IMPLEMENTATION.md
  • docs/adr/0003-version-file-is-the-product-version-source.md
  • docs/adr/0005-pr-local-version-preparation.md
  • docs/adr/0006-tag-reservation-and-legacy-release-reconciliation.md
  • docs/solutions/release/github-native-verified-version-commit.md
  • docs/solutions/engineering/rust-style-debt-remediation.md
  • docs/guides/flux-purr-developer-policy.md
  • Flux Purr PR #102
  • GitHub Actions concurrency documentation
  • actionlint issue #687