mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-07-27 22:38:30 +08:00
fix: exclude .claude-pr snapshot from git staging (#1277)
This commit is contained in:
parent
f4d6a11de1
commit
0756f6ef2b
@ -1,5 +1,13 @@
|
|||||||
import { execFileSync } from "child_process";
|
import { execFileSync } from "child_process";
|
||||||
import { cpSync, existsSync, rmSync } from "fs";
|
import {
|
||||||
|
appendFileSync,
|
||||||
|
cpSync,
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
rmSync,
|
||||||
|
} from "fs";
|
||||||
|
import { dirname } from "path";
|
||||||
|
|
||||||
// Paths that are both PR-controllable and read from cwd at CLI startup.
|
// Paths that are both PR-controllable and read from cwd at CLI startup.
|
||||||
//
|
//
|
||||||
@ -20,6 +28,30 @@ const SENSITIVE_PATHS = [
|
|||||||
".husky",
|
".husky",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const CLAUDE_PR_EXCLUDE_PATTERN = "/.claude-pr/";
|
||||||
|
|
||||||
|
function ensureClaudePrExcludedFromGit(): void {
|
||||||
|
const excludePath = execFileSync(
|
||||||
|
"git",
|
||||||
|
["rev-parse", "--git-path", "info/exclude"],
|
||||||
|
{ encoding: "utf8" },
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
const excludeContents = existsSync(excludePath)
|
||||||
|
? readFileSync(excludePath, "utf8")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (excludeContents.split(/\r?\n/).includes(CLAUDE_PR_EXCLUDE_PATTERN)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(dirname(excludePath), { recursive: true });
|
||||||
|
|
||||||
|
const prefix =
|
||||||
|
excludeContents.length === 0 || excludeContents.endsWith("\n") ? "" : "\n";
|
||||||
|
appendFileSync(excludePath, `${prefix}${CLAUDE_PR_EXCLUDE_PATTERN}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores security-sensitive config paths from the PR base branch.
|
* Restores security-sensitive config paths from the PR base branch.
|
||||||
*
|
*
|
||||||
@ -59,8 +91,9 @@ export function restoreConfigFromBase(baseBranch: string): void {
|
|||||||
}
|
}
|
||||||
if (existsSync(".claude-pr")) {
|
if (existsSync(".claude-pr")) {
|
||||||
console.log(
|
console.log(
|
||||||
"Preserved PR's sensitive paths → .claude-pr/ for review agents (not executed)",
|
"Preserved PR's sensitive paths -> .claude-pr/ for review agents (not executed)",
|
||||||
);
|
);
|
||||||
|
ensureClaudePrExcludedFromGit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete PR-controlled versions BEFORE fetching so the attacker-controlled
|
// Delete PR-controlled versions BEFORE fetching so the attacker-controlled
|
||||||
|
|||||||
169
test/restore-config.test.ts
Normal file
169
test/restore-config.test.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { execFileSync } from "child_process";
|
||||||
|
import {
|
||||||
|
existsSync,
|
||||||
|
mkdtempSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
rmSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "fs";
|
||||||
|
import { dirname, isAbsolute, join } from "path";
|
||||||
|
import { restoreConfigFromBase } from "../src/github/operations/restore-config";
|
||||||
|
|
||||||
|
const CLAUDE_PR_EXCLUDE_PATTERN = "/.claude-pr/";
|
||||||
|
|
||||||
|
describe("restoreConfigFromBase", () => {
|
||||||
|
let originalCwd: string;
|
||||||
|
let tempDir = "";
|
||||||
|
let repoDir: string;
|
||||||
|
let remoteDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalCwd = process.cwd();
|
||||||
|
tempDir = mkdtempSync(join("/tmp", "restore-config-"));
|
||||||
|
repoDir = join(tempDir, "repo");
|
||||||
|
remoteDir = join(tempDir, "origin.git");
|
||||||
|
|
||||||
|
execFileSync("git", ["init", "--bare", remoteDir], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["init", repoDir], { stdio: "pipe" });
|
||||||
|
git(["checkout", "-b", "main"]);
|
||||||
|
git(["config", "user.email", "test@example.com"]);
|
||||||
|
git(["config", "user.name", "Test User"]);
|
||||||
|
|
||||||
|
writeRepoFile("CLAUDE.md", "base claude instructions\n");
|
||||||
|
writeRepoFile(
|
||||||
|
".claude/settings.json",
|
||||||
|
`${JSON.stringify({ source: "base" })}\n`,
|
||||||
|
);
|
||||||
|
writeRepoFile("src/index.ts", "export const base = true;\n");
|
||||||
|
|
||||||
|
git(["add", "CLAUDE.md", ".claude/settings.json", "src/index.ts"]);
|
||||||
|
git(["commit", "-m", "base config"]);
|
||||||
|
git(["remote", "add", "origin", remoteDir]);
|
||||||
|
git(["push", "-u", "origin", "main"]);
|
||||||
|
|
||||||
|
git(["checkout", "-b", "pr"]);
|
||||||
|
writeRepoFile("CLAUDE.md", "pr claude instructions\n");
|
||||||
|
writeRepoFile(
|
||||||
|
".claude/settings.json",
|
||||||
|
`${JSON.stringify({ source: "pr" })}\n`,
|
||||||
|
);
|
||||||
|
git(["add", "CLAUDE.md", ".claude/settings.json"]);
|
||||||
|
git(["commit", "-m", "pr config"]);
|
||||||
|
|
||||||
|
process.chdir(repoDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
if (tempDir) {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves PR sensitive files while excluding .claude-pr from broad staging", () => {
|
||||||
|
const gitignoreExistedBefore = existsRepoFile(".gitignore");
|
||||||
|
const gitignoreContentsBefore = gitignoreExistedBefore
|
||||||
|
? readRepoFile(".gitignore")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
restoreConfigFromBase("main");
|
||||||
|
|
||||||
|
expect(readRepoFile(".claude-pr/CLAUDE.md")).toBe(
|
||||||
|
"pr claude instructions\n",
|
||||||
|
);
|
||||||
|
expect(readRepoFile(".claude-pr/.claude/settings.json")).toBe(
|
||||||
|
`${JSON.stringify({ source: "pr" })}\n`,
|
||||||
|
);
|
||||||
|
expect(readRepoFile("CLAUDE.md")).toBe("base claude instructions\n");
|
||||||
|
expect(readRepoFile(".claude/settings.json")).toBe(
|
||||||
|
`${JSON.stringify({ source: "base" })}\n`,
|
||||||
|
);
|
||||||
|
expect(git(["check-ignore", ".claude-pr/CLAUDE.md"]).trim()).toBe(
|
||||||
|
".claude-pr/CLAUDE.md",
|
||||||
|
);
|
||||||
|
expect(countClaudePrExcludeEntries()).toBe(1);
|
||||||
|
|
||||||
|
restoreConfigFromBase("main");
|
||||||
|
|
||||||
|
expect(countClaudePrExcludeEntries()).toBe(1);
|
||||||
|
expect(existsRepoFile(".gitignore")).toBe(gitignoreExistedBefore);
|
||||||
|
if (gitignoreExistedBefore) {
|
||||||
|
expect(readRepoFile(".gitignore")).toBe(gitignoreContentsBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeRepoFile("src/fix.ts", "export const fix = true;\n");
|
||||||
|
git(["add", "-A"]);
|
||||||
|
|
||||||
|
const stagedFiles = git(["diff", "--cached", "--name-only"])
|
||||||
|
.trim()
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter(Boolean);
|
||||||
|
expect(stagedFiles).toContain("src/fix.ts");
|
||||||
|
expect(stagedFiles.some((file) => file.startsWith(".claude-pr/"))).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
git(["commit", "-m", "apply fix"]);
|
||||||
|
|
||||||
|
const committedFiles = git(["show", "--name-only", "--format=", "HEAD"])
|
||||||
|
.trim()
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter(Boolean);
|
||||||
|
expect(committedFiles).toContain("src/fix.ts");
|
||||||
|
expect(committedFiles.some((file) => file.startsWith(".claude-pr/"))).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(existsRepoFile(".gitignore")).toBe(gitignoreExistedBefore);
|
||||||
|
if (gitignoreExistedBefore) {
|
||||||
|
expect(readRepoFile(".gitignore")).toBe(gitignoreContentsBefore);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not modify an existing .gitignore", () => {
|
||||||
|
writeRepoFile(".gitignore", "node_modules\n");
|
||||||
|
git(["add", ".gitignore"]);
|
||||||
|
git(["commit", "-m", "add gitignore"]);
|
||||||
|
|
||||||
|
const gitignoreBefore = readRepoFile(".gitignore");
|
||||||
|
|
||||||
|
restoreConfigFromBase("main");
|
||||||
|
|
||||||
|
expect(readRepoFile(".gitignore")).toBe(gitignoreBefore);
|
||||||
|
expect(countClaudePrExcludeEntries()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
function git(args: string[]): string {
|
||||||
|
return execFileSync("git", args, {
|
||||||
|
cwd: repoDir,
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRepoFile(path: string, contents: string): void {
|
||||||
|
const fullPath = join(repoDir, path);
|
||||||
|
mkdirSync(dirname(fullPath), { recursive: true });
|
||||||
|
writeFileSync(fullPath, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRepoFile(path: string): string {
|
||||||
|
return readFileSync(join(repoDir, path), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function existsRepoFile(path: string): boolean {
|
||||||
|
return existsSync(join(repoDir, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
function countClaudePrExcludeEntries(): number {
|
||||||
|
return readFileSync(getExcludePath(), "utf8")
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter((line) => line === CLAUDE_PR_EXCLUDE_PATTERN).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExcludePath(): string {
|
||||||
|
const gitPath = git(["rev-parse", "--git-path", "info/exclude"]).trim();
|
||||||
|
return isAbsolute(gitPath) ? gitPath : join(repoDir, gitPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user