From 104396ba2fb98c9e6b0e9a284eaa16bc115962f1 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Sun, 20 Sep 2026 09:34:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=E8=B4=A8=E9=87=8F=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=20-=20=E4=BF=AE=E5=A4=8D=E5=A2=9E=E9=87=8F=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=20EPIPE=20=E4=B8=8E=E8=BF=9E=E7=BA=BF?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/quality.yml | 25 +---- web/scripts/check-changed-formatting.mjs | 45 +++++++++ .../canvas/canvas-connection-visibility.ts | 8 +- web/test/ci-formatting.test.mjs | 97 +++++++++++++++++++ 4 files changed, 145 insertions(+), 30 deletions(-) create mode 100644 web/scripts/check-changed-formatting.mjs create mode 100644 web/test/ci-formatting.test.mjs diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index f5bee3642..7ab092784 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -78,30 +78,7 @@ jobs: - name: Check formatting env: BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} - run: | - changed_files=() - repo_prefix="$(git rev-parse --show-prefix)" - while IFS= read -r -d '' file; do - case "$file" in - *.css|*.html|*.json|*.js|*.jsx|*.md|*.mdx|*.mjs|*.cjs|*.ts|*.tsx|*.yaml|*.yml) - base_path="${repo_prefix}${file}" - if git cat-file -e "$BASE_SHA:$base_path" 2>/dev/null \ - && ! diff -q \ - <(git show "$BASE_SHA:$base_path") \ - <(git show "$BASE_SHA:$base_path" | bunx prettier --ignore-path /dev/null --stdin-filepath "$file") \ - >/dev/null; then - echo "Skipping legacy unformatted file: $file" - continue - fi - changed_files+=("$file") - ;; - esac - done < <(git diff --relative --name-only --diff-filter=ACMR -z "$BASE_SHA" "$GITHUB_SHA" -- .) - if (( ${#changed_files[@]} > 0 )); then - bunx prettier --check "${changed_files[@]}" - else - echo "No changed web files require formatting checks." - fi + run: bun scripts/check-changed-formatting.mjs - name: Typecheck run: bun run typecheck - name: Lint retired Ant Design APIs diff --git a/web/scripts/check-changed-formatting.mjs b/web/scripts/check-changed-formatting.mjs new file mode 100644 index 000000000..1a3225354 --- /dev/null +++ b/web/scripts/check-changed-formatting.mjs @@ -0,0 +1,45 @@ +import { spawnSync } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import * as prettier from "prettier"; + +export async function checkChangedFormatting({ cwd = process.cwd(), baseSha = process.env.BASE_SHA, headSha = process.env.GITHUB_SHA || "HEAD", log = console.log } = {}) { + const git = (args) => { + const result = spawnSync("git", args, { cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error(`git ${args.join(" ")}: ${result.stderr}`); + return result.stdout; + }; + const head = git(["rev-parse", "--verify", `${headSha}^{commit}`]).trim(); + // 新分支/tag 的 before 为全零;首次提交没有父提交,改为检查整棵树。 + const base = baseSha && !/^0+$/.test(baseSha) ? git(["rev-parse", "--verify", `${baseSha}^{commit}`]).trim() : git(["rev-list", "--parents", "-n", "1", head]).trim().split(" ")[1]; + const files = (base ? git(["diff", "--relative", "--name-only", "--diff-filter=ACMR", "-z", base, head, "--", "."]) : git(["ls-tree", "-r", "--name-only", "-z", head, "--", "."])) + .split("\0") + .filter((file) => /\.(css|html|json|js|jsx|md|mdx|mjs|cjs|ts|tsx|yaml|yml)$/.test(file)); + const prefix = git(["rev-parse", "--show-prefix"]).trim(); + const previousFiles = new Set(base ? git(["ls-tree", "-r", "--name-only", "-z", base, "--", "."]).split("\0") : []); + const failures = []; + for (const file of files) { + const filepath = path.join(cwd, file); + const info = await prettier.getFileInfo(filepath, { ignorePath: path.join(cwd, ".prettierignore") }); + if (info.ignored) continue; + const options = { ...(await prettier.resolveConfig(filepath)), filepath }; + // 只允许明确的历史格式问题跳过;语法、Git、文件读取错误全部向上抛出。 + const previous = previousFiles.has(file) ? git(["show", `${base}:${prefix}${file}`]) : null; + if (previous !== null && !(await prettier.check(previous, options))) { + log(`Skipping legacy unformatted file: ${file}`); + continue; + } + if (!(await prettier.check(await readFile(filepath, "utf8"), options))) failures.push(file); + } + if (failures.length) throw new Error(`Code style issues found:\n${failures.join("\n")}\nRun Prettier with --write to fix.`); + log("Changed web files passed formatting checks."); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + checkChangedFormatting().catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/web/src/lib/canvas/canvas-connection-visibility.ts b/web/src/lib/canvas/canvas-connection-visibility.ts index 4e2e9e865..27b4c46a2 100644 --- a/web/src/lib/canvas/canvas-connection-visibility.ts +++ b/web/src/lib/canvas/canvas-connection-visibility.ts @@ -40,11 +40,7 @@ export function filterCanvasDisplayConnections( if (options.activeNodeId) focusedNodeIds.add(options.activeNodeId); options.selectedNodeIds?.forEach((nodeId) => focusedNodeIds.add(nodeId)); - return connections.filter(({ connection, from, to }) => - connection.id === options.selectedConnectionId - || focusedNodeIds.has(connection.fromNodeId) - || focusedNodeIds.has(connection.toNodeId) - || focusedNodeIds.has(from.id) - || focusedNodeIds.has(to.id), + return connections.filter( + ({ connection, from, to }) => connection.id === options.selectedConnectionId || focusedNodeIds.has(connection.fromNodeId) || focusedNodeIds.has(connection.toNodeId) || focusedNodeIds.has(from.id) || focusedNodeIds.has(to.id), ); } diff --git a/web/test/ci-formatting.test.mjs b/web/test/ci-formatting.test.mjs new file mode 100644 index 000000000..948eecce8 --- /dev/null +++ b/web/test/ci-formatting.test.mjs @@ -0,0 +1,97 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { checkChangedFormatting } from "../scripts/check-changed-formatting.mjs"; + +const temporaryDirectories = []; +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) rmSync(directory, { recursive: true, force: true }); +}); + +function fixture(initialFiles = {}) { + const root = mkdtempSync(path.join(os.tmpdir(), "canvas-ci-format-")); + temporaryDirectories.push(root); + const cwd = path.join(root, "web"); + mkdirSync(cwd); + const git = (...args) => { + const result = spawnSync("git", args, { cwd: root, encoding: "utf8" }); + if (result.status !== 0) throw new Error(result.stderr); + return result.stdout.trim(); + }; + const write = (name, content) => writeFileSync(path.join(cwd, name), content); + git("init", "-q"); + git("config", "user.email", "ci-test@example.invalid"); + git("config", "user.name", "CI fixture"); + write(".prettierrc.json", '{ "printWidth": 80 }\n'); + write(".prettierignore", "ignored.js\n"); + for (const [name, content] of Object.entries(initialFiles)) write(name, content); + const commit = () => { + git("add", "."); + git("-c", "commit.gpgsign=false", "commit", "-qm", "fixture"); + return git("rev-parse", "HEAD"); + }; + const baseSha = commit(); + const messages = []; + return { cwd, git, write, commit, baseSha, messages, check: (options = {}) => checkChangedFormatting({ cwd, baseSha, headSha: "HEAD", log: (message) => messages.push(message), ...options }) }; +} + +describe("changed-file formatting gate", () => { + test("checks files relative to web, supports spaces, and rejects a new formatting regression", async () => { + const repo = fixture({ "has space.ts": "const value = 1;\n" }); + repo.write("has space.ts", "const value=2;\n"); + repo.commit(); + await expect(repo.check()).rejects.toThrow("has space.ts"); + }); + + test("skips legacy formatting without creating a broken pipe", async () => { + const repo = fixture({ "legacy.ts": "const value=1;\n" }); + repo.write("legacy.ts", "const value=2;\n"); + repo.commit(); + await repo.check(); + expect(repo.messages).toContain("Skipping legacy unformatted file: legacy.ts"); + }); + + test("checks added files and respects prettierignore", async () => { + const repo = fixture(); + repo.write("ignored.js", "const invalid = ;"); + repo.write("added.ts", "const value=1;\n"); + repo.commit(); + await expect(repo.check()).rejects.toThrow("added.ts"); + repo.write("added.ts", "const value = 1;\n"); + await repo.check(); + }); + + test("syntax errors are failures, not legacy-format skips", async () => { + const repo = fixture({ "broken.ts": "const value = ;\n" }); + repo.write("broken.ts", "const value = 1;\n"); + repo.commit(); + await expect(repo.check()).rejects.toThrow(); + expect(repo.messages).toEqual([]); + }); + + test("invalid explicit base SHA fails closed", async () => { + const repo = fixture(); + await expect(repo.check({ baseSha: "f".repeat(40) })).rejects.toThrow("git rev-parse"); + }); + + test("zero before SHA compares the parent commit", async () => { + const repo = fixture({ "existing.ts": "const value = 1;\n" }); + repo.write("existing.ts", "const value=2;\n"); + repo.commit(); + await expect(repo.check({ baseSha: "0".repeat(40) })).rejects.toThrow("existing.ts"); + }); + + test("an initial commit checks the whole tree", async () => { + const repo = fixture({ "new.ts": "const value=1;\n" }); + await expect(repo.check({ baseSha: "0".repeat(40) })).rejects.toThrow("new.ts"); + }); + + test("deletions do not attempt to read missing files", async () => { + const repo = fixture({ "deleted.ts": "const value = 1;\n" }); + repo.git("rm", "web/deleted.ts"); + repo.commit(); + await repo.check(); + }); +});