Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 14 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,13 @@ jobs:

- uses: dtolnay/rust-toolchain@stable

- name: Generate dependency lockfile
run: cargo generate-lockfile

- uses: swatinem/rust-cache@v2
with:
shared-key: "cli-ci-v2-${{ matrix.cache_key }}"
cache-bin: false
save-if: ${{ github.event_name != 'pull_request' }}
# PRs restore trusted caches but never publish merge-ref artifacts.
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
cache-on-failure: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}

- name: Run CLI and ACP tests on macOS
if: runner.os == 'macOS'
Expand All @@ -133,7 +132,6 @@ jobs:
rust-build-check:
name: Rust Build Check (${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: frontend-build
env:
# Keep the workspace check plus desktop test profiles within hosted-runner disk limits.
CARGO_INCREMENTAL: "0"
Expand All @@ -154,15 +152,11 @@ jobs:
shell: pwsh
run: ./scripts/ci/setup-openssl-windows.ps1

- name: Download frontend build artifacts
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: dist

- name: Create mobile-web dist directory (workaround for Tauri)
# Tauri code generation only requires its configured resource roots to
# exist during check/test; distributable assets remain frontend-build's owner.
- name: Create Tauri resource directories
shell: bash
run: mkdir -p src/mobile-web/dist
run: mkdir -p dist src/mobile-web/dist

- name: Install Linux system dependencies (Tauri)
if: runner.os == 'Linux'
Expand Down Expand Up @@ -197,15 +191,14 @@ jobs:

- uses: dtolnay/rust-toolchain@stable

- name: Generate dependency lockfile
run: cargo generate-lockfile

- uses: swatinem/rust-cache@v2
with:
shared-key: "ci-check-v3-${{ runner.os }}-no-cargo-bin-v1"
cache-bin: false
# PR caches are scoped to merge refs; main pushes own shared cache refreshes.
save-if: ${{ github.event_name != 'pull_request' }}
# PR caches are scoped to merge refs; trusted main pushes own shared
# refreshes and retain completed dependency builds after late test failures.
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
cache-on-failure: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}

- name: Check compilation
run: cargo check --locked --workspace
Expand All @@ -224,7 +217,7 @@ jobs:
run: cargo test --locked -p bitfun-relay-service

- name: Run subscription authentication tests
run: cargo test --locked -p bitfun-ai-adapters --features subscription-auth subscription_auth
run: cargo test --locked -p bitfun-ai-adapters --features subscription-auth --lib subscription_auth

# File watching is backed by a different OS API on every platform
# (ReadDirectoryChangesW / FSEvents / inotify), so watch registration
Expand All @@ -237,14 +230,14 @@ jobs:
# blocking unrelated work.
- name: Run file watch contract tests
if: runner.os != 'macOS'
run: cargo test --locked -p bitfun-services-integrations --features file-watch
run: cargo test --locked -p bitfun-services-integrations --no-default-features --features file-watch --test file_watch_contracts

# Search tools resolve paths and symlinks directly, which also differs
# across platforms. Scoped to the search module: the glob tests in this
# crate fail on Windows independently of this branch (walk-root
# derivation treats separators differently) and need their own fix.
- name: Run search tool tests
run: "cargo test --locked -p tool-runtime search::"
run: "cargo test --locked -p tool-runtime --lib search::"

# ── Frontend: build ────────────────────────────────────────────────
frontend-build:
Expand Down Expand Up @@ -317,10 +310,3 @@ jobs:

- name: Build mobile web
run: pnpm run build:mobile-web

- name: Upload frontend build artifacts
uses: actions/upload-artifact@v6
with:
name: frontend-dist
path: dist
retention-days: 1
9 changes: 9 additions & 0 deletions docs/architecture/rust-build-dependency-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ Cargo target gate 语义见

CI 负责 workspace 级检查、真实产品 feature 组合、跨平台、完整测试和最终产品构建。本地未执行的宽泛验证必须标记为未执行或 CI 覆盖,不能表述为本地通过。CI 失败时再按失败路径复现对应重命令,不要求每次本地改动预跑全部构建。

### 7.1 Hosted CI 关键路径与缓存

- 验证 job 只依赖自身的编译期前置条件。Tauri `check`/`test` 只要求配置中的前端和资源目录存在时,Rust job 自行创建空目录,不等待或传递可发布前端产物;真实静态资源仍由前端构建和产品打包 owner 负责。
- Pull Request 可以恢复可信分支产生的 Cargo 缓存,但不得写入 merge-ref 缓存。只有可信 `main` push 可以保存共享缓存;若依赖编译已经完成而后段测试失败,允许该可信构建保存依赖缓存,避免下一次跨平台构建无谓冷启动。
- 未先修改 Cargo manifest 的 PR/main 验证 job 以仓库提交的 `Cargo.lock` 为唯一解析结果并通过 `--locked` 验证,不在 cache restore 前重新生成 lockfile。依赖解析更新必须作为可评审的源码变更提交,不能让同一 commit 因上游兼容版本发布而自然产生新的 cache key;先改写版本号的发布 job 不属于该前提。
- 缓存只承载可复用依赖产物,不为追求命中率启用 workspace crate 或 incremental artifact 缓存;缓存容量、失效粒度和可信边界优先于单次命中率。
- focused test 同时选择最小 Cargo target(如 `--lib` 或 `--test <target>`)和必要 feature;仅使用名称过滤不能阻止无关 test target 进入编译图。
- 只有具备独立 owner、平台矩阵或失败归因价值的验证才拆成 job。顺序执行但共享同一依赖图的命令优先留在既有 job 中,避免用新增 job 重复结账工具链、checkout 和缓存恢复成本。

## 8. 评审证据

依赖治理 PR 按改动选择证据,不机械执行全部命令:
Expand Down
78 changes: 77 additions & 1 deletion scripts/check-github-config.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import assert from 'node:assert/strict';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
Expand All @@ -8,6 +15,10 @@ import { fileURLToPath } from 'node:url';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const scriptPath = path.join(repoRoot, 'scripts/check-github-config.mjs');
const requireFromWebUi = createRequire(
path.join(repoRoot, 'src/web-ui/package.json'),
);
const yaml = requireFromWebUi('yaml');

function createRepo({ workflow, nodeVersionFile }) {
const root = mkdtempSync(path.join(tmpdir(), 'bitfun-github-config-'));
Expand Down Expand Up @@ -191,3 +202,68 @@ jobs:
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /GitHub YAML config check passed/);
});

test('keeps Rust CI independent, restore-only on PRs, and target-focused', () => {
const workflow = yaml.parse(
readFileSync(path.join(repoRoot, '.github/workflows/ci.yml'), 'utf8'),
);
const rustJob = workflow.jobs['rust-build-check'];
const frontendJob = workflow.jobs['frontend-build'];
const trustedMain =
"${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}";

assert.equal(
rustJob.needs,
undefined,
'Rust validation must not wait for the frontend build',
);
assert.equal(
rustJob.steps.some((step) => step.uses?.startsWith('actions/download-artifact@')),
false,
'Rust validation must not download frontend artifacts',
);
assert.match(
rustJob.steps.find((step) => step.name === 'Create Tauri resource directories')
?.run ?? '',
/mkdir -p dist src\/mobile-web\/dist/,
);
assert.equal(
frontendJob.steps.some(
(step) =>
step.uses?.startsWith('actions/upload-artifact@') &&
step.with?.name === 'frontend-dist',
),
false,
'The frontend build must not upload an artifact with no consumer',
);

for (const jobName of ['cli-test', 'rust-build-check']) {
const job = workflow.jobs[jobName];
const cache = job.steps.find((step) =>
step.uses?.startsWith('swatinem/rust-cache@'),
);
assert.equal(
job.steps.some((step) => step.run?.includes('cargo generate-lockfile')),
false,
`${jobName} must consume the committed Cargo.lock`,
);
assert.equal(cache?.with?.['save-if'], trustedMain);
assert.equal(cache?.with?.['cache-on-failure'], trustedMain);
}

const commandByStep = new Map(
rustJob.steps.map((step) => [step.name, step.run]),
);
assert.equal(
commandByStep.get('Run subscription authentication tests'),
'cargo test --locked -p bitfun-ai-adapters --features subscription-auth --lib subscription_auth',
);
assert.equal(
commandByStep.get('Run file watch contract tests'),
'cargo test --locked -p bitfun-services-integrations --no-default-features --features file-watch --test file_watch_contracts',
);
assert.equal(
commandByStep.get('Run search tool tests'),
'cargo test --locked -p tool-runtime --lib search::',
);
});
Loading