Files
claude-code-action/test/modes/detector.test.ts
Ashwin Bhat 7e4bf87b1c feat: add ssh_signing_key input for SSH commit signing (#784)
* feat: add ssh_signing_key input for SSH commit signing

Add a new ssh_signing_key input that allows passing an SSH signing key
for commit signing, as an alternative to the existing use_commit_signing
(which uses GitHub API-based commits).

When ssh_signing_key is provided:
- Git is configured to use SSH signing (gpg.format=ssh, commit.gpgsign=true)
- The key is written to ~/.ssh/claude_signing_key with 0600 permissions
- Git CLI commands are used (not MCP file ops)
- The key is cleaned up in a post step for security

Behavior matrix:
| ssh_signing_key | use_commit_signing | Result |
|-----------------|-------------------|--------|
| not set         | false             | Regular git, no signing |
| not set         | true              | GitHub API (MCP), verified commits |
| set             | false             | Git CLI with SSH signing |
| set             | true              | Git CLI with SSH signing (ssh_signing_key takes precedence)

* docs: add SSH signing key documentation

- Update security.md with detailed setup instructions for both signing options
- Explain that ssh_signing_key enables full git CLI operations (rebasing, etc.)
- Add ssh_signing_key to inputs table in usage.md
- Update bot_id/bot_name descriptions to note they're needed for verified commits

* fix: address security review feedback for SSH signing

- Write SSH key atomically with mode 0o600 (fixes TOCTOU race condition)
- Create .ssh directory with mode 0o700 (SSH best practices)
- Add input validation for SSH key format
- Remove unused chmod import
- Add tests for validation logic
2026-01-02 10:37:25 -08:00

262 lines
7.8 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { detectMode } from "../../src/modes/detector";
import type { GitHubContext } from "../../src/github/context";
describe("detectMode with enhanced routing", () => {
const baseContext = {
runId: "test-run",
eventAction: "opened",
repository: {
owner: "test-owner",
repo: "test-repo",
full_name: "test-owner/test-repo",
},
actor: "test-user",
inputs: {
prompt: "",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
branchPrefix: "claude/",
useStickyComment: false,
useCommitSigning: false,
sshSigningKey: "",
botId: "123456",
botName: "claude-bot",
allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false,
includeFixLinks: true,
},
};
describe("PR Events with track_progress", () => {
it("should use tag mode when track_progress is true for pull_request.opened", () => {
const context: GitHubContext = {
...baseContext,
eventName: "pull_request",
eventAction: "opened",
payload: { pull_request: { number: 1 } } as any,
entityNumber: 1,
isPR: true,
inputs: { ...baseContext.inputs, trackProgress: true },
};
expect(detectMode(context)).toBe("tag");
});
it("should use tag mode when track_progress is true for pull_request.synchronize", () => {
const context: GitHubContext = {
...baseContext,
eventName: "pull_request",
eventAction: "synchronize",
payload: { pull_request: { number: 1 } } as any,
entityNumber: 1,
isPR: true,
inputs: { ...baseContext.inputs, trackProgress: true },
};
expect(detectMode(context)).toBe("tag");
});
it("should use agent mode when track_progress is false for pull_request.opened", () => {
const context: GitHubContext = {
...baseContext,
eventName: "pull_request",
eventAction: "opened",
payload: { pull_request: { number: 1 } } as any,
entityNumber: 1,
isPR: true,
inputs: { ...baseContext.inputs, trackProgress: false },
};
expect(detectMode(context)).toBe("agent");
});
it("should throw error when track_progress is used with unsupported PR action", () => {
const context: GitHubContext = {
...baseContext,
eventName: "pull_request",
eventAction: "closed",
payload: { pull_request: { number: 1 } } as any,
entityNumber: 1,
isPR: true,
inputs: { ...baseContext.inputs, trackProgress: true },
};
expect(() => detectMode(context)).toThrow(
/track_progress for pull_request events is only supported for actions/,
);
});
});
describe("Issue Events with track_progress", () => {
it("should use tag mode when track_progress is true for issues.opened", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "Test" } } as any,
entityNumber: 1,
isPR: false,
inputs: { ...baseContext.inputs, trackProgress: true },
};
expect(detectMode(context)).toBe("tag");
});
it("should use agent mode when track_progress is false for issues", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "Test" } } as any,
entityNumber: 1,
isPR: false,
inputs: { ...baseContext.inputs, trackProgress: false },
};
expect(detectMode(context)).toBe("agent");
});
it("should use agent mode for issues with explicit prompt", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "Test issue" } } as any,
entityNumber: 1,
isPR: false,
inputs: { ...baseContext.inputs, prompt: "Analyze this issue" },
};
expect(detectMode(context)).toBe("agent");
});
it("should use tag mode for issues with @claude mention and no prompt", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "@claude help" } } as any,
entityNumber: 1,
isPR: false,
};
expect(detectMode(context)).toBe("tag");
});
});
describe("Comment Events (unchanged behavior)", () => {
it("should use tag mode for issue_comment with @claude mention", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issue_comment",
payload: {
issue: { number: 1, body: "Test" },
comment: { body: "@claude help" },
} as any,
entityNumber: 1,
isPR: false,
};
expect(detectMode(context)).toBe("tag");
});
it("should use agent mode for issue_comment with prompt provided", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issue_comment",
payload: {
issue: { number: 1, body: "Test" },
comment: { body: "@claude help" },
} as any,
entityNumber: 1,
isPR: false,
inputs: { ...baseContext.inputs, prompt: "Review this PR" },
};
expect(detectMode(context)).toBe("agent");
});
it("should use tag mode for PR review comments with @claude mention", () => {
const context: GitHubContext = {
...baseContext,
eventName: "pull_request_review_comment",
payload: {
pull_request: { number: 1, body: "Test" },
comment: { body: "@claude check this" },
} as any,
entityNumber: 1,
isPR: true,
};
expect(detectMode(context)).toBe("tag");
});
});
describe("Automation Events (should error with track_progress)", () => {
it("should throw error when track_progress is used with workflow_dispatch", () => {
const context: GitHubContext = {
...baseContext,
eventName: "workflow_dispatch",
payload: {} as any,
inputs: { ...baseContext.inputs, trackProgress: true },
};
expect(() => detectMode(context)).toThrow(
/track_progress is only supported /,
);
});
it("should use agent mode for workflow_dispatch without track_progress", () => {
const context: GitHubContext = {
...baseContext,
eventName: "workflow_dispatch",
payload: {} as any,
inputs: { ...baseContext.inputs, prompt: "Run workflow" },
};
expect(detectMode(context)).toBe("agent");
});
});
describe("Custom prompt injection in tag mode", () => {
it("should use tag mode for PR events when both track_progress and prompt are provided", () => {
const context: GitHubContext = {
...baseContext,
eventName: "pull_request",
eventAction: "opened",
payload: { pull_request: { number: 1 } } as any,
entityNumber: 1,
isPR: true,
inputs: {
...baseContext.inputs,
trackProgress: true,
prompt: "Review for security issues",
},
};
expect(detectMode(context)).toBe("tag");
});
it("should use tag mode for issue events when both track_progress and prompt are provided", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "Test" } } as any,
entityNumber: 1,
isPR: false,
inputs: {
...baseContext.inputs,
trackProgress: true,
prompt: "Analyze this issue",
},
};
expect(detectMode(context)).toBe("tag");
});
});
});