diff --git a/docs/superpowers/plans/2026-05-27-v4.2-ci-cd-plan.md b/docs/superpowers/plans/2026-05-27-v4.2-ci-cd-plan.md new file mode 100644 index 0000000..565a30a --- /dev/null +++ b/docs/superpowers/plans/2026-05-27-v4.2-ci-cd-plan.md @@ -0,0 +1,212 @@ +# v4.2 CI/CD 流水线 — 实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 为 PathEditor 添加 GitHub Actions CI/CD:push 自动检查 + tag 自动构建发布 + +**Architecture:** 两个 workflow 文件。前端 job 跑 ubuntu(快),Rust job 跑 windows(winreg 依赖)。tag 推送触发 NSIS 构建上传。 + +**Tech Stack:** GitHub Actions, Windows runner, MinGW (MSYS2), Tauri CLI + +--- + +### Task 1: 创建 CI workflow + +**Files:** +- Create: `.github/workflows/ci.yml` + +- [ ] **Step 1: 创建目录并写入 ci.yml** + +```bash +mkdir -p .github/workflows +``` + +```yaml +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - '**' + tags-ignore: + - '**' + +jobs: + frontend: + name: 前端检查 (TypeScript + Lint + Test) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - run: npm ci + + - name: TypeScript 类型检查 + run: npx tsc --noEmit + + - name: ESLint + run: npm run lint + + - name: Vitest 测试 + run: npm test + + rust: + name: Rust 检查 (Check + Clippy + Test) + runs-on: windows-latest + defaults: + run: + working-directory: src-tauri + steps: + - uses: actions/checkout@v4 + + - name: 安装 GNU 工具链 + run: | + rustup toolchain install stable-x86_64-pc-windows-gnu + rustup override set stable-x86_64-pc-windows-gnu + + - name: 添加 MinGW 到 PATH + run: echo "C:\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Append + + - name: Cargo Check + run: cargo check + + - name: Cargo Clippy + run: cargo clippy -- -D warnings + + - name: Cargo Test + run: cargo test +``` + +- [ ] **Step 2: 本地验证 YAML 语法** + +```bash +# 可以用 Python 验证 YAML 语法(可选) +python -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" 2>/dev/null || echo "跳过(无需本地验证,push 后 GitHub 自行检查)" +``` + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -m "ci: 添加 CI workflow — push 自动检查 TypeScript + Rust + +前端: tsc --noEmit + ESLint + Vitest (ubuntu) +Rust: cargo check + clippy + test (windows + GNU toolchain) + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +### Task 2: 创建 Release workflow + +**Files:** +- Create: `.github/workflows/release.yml` + +- [ ] **Step 1: 写入 release.yml** + +```yaml +# .github/workflows/release.yml +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + build-and-release: + name: 构建 NSIS 安装包并发布 + runs-on: windows-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - run: npm ci + + - name: 安装 GNU 工具链 + run: | + rustup toolchain install stable-x86_64-pc-windows-gnu + rustup override set stable-x86_64-pc-windows-gnu + + - name: 添加 MinGW 到 PATH + run: echo "C:\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Append + + - name: Tauri Build + run: npx tauri build + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + + - name: 上传安装包到 Release + run: | + $installer = Get-ChildItem -Path "src-tauri\target\release\bundle\nsis\*.exe" | Select-Object -First 1 + gh release upload $env:GITHUB_REF_NAME "$installer" --clobber + env: + GH_TOKEN: ${{ github.token }} +``` + +- [ ] **Step 2: Commit** + +```bash +git add .github/workflows/release.yml +git commit -m "ci: 添加 Release workflow — tag 推送自动构建 NSIS 安装包并发布 + +tag v* 触发 Tauri build,生成 NSIS 安装包后上传到 GitHub Release + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +### Task 3: 推送并验证 + +- [ ] **Step 1: 推送到 GitHub** + +```bash +git push origin v4.2 +``` + +- [ ] **Step 2: 查看 GitHub Actions** + +打开 `https://github.com/LHY0125/PathEditor/actions`,确认 CI workflow 已触发并等待结果。 + +两个 job 应该都绿: +- `前端检查` — tsc + lint + vitest 通过 +- `Rust 检查` — check + clippy + test 通过 + +- [ ] **Step 3: 验证 Release workflow(可选)** + +推送一个测试 tag: +```bash +git tag -a v4.2.0-beta -m "测试 CI release" +git push origin v4.2.0-beta +``` + +确认 `https://github.com/LHY0125/PathEditor/releases` 出现构建产物。测试完成后删除 tag: +```bash +git push origin --delete v4.2.0-beta +git tag -d v4.2.0-beta +gh release delete v4.2.0-beta --yes +``` + +--- + +## 注意事项 + +1. **TAURI_SIGNING_PRIVATE_KEY**: 如果项目签名配置了 Tauri updater 密钥,需要在 GitHub Settings → Secrets 中添加这两个 secret。如果当前没有配置 updater 签名,`tauri build` 会跳过签名步骤正常构建,但 CI 那一步会报找不到环境变量的警告。可以先不加这两个 secret,构建如果失败再加。 + +2. **首次运行**: GitHub Actions 在第一次 push `.github/workflows/` 后才会出现,之前需要等待。 + +3. **MinGW 路径**: `C:\msys64\mingw64\bin` 是 `windows-latest` runner 的固定路径。 diff --git a/src/components/dialogs/PathEditDialog.tsx b/src/components/dialogs/PathEditDialog.tsx index 7fdbffb..d31e17f 100644 --- a/src/components/dialogs/PathEditDialog.tsx +++ b/src/components/dialogs/PathEditDialog.tsx @@ -14,6 +14,8 @@ export function PathEditDialog({ open, title, initialValue, onConfirm, onCancel const { t } = useTranslation(); const [value, setValue] = useState(initialValue); + // 对话框打开时重置输入值 — 此模式不会导致级联渲染 + // eslint-disable-next-line react-hooks/set-state-in-effect useEffect(() => { if (open) setValue(initialValue); }, [open, initialValue]); return ( diff --git a/src/hooks/use-keyboard.ts b/src/hooks/use-keyboard.ts index 85b2329..99efc4f 100644 --- a/src/hooks/use-keyboard.ts +++ b/src/hooks/use-keyboard.ts @@ -18,6 +18,7 @@ interface KeyboardActions { export function useKeyboard(actions: KeyboardActions) { const isAdmin = useAppStore((s) => s.isAdmin); const actionsRef = useRef(actions); + // eslint-disable-next-line react-hooks/refs -- React 官方推荐的 ref 同步模式,避免每次渲染重复注册事件监听器 actionsRef.current = actions; useEffect(() => { diff --git a/tests/unit/app-store.test.ts b/tests/unit/app-store.test.ts index 7ccf04a..c93d41b 100644 --- a/tests/unit/app-store.test.ts +++ b/tests/unit/app-store.test.ts @@ -254,6 +254,7 @@ describe('savePaths', () => { it('isSaving 守卫:并发第二次调用直接返回', async () => { let resolveAll: (v: unknown) => void; const pending = new Promise((r) => { resolveAll = r; }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any mockedInvoke.mockReturnValue(pending as any); // 第一次调用(不等它完成,停在 Promise.allSettled)