From d6cbd743f9e73469b2f9abc23e5227bd19bab768 Mon Sep 17 00:00:00 2001 From: Kefei Qian Date: Tue, 14 Jul 2026 17:19:52 +0800 Subject: [PATCH 1/3] ci: skip unaffected validation jobs Classify changed paths before running CI so Rust and TUI jobs only execute when their inputs change, while workflow changes still exercise both job families. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c127c9c-ac00-4a90-be0f-4357720a6a8d --- .github/workflows/ci.yml | 62 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f9c079d..f36254e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,8 @@ # Continuous integration for pushes to `main` and every pull request. # # Runs the same checks documented in AGENTS.md and used locally, split into two -# parallel jobs so a failure points at either the Rust core or the nested TUI: +# jobs gated by changed paths so docs/skills-only PRs do not spend time on +# unrelated build matrices: # - rust: cargo fmt --check, clippy (warnings denied), build, and test. # - tui: cargo xtask tui-typecheck and tui-test (Bun + vscode-jsonrpc). # @@ -26,8 +27,65 @@ concurrency: cancel-in-progress: true jobs: + changes: + name: Detect changed areas + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + rust: ${{ steps.paths.outputs.rust }} + tui: ${{ steps.paths.outputs.tui }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Classify changed paths + id: paths + shell: bash + run: | + set -euo pipefail + + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + base_ref="origin/${{ github.base_ref }}" + git fetch --no-tags origin "${{ github.base_ref }}" + base="$(git merge-base "$base_ref" HEAD)" + head="HEAD" + else + base="${{ github.event.before }}" + head="${{ github.sha }}" + if [[ "$base" =~ ^0+$ ]] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then + base="$(git rev-list --max-parents=0 "$head" | tail -1)" + fi + fi + + git diff --name-only "$base" "$head" > changed-files.txt + rust=false + tui=false + + while IFS= read -r changed_path; do + case "$changed_path" in + .cargo/*|.github/workflows/ci.yml|Cargo.lock|Cargo.toml|build.rs|main.rs|migrations/*|scripts/*|src/*|tests/*|xtask/*) + rust=true + ;; + esac + + case "$changed_path" in + .cargo/*|.github/workflows/ci.yml|Cargo.lock|Cargo.toml|scripts/*|tui/*|xtask/*) + tui=true + ;; + esac + done < changed-files.txt + + echo "Changed files:" + cat changed-files.txt + echo "rust=$rust" >> "$GITHUB_OUTPUT" + echo "tui=$tui" >> "$GITHUB_OUTPUT" + echo "Rust checks: $rust" + echo "TUI checks: $tui" + rust: name: Rust (fmt, clippy, build, test) + needs: changes + if: needs.changes.outputs.rust == 'true' runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -47,6 +105,8 @@ jobs: tui: name: TUI (typecheck, test) + needs: changes + if: needs.changes.outputs.tui == 'true' runs-on: ubuntu-latest timeout-minutes: 20 steps: From 963fa1a763e37f27aebe5979935e83daac340107 Mon Sep 17 00:00:00 2001 From: Kefei Qian Date: Tue, 14 Jul 2026 17:26:30 +0800 Subject: [PATCH 2/3] ci: add packaging path validation Run Node packaging tests when npm or Homebrew packaging inputs change, keeping packaging-only PRs out of unrelated Rust/TUI jobs while still validating release distribution helpers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c127c9c-ac00-4a90-be0f-4357720a6a8d --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f36254e6..3d5866a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ # unrelated build matrices: # - rust: cargo fmt --check, clippy (warnings denied), build, and test. # - tui: cargo xtask tui-typecheck and tui-test (Bun + vscode-jsonrpc). +# - packaging: Node's built-in test runner for npm/Homebrew packaging helpers. # # Authenticity controls mirror the other workflows: read-only permissions and # third-party actions pinned by version. This workflow only reads the repo and @@ -32,6 +33,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 outputs: + packaging: ${{ steps.paths.outputs.packaging }} rust: ${{ steps.paths.outputs.rust }} tui: ${{ steps.paths.outputs.tui }} steps: @@ -58,6 +60,7 @@ jobs: fi git diff --name-only "$base" "$head" > changed-files.txt + packaging=false rust=false tui=false @@ -73,12 +76,20 @@ jobs: tui=true ;; esac + + case "$changed_path" in + .github/workflows/ci.yml|.github/workflows/homebrew-publish.yml|.github/workflows/npm-publish.yml|packaging/*) + packaging=true + ;; + esac done < changed-files.txt echo "Changed files:" cat changed-files.txt + echo "packaging=$packaging" >> "$GITHUB_OUTPUT" echo "rust=$rust" >> "$GITHUB_OUTPUT" echo "tui=$tui" >> "$GITHUB_OUTPUT" + echo "Packaging checks: $packaging" echo "Rust checks: $rust" echo "TUI checks: $tui" @@ -123,3 +134,24 @@ jobs: run: cargo xtask tui-typecheck - name: Test run: cargo xtask tui-test + + packaging: + name: Packaging (node tests) + needs: changes + if: needs.changes.outputs.packaging == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - name: npm packaging tests + run: node --test + working-directory: packaging/npm/kqode + - name: npm helper tests + run: node --test + working-directory: packaging/npm/scripts + - name: Homebrew packaging tests + run: node --test + working-directory: packaging/homebrew From a55deeaf33fb04a568ecae4cfe86d85e5e3af98f Mon Sep 17 00:00:00 2001 From: Kefei Qian Date: Tue, 14 Jul 2026 17:29:03 +0800 Subject: [PATCH 3/3] test(packaging): create valid zip fixtures Prefer the zip command for Windows archive fixtures so Linux CI exercises the same unzip path as production packaging tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c127c9c-ac00-4a90-be0f-4357720a6a8d --- packaging/npm/scripts/test/generate.test.cjs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packaging/npm/scripts/test/generate.test.cjs b/packaging/npm/scripts/test/generate.test.cjs index 9af37ab3..5b3b939f 100644 --- a/packaging/npm/scripts/test/generate.test.cjs +++ b/packaging/npm/scripts/test/generate.test.cjs @@ -22,13 +22,16 @@ function makeArchive(dir, relName, ext, binName, contents) { const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'kqode-stage-')); fs.writeFileSync(path.join(stage, binName), contents); const archivePath = path.join(dir, `${relName}.${ext}`); - const args = + const result = ext === 'tar.gz' - ? ['-czf', archivePath, '-C', stage, binName] - : ['-a', '-cf', archivePath, '-C', stage, binName]; - const status = spawnSync('tar', args, { stdio: 'ignore' }).status; + ? spawnSync('tar', ['-czf', archivePath, '-C', stage, binName], { stdio: 'ignore' }) + : spawnSync('zip', ['-q', archivePath, binName], { cwd: stage, stdio: 'ignore' }); + const fallback = + ext === 'zip' && result.error + ? spawnSync('tar', ['-a', '-cf', archivePath, '-C', stage, binName], { stdio: 'ignore' }) + : result; fs.rmSync(stage, { recursive: true, force: true }); - if (status !== 0) return null; + if (fallback.error || fallback.status !== 0) return null; fs.writeFileSync(path.join(dir, `${relName}.sha256`), `${sha256(archivePath)} ${relName}.${ext}\n`); return archivePath; }