fix(ci): 质量检查 - 修复增量格式检查 EPIPE 与连线代码格式
This commit is contained in:
25
.github/workflows/quality.yml
vendored
25
.github/workflows/quality.yml
vendored
@@ -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
|
||||
|
||||
45
web/scripts/check-changed-formatting.mjs
Normal file
45
web/scripts/check-changed-formatting.mjs
Normal file
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
97
web/test/ci-formatting.test.mjs
Normal file
97
web/test/ci-formatting.test.mjs
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user